From f1f7f693da8cc834b5c24dccbf5f61033316640e Mon Sep 17 00:00:00 2001 From: Marcus Oskarsson Date: Wed, 4 Sep 2024 15:47:47 +0200 Subject: [PATCH] [SSO] - Handling sso logout --- demo/mxcube-web/server.yaml | 1 + mxcubeweb/core/components/user/usermanager.py | 45 ++++++++++++------- mxcubeweb/core/models/configmodels.py | 5 ++- mxcubeweb/core/models/usermodels.py | 1 + mxcubeweb/routes/login.py | 15 +++++-- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/demo/mxcube-web/server.yaml b/demo/mxcube-web/server.yaml index 016449b54..b42b24a3b 100644 --- a/demo/mxcube-web/server.yaml +++ b/demo/mxcube-web/server.yaml @@ -19,6 +19,7 @@ server: sso: ISSUER: https://websso.esrf.fr/realms/ESRF/ LOGOUT_URI: https://websso.esrf.fr/auth/realms/ESRF/protocol/openid-connect/logout + TOKEN_INFO_URI: https://websso.esrf.fr/auth/realms/ESRF/protocol/openid-connect/token CLIENT_SECRET: 95nOugpRxwF3ttXxYnXFiK6bou5wtSP1 CLIENT_ID: mxcube SCOPE: openid email profile diff --git a/mxcubeweb/core/components/user/usermanager.py b/mxcubeweb/core/components/user/usermanager.py index 4de80172d..f8dbb5f6a 100644 --- a/mxcubeweb/core/components/user/usermanager.py +++ b/mxcubeweb/core/components/user/usermanager.py @@ -2,6 +2,7 @@ import uuid import datetime import requests +import json import flask import flask_security @@ -24,18 +25,11 @@ def __init__(self, app, config): super().__init__(app, config) self.oauth_client = OAuth(app=app.server.flask) - self.oauth_issuer = self.app.CONFIG.sso.ISSUER - self.oauth_logout_url = self.app.CONFIG.sso.LOGOUT_URI - self.oauth_client_secret = self.app.CONFIG.sso.CLIENT_SECRET - self.oauth_client_id = self.app.CONFIG.sso.CLIENT_ID - self.oauth_client.register( name="keycloak", - client_id=self.oauth_client_id, - client_secret=self.oauth_client_secret, - server_metadata_url=( - "https://websso.esrf.fr/realms/ESRF/.well-known/openid-configuration" - ), + client_id=self.app.CONFIG.sso.CLIENT_ID, + client_secret=self.app.CONFIG.sso.CLIENT_SECRET, + server_metadata_url=self.app.CONFIG.sso.META_DATA_URI, client_kwargs={ "scope": "openid email profile", "code_challenge_method": "S256", # enable PKCE @@ -172,6 +166,25 @@ def sso_validate(self) -> str: else: self.login(username, token, sso_data=token_response) + def sso_token_expired(self) -> bool: + res = json.loads( + requests.post( + self.app.CONFIG.sso.TOKEN_INFO_URI, + headers={"Authorization": "Bearer %s" % current_user.token}, + data={ + "grant_type": "refresh_token", + "refresh_token": current_user.refresh_token, + }, + ).json() + ) + + return "access_token" not in res + + def handle_sso_logout(self): + if current_user.is_anonymous: + if self.sso_token_expired(): + self.signout() + def login(self, login_id: str, password: str, sso_data: dict = {}): try: sessionManager: LimsSessionManager = self._login(login_id, password) @@ -334,13 +347,15 @@ def db_create_user( nickname=user, session_id=sid, selected_proposal=selected_proposal, - limsdata=lims_data.json(), # json.dumps(lims_data), + limsdata=lims_data.json(), refresh_token=sso_data.get("refresh_token", str(uuid.uuid4())), + token=sso_data.get("token", str(uuid.uuid4())), roles=self._get_configured_roles(user), ) else: _u.limsdata = lims_data.json() # json.dumps(lims_data) - _u.refresh_token = sso_data.get("refresh_token", "") + _u.refresh_token = sso_data.get("refresh_token", str(uuid.uuid4())) + _u.token = sso_data.get("token", str(uuid.uuid4())) user_datastore.append_roles(_u, self._get_configured_roles(user)) self.app.server.user_datastore.commit() @@ -455,10 +470,10 @@ def _login(self, login_id: str, password: str) -> LimsSessionManager: def _signout(self): requests.post( - self.oauth_logout_url, + self.app.CONFIG.sso.LOGOUT_URI, data={ - "client_id": self.oauth_client_id, - "client_secret": self.oauth_client_secret, + "client_id": self.app.CONFIG.sso.CLIENT_ID, + "client_secret": self.app.CONFIG.sso.CLIENT_SECRET, "refresh_token": current_user.refresh_token, }, ) diff --git a/mxcubeweb/core/models/configmodels.py b/mxcubeweb/core/models/configmodels.py index 610273f18..6e792ab30 100644 --- a/mxcubeweb/core/models/configmodels.py +++ b/mxcubeweb/core/models/configmodels.py @@ -32,6 +32,9 @@ class SSOConfigModel(BaseModel): LOGOUT_URI: str = Field("", description="OpenIDConnect / OAuth logout URI") CLIENT_SECRET: str = Field("", description="OpenIDConnect / OAuth client secret") CLIENT_ID: str = Field("", description="OpenIDConnect / OAuth client id") + META_DATA_URI: str = Field( + "", description="OpenIDConnect / OAuth .well-known configuration" + ) SCOPE: str = Field( "openid email profile", description="OpenIDConnect / OAuth scope" ) @@ -139,4 +142,4 @@ class MXCUBEAppConfigModel(BaseModel): class AppConfigModel(BaseModel): server: FlaskConfigModel mxcube: MXCUBEAppConfigModel - sso: SSOConfigModel + sso: Optional[SSOConfigModel] diff --git a/mxcubeweb/core/models/usermodels.py b/mxcubeweb/core/models/usermodels.py index 21e9b12b4..99264cbae 100644 --- a/mxcubeweb/core/models/usermodels.py +++ b/mxcubeweb/core/models/usermodels.py @@ -76,6 +76,7 @@ class User(Base, UserMixin): limsdata = Column(JSON, unique=False) last_request_timestamp = Column(DateTime()) refresh_token = Column(String(255), unique=True) + token = Column(String(255), unique=True) roles = relationship( "Role", secondary="roles_users", diff --git a/mxcubeweb/routes/login.py b/mxcubeweb/routes/login.py index 8742d1a31..1f9b62a93 100644 --- a/mxcubeweb/routes/login.py +++ b/mxcubeweb/routes/login.py @@ -36,7 +36,8 @@ def login(): try: app.usermanager.login(login_id, password) except BaseException as ex: - import sys, traceback + import sys + import traceback traceback.print_exc(file=sys.stdout) msg = "[LOGIN] User %s could not login" % login_id @@ -55,8 +56,14 @@ def login(): return res + @bp.route("/sso_post_logout", methods=["GET"]) + @server.restrict + def ssosignout(): + app.usermanager.signout() + return redirect("/") + @bp.route("/ssologin", methods=["GET"]) - def ssosingin(): + def ssosignin(): redirect_uri = url_for("login.auth", _external=True) response = app.usermanager.oauth_client.keycloak.authorize_redirect( redirect_uri @@ -103,12 +110,11 @@ def login_info(): 200: Error, could not log in, {"loggedIn": False} """ try: - res = app.usermanager.login_info() response = jsonify(res) session.permanent = True - except Exception as e: + except Exception: response = make_response(jsonify({"loggedIn": False}), 200) return response @@ -127,6 +133,7 @@ def refresh_session(): # Since default value of `SESSION_REFRESH_EACH_REQUEST` config setting is `True` # there is no need to do anything to refresh the session. app.usermanager.update_active_users() + app.usermanager.handle_sso_logout() return make_response("", 200) return bp