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')