From f990144cc44f8a86192ed2c5a54a65da654e7f01 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Jun 2024 15:33:41 +0200 Subject: [PATCH] Add support for logout from OAuth provider --- panel/_templates/logout.html | 3 ++ panel/auth.py | 59 +++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/panel/_templates/logout.html b/panel/_templates/logout.html index 9a033043fe..3842d9fa70 100644 --- a/panel/_templates/logout.html +++ b/panel/_templates/logout.html @@ -84,6 +84,9 @@

Successfully logged out.

+ {% if SIGNOUT_ENDPOINT -%} + + {% endif %}
diff --git a/panel/auth.py b/panel/auth.py index a68a50c41a..e5549b0a16 100644 --- a/panel/auth.py +++ b/panel/auth.py @@ -117,6 +117,10 @@ def _SCOPE(self): return self._DEFAULT_SCOPES return [scope for scope in os.environ['PANEL_OAUTH_SCOPE'].split(',')] + @property + def _OAUTH_LOGOUT_URL(self): + return None + async def get_authenticated_user(self, redirect_uri, client_id, state, client_secret=None, code=None): """ @@ -496,6 +500,21 @@ def _OAUTH_ACCESS_TOKEN_URL(self): def _OAUTH_AUTHORIZE_URL(self): return config.oauth_extra_params.get('AUTHORIZE_URL', os.environ.get('PANEL_OAUTH_AUTHORIZE_URL')) + @property + def _OAUTH_LOGOUT_URL(self): + id_token = (self.get_secure_cookie('id_token', max_age_days=config.oauth_expiry) or b'').decode('utf8', 'replace') + id_token = state._decrypt_cookie(id_token) + if config.oauth_redirect_uri: + redirect_uri = config.oauth_redirect_uri + else: + redirect_uri = f"{self.request.protocol}://{self.request.host}{self._login_endpoint}" + return config.oauth_extra_params.get('logout_url', os.environ.get('PANEL_OAUTH_LOGOUT_URL')).format( + id_token_hint=id_token, + post_logout_redirect_uri=redirect_uri, + redirect_uri=redirect_uri, + client_id=config.oauth_key, + ) + @property def _OAUTH_USER_URL(self): return config.oauth_extra_params.get('USER_URL', os.environ.get('PANEL_OAUTH_USER_URL')) @@ -689,6 +708,7 @@ class AzureAdLoginHandler(OAuthLoginHandler): _OAUTH_ACCESS_TOKEN_URL_ = 'https://login.microsoftonline.com/{tenant}/oauth2/token' _OAUTH_AUTHORIZE_URL_ = 'https://login.microsoftonline.com/{tenant}/oauth2/authorize' + _OAUTH_logout_URL_ = 'https://login.microsoftonline.com/{tenant}/oauth2/logout' _OAUTH_USER_URL_ = '' _USER_KEY = 'unique_name' @@ -752,6 +772,8 @@ class OktaLoginHandler(OAuthLoginHandler): _OAUTH_ACCESS_TOKEN_URL__ = 'https://{0}/oauth2/v1/token' _OAUTH_AUTHORIZE_URL_ = 'https://{0}/oauth2/{1}/v1/authorize' _OAUTH_AUTHORIZE_URL__ = 'https://{0}/oauth2/v1/authorize' + _OAUTH_LOGOUT_URL_ = 'https://{0}/oauth2/{1}/v1/logout?id_token_hint={2}&post_logout_redirect_uri={3}' + _OAUTH_LOGOUT_URL__ = 'https://{0}/oauth2/v1/logout?id_token_hint={2}&post_logout_redirect_uri={3}' _OAUTH_USER_URL_ = 'https://{0}/oauth2/{1}/v1/userinfo?access_token=' _OAUTH_USER_URL__ = 'https://{0}/oauth2/v1/userinfo?access_token=' @@ -775,6 +797,23 @@ def _OAUTH_AUTHORIZE_URL(self): else: return self._OAUTH_AUTHORIZE_URL__.format(url) + @property + def _OAUTH_LOGOUT_URL(self): + id_token = (self.get_secure_cookie('id_token', max_age_days=config.oauth_expiry) or b'').decode('utf8', 'replace') + if id_token is None: + return None + id_token = state._decrypt_cookie(id_token) + url = config.oauth_extra_params.get('url', 'okta.com') + server = config.oauth_extra_params.get('server', 'default') + if config.oauth_redirect_uri: + redirect_uri = config.oauth_redirect_uri + else: + redirect_uri = f"{self.request.protocol}://{self.request.host}{self._login_endpoint}" + if server: + return self._OAUTH_LOGOUT_URL_.format(url, server, id_token, redirect_uri) + else: + return self._OAUTH_LOGOUT_URL__.format(url, id_token, redirect_uri) + @property def _OAUTH_USER_URL(self): url = config.oauth_extra_params.get('url', 'okta.com') @@ -862,7 +901,18 @@ class LogoutHandler(tornado.web.RequestHandler): _logout_template = LOGOUT_TEMPLATE + _login_handler = None + def get(self): + # Logout URL may need cookies to generate so we have to run it before clearing cookies + if self._login_handler: + handler = self._login_handler(self.application, self.request) + try: + signout_url = handler._OAUTH_LOGOUT_URL + except Exception: + signout_url = None + else: + signout_url = None self.clear_cookie("user") self.clear_cookie("id_token") self.clear_cookie("access_token") @@ -871,7 +921,8 @@ def get(self): self.clear_cookie(STATE_COOKIE_NAME) html = self._logout_template.render( PANEL_CDN=CDN_DIST, - LOGIN_ENDPOINT=self._login_endpoint + LOGIN_ENDPOINT=self._login_endpoint, + SIGNOUT_ENDPOINT=signout_url ) self.write(html) @@ -1090,6 +1141,12 @@ def login_handler(self): handler._login_endpoint = self._login_endpoint return handler + @property + def logout_handler(self): + handler = super().logout_handler + handler._login_handler = self.login_handler + return handler + def _remove_user(self, session_context): guest_cookie = session_context.request.cookies.get('is_guest') user_cookie = session_context.request.cookies.get('user')