Skip to content

Commit

Permalink
Merge pull request galaxyproject#9162 from machristie/idp-logout
Browse files Browse the repository at this point in the history
Add support for logging out of OIDC Identity Provider
  • Loading branch information
dannon authored and almahmoud committed Mar 4, 2020
1 parent cf3419c commit 6abc96f
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 11 deletions.
26 changes: 20 additions & 6 deletions client/galaxy/scripts/layout/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,26 @@ function logoutClick() {
const galaxy = getGalaxyInstance();
const session_csrf_token = galaxy.session_csrf_token;
const url = `${galaxy.root}user/logout?session_csrf_token=${session_csrf_token}`;
axios.get(url).then(() => {
if (galaxy.user) {
galaxy.user.clearSessionStorage();
}
window.top.location.href = `${galaxy.root}root/login?is_logout_redirect=true`;
});
axios
.get(url)
.then(() => {
if (galaxy.user) {
galaxy.user.clearSessionStorage();
}
// Check if we need to logout of OIDC IDP
if (galaxy.config.enable_oidc) {
return axios.get(`${galaxy.root}authnz/logout`);
} else {
return {};
}
})
.then(response => {
if (response.data && response.data.redirect_uri) {
window.top.location.href = response.data.redirect_uri;
} else {
window.top.location.href = `${galaxy.root}root/login?is_logout_redirect=true`;
}
});
}

const Collection = Backbone.Collection.extend({
Expand Down
13 changes: 13 additions & 0 deletions lib/galaxy/authnz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,16 @@ def callback(self, state_token, authz_code, trans, login_redirect_url):

def disconnect(self, provider, trans, disconnect_redirect_url=None):
raise NotImplementedError()

def logout(self, trans, post_logout_redirect_url=None):
"""
Return a URL that will log the user out of the IDP. In OIDC this is
called the 'end_session_endpoint'.
:type trans: GalaxyWebTransaction
:param trans: Galaxy web transaction.
:type trans: string
:param trans: Optional URL to redirect to after logging out of IDP.
"""
raise NotImplementedError()
13 changes: 13 additions & 0 deletions lib/galaxy/authnz/custos_authnz.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import requests
from oauthlib.common import generate_nonce
from requests_oauthlib import OAuth2Session
from six.moves.urllib.parse import quote

from galaxy import util
from galaxy.model import CustosAuthnzToken, User
Expand Down Expand Up @@ -39,6 +40,7 @@ def __init__(self, provider, oidc_config, oidc_backend_config):
self.config['authorization_endpoint'] = well_known_oidc_config['authorization_endpoint']
self.config['token_endpoint'] = well_known_oidc_config['token_endpoint']
self.config['userinfo_endpoint'] = well_known_oidc_config['userinfo_endpoint']
self.config['end_session_endpoint'] = well_known_oidc_config['end_session_endpoint']
else:
realm = oidc_backend_config['realm']
self._load_config_for_provider_and_realm(self.config['provider'], realm)
Expand Down Expand Up @@ -142,6 +144,16 @@ def disconnect(self, provider, trans, disconnect_redirect_url=None):
except Exception as e:
return False, "Failed to disconnect provider {}: {}".format(provider, util.unicodify(e)), None

def logout(self, trans, post_logout_redirect_url=None):
try:
redirect_url = self.config['end_session_endpoint']
if post_logout_redirect_url is not None:
redirect_url += "?redirect_uri={}".format(quote(post_logout_redirect_url))
return redirect_url
except Exception as e:
log.error("Failed to generate logout redirect_url", exc_info=e)
return None

def _create_oauth2_session(self, state=None, scope=None):
client_id = self.config['client_id']
redirect_uri = self.config['redirect_uri']
Expand Down Expand Up @@ -191,6 +203,7 @@ def _load_config_for_provider_and_realm(self, provider, realm):
self.config['authorization_endpoint'] = well_known_oidc_config['authorization_endpoint']
self.config['token_endpoint'] = well_known_oidc_config['token_endpoint']
self.config['userinfo_endpoint'] = well_known_oidc_config['userinfo_endpoint']
self.config['end_session_endpoint'] = well_known_oidc_config['end_session_endpoint']

def _get_well_known_uri_for_provider_and_realm(self, provider, realm):
# TODO: Look up this URL from a Python library
Expand Down
37 changes: 34 additions & 3 deletions lib/galaxy/authnz/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

from galaxy import exceptions
from galaxy import model
from galaxy.util import string_as_bool
from galaxy.util import asbool, string_as_bool
from galaxy.util import unicodify
from .custos_authnz import CustosAuthnz
from .psa_authnz import (
Expand Down Expand Up @@ -117,7 +117,8 @@ def _parse_idp_config(self, config_xml):
rtv = {
'client_id': config_xml.find('client_id').text,
'client_secret': config_xml.find('client_secret').text,
'redirect_uri': config_xml.find('redirect_uri').text}
'redirect_uri': config_xml.find('redirect_uri').text,
'enable_idp_logout': asbool(config_xml.findtext('enable_idp_logout', 'false'))}
if config_xml.find('prompt') is not None:
rtv['prompt'] = config_xml.find('prompt').text
return rtv
Expand All @@ -128,7 +129,8 @@ def _parse_custos_config(self, config_xml):
'client_id': config_xml.find('client_id').text,
'client_secret': config_xml.find('client_secret').text,
'redirect_uri': config_xml.find('redirect_uri').text,
'realm': config_xml.find('realm').text}
'realm': config_xml.find('realm').text,
'enable_idp_logout': asbool(config_xml.findtext('enable_idp_logout', 'false'))}
if config_xml.find('well_known_oidc_config_uri') is not None:
rtv['well_known_oidc_config_uri'] = config_xml.find('well_known_oidc_config_uri').text
if config_xml.find('idphint') is not None:
Expand Down Expand Up @@ -262,6 +264,35 @@ def callback(self, provider, state_token, authz_code, trans, login_redirect_url)
log.exception(msg)
return False, msg, (None, None)

def logout(self, provider, trans, post_logout_redirect_url=None):
"""
Log the user out of the identity provider.
:type provider: string
:param provider: set the name of the identity provider.
:type trans: GalaxyWebTransaction
:param trans: Galaxy web transaction.
:type post_logout_redirect_url: string
:param post_logout_redirect_url: (Optional) URL for identity provider
to redirect to after logging user out.
:return: a tuple (success boolean, message, redirect URI)
"""
try:
# check if logout is enabled for this idp and return false if not
unified_provider_name = self._unify_provider_name(provider)
if self.oidc_backends_config[unified_provider_name]['enable_idp_logout'] is False:
return False, "IDP logout is not enabled for {}".format(provider), None

success, message, backend = self._get_authnz_backend(provider)
if success is False:
return False, message, None
return True, message, backend.logout(trans, post_logout_redirect_url)
except Exception as e:
msg = 'The following error occurred when logging out from `{}` identity provider: ' \
'{}'.format(provider, e.message)
log.exception(msg)
return False, msg, None

def disconnect(self, provider, trans, disconnect_redirect_url=None):
try:
success, message, backend = self._get_authnz_backend(provider)
Expand Down
2 changes: 2 additions & 0 deletions lib/galaxy/config/sample/oidc_backends_config.xml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Please mind `http` and `https`.
<!-- <well_known_oidc_config_uri>https://.../.well-known/openid-configuration</well_known_oidc_config_uri> -->
<!-- (Optional) Override the default idp hint -->
<!-- <idphint>cilogon</idphint> -->
<!-- (Optional) Enable logging out of the IDP when user logs out of Galaxy -->
<!-- <enable_idp_logout>true</enable_idp_logout> -->
</provider>
<!-- Documentation: https://galaxyproject.org/authnz/config/oidc/idps/elixir-aai -->
<provider name="Elixir">
Expand Down
3 changes: 3 additions & 0 deletions lib/galaxy/webapps/galaxy/buildapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ def app_factory(global_conf, load_app_kwds={}, **kwargs):
webapp.add_route('/authnz/{provider}/login', controller='authnz', action='login', provider=None)
webapp.add_route('/authnz/{provider}/callback', controller='authnz', action='callback', provider=None)
webapp.add_route('/authnz/{provider}/disconnect', controller='authnz', action='disconnect', provider=None)
webapp.add_route('/authnz/{provider}/logout', controller='authnz', action='logout', provider=None)
# Returns the provider specific logout url for currently logged in provider
webapp.add_route('/authnz/logout', controller='authnz', action='get_logout_url')

# These two routes handle our simple needs at the moment
webapp.add_route('/async/{tool_id}/{data_id}/{data_secret}', controller='async', action='index', tool_id=None, data_id=None, data_secret=None)
Expand Down
21 changes: 21 additions & 0 deletions lib/galaxy/webapps/galaxy/controllers/authnz.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

log = logging.getLogger(__name__)

PROVIDER_COOKIE_NAME = 'oidc-provider'


class OIDC(JSAppLauncher):

Expand Down Expand Up @@ -79,6 +81,8 @@ def callback(self, trans, provider, **kwargs):
"identity provider. Please try again, and if the problem persists, "
"contact the Galaxy instance admin.".format(provider))
trans.handle_user_login(user)
# Record which idp provider was logged into, so we can logout of it later
trans.set_cookie(value=provider, name=PROVIDER_COOKIE_NAME)
return trans.response.send_redirect(url_for('/'))

@web.expose
Expand All @@ -95,3 +99,20 @@ def disconnect(self, trans, provider, **kwargs):
if redirect_url is None:
redirect_url = url_for('/')
return trans.response.send_redirect(redirect_url)

@web.json
def logout(self, trans, provider, **kwargs):
post_logout_redirect_url = trans.request.base + url_for('/') + 'root/login?is_logout_redirect=true'
success, message, redirect_uri = trans.app.authnz_manager.logout(provider,
trans,
post_logout_redirect_url=post_logout_redirect_url)
if success:
return {'redirect_uri': redirect_uri}
else:
return {'message': message}

@web.expose
def get_logout_url(self, trans, **kwargs):
idp_provider = trans.get_cookie(name=PROVIDER_COOKIE_NAME)
if idp_provider:
return trans.response.send_redirect(url_for(controller='authnz', action='logout', provider=idp_provider))
12 changes: 10 additions & 2 deletions test/unit/authnz/test_custos_authnz.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import jwt
import requests
from six.moves.urllib.parse import parse_qs, urlparse
from six.moves.urllib.parse import parse_qs, quote, urlparse

from galaxy.authnz import custos_authnz
from galaxy.model import CustosAuthnzToken, User
Expand All @@ -33,7 +33,8 @@ def setUp(self):
requests.get = self.mockRequest(self._get_idp_url(), {
"authorization_endpoint": "https://test-auth-endpoint",
"token_endpoint": "https://test-token-endpoint",
"userinfo_endpoint": "https://test-userinfo-endpoint"
"userinfo_endpoint": "https://test-userinfo-endpoint",
"end_session_endpoint": "https://test-end-session-endpoint"
})
self.custos_authnz = custos_authnz.CustosAuthnz('Custos', {
'VERIFY_SSL': True
Expand Down Expand Up @@ -476,3 +477,10 @@ def test_disconnect_when_more_than_one_associated_token_for_provider(self):
self.assertFalse(success)
self.assertNotEqual("", message)
self.assertIsNone(redirect_uri)

def test_logout_with_redirect(self):

logout_redirect_url = "http://localhost:8080/post-logout"
redirect_url = self.custos_authnz.logout(self.trans, logout_redirect_url)

self.assertEqual(redirect_url, "https://test-end-session-endpoint?redirect_uri=" + quote(logout_redirect_url))

0 comments on commit 6abc96f

Please sign in to comment.