From acd6cd0b93e316887436b549fcece827519d2d08 Mon Sep 17 00:00:00 2001 From: Sergei Zhekpisov <7286747+vertrost@users.noreply.github.com> Date: Mon, 9 Jun 2025 13:31:53 +0100 Subject: [PATCH 1/3] upgrade authlib --- aleph/oauth.py | 43 +++++++++++++++++++++++++++----- aleph/tests/test_sessions_api.py | 2 +- aleph/views/sessions_api.py | 29 +++++++++++---------- requirements.txt | 2 +- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/aleph/oauth.py b/aleph/oauth.py index bf11ff0bfd..3d371a849d 100644 --- a/aleph/oauth.py +++ b/aleph/oauth.py @@ -41,10 +41,16 @@ def load_key(header, payload): return jwk_set.find_by_kid(header.get("kid")) metadata = provider.load_server_metadata() - algs = metadata.get("id_token_signing_alg_values_supported", ["RS256"]) + # Use a wider range of supported algorithms for better compatibility + algs = metadata.get("id_token_signing_alg_values_supported", ["RS256", "HS256", "ES256"]) jwt = JsonWebToken(algs) claims = {"exp": {"essential": True}} - return jwt.decode(token, key=load_key, claims_options=claims) + try: + return jwt.decode(token, key=load_key, claims_options=claims) + except Exception as e: + # If decoding fails, log the error and return empty dict + log.warning("Failed to decode access token: %r", e) + return {} def _get_groups(provider, oauth_token, id_token): @@ -83,12 +89,37 @@ def _get_groups(provider, oauth_token, id_token): def handle_oauth(provider, oauth_token): from aleph.model import Role, RoleBlockedError - token = provider.parse_id_token(oauth_token) - if token is None: - raise OAuthError() + # Extract ID token directly if it's available + id_token_value = oauth_token.get("id_token") + if id_token_value: + try: + # In Authlib 1.6.0, parse_id_token requires a nonce parameter + token = provider.parse_id_token(oauth_token, nonce=None) + except Exception as e: + log.warning("Failed to parse ID token: %r", e) + # Extract claims directly from the access token instead + token = _parse_access_token(provider, oauth_token) + else: + # If no ID token is available, use the access token + token = _parse_access_token(provider, oauth_token) + + if not token: + raise OAuthError("No valid token found") + name = token.get("name", token.get("given_name")) email = token.get("email", token.get("upn")) - role_id = "%s:%s" % (SETTINGS.OAUTH_HANDLER, token.get("sub", email)) + # Fallback to preferred_username if email is not available + if not email: + email = token.get("preferred_username") + + if not name and not email: + raise OAuthError("No user information found in token") + + subject = token.get("sub", email) + if not subject: + raise OAuthError("No subject identifier found in token") + + role_id = "%s:%s" % (SETTINGS.OAUTH_HANDLER, subject) role = Role.by_foreign_id(role_id) if SETTINGS.OAUTH_MIGRATE_SUB and role is None: role = Role.by_email(email) diff --git a/aleph/tests/test_sessions_api.py b/aleph/tests/test_sessions_api.py index 7a36321609..d5ec9ab266 100644 --- a/aleph/tests/test_sessions_api.py +++ b/aleph/tests/test_sessions_api.py @@ -361,7 +361,7 @@ def test_oauth_callback_sync_groups(self): def mock_oauth_token_exchange(name: str, email: str, groups: List[str] = []): patch_send = mock.patch("requests.sessions.Session.send") patch_parse = mock.patch( - "authlib.integrations.flask_client.remote_app.FlaskRemoteApp.parse_id_token" + "authlib.integrations.flask_client.remote_app.RemoteApp.parse_id_token" ) with patch_send as send_mock, patch_parse as parse_mock: diff --git a/aleph/views/sessions_api.py b/aleph/views/sessions_api.py index 86b2734925..d27e64c9b5 100644 --- a/aleph/views/sessions_api.py +++ b/aleph/views/sessions_api.py @@ -107,29 +107,27 @@ def oauth_init(): """ require(SETTINGS.OAUTH) url = url_for(".oauth_callback") - state = oauth.provider.create_authorization_url(url) - state["next_url"] = request.args.get("next", request.referrer) - state["redirect_uri"] = url - cache.set_complex(_oauth_session(state.get("state")), state, expires=3600) - return redirect(state["url"]) + redirect_uri = url + + # Save the next URL in the session to retrieve after callback + next_url = request.args.get("next", request.referrer) + session["next_url"] = next_url + + # Let Authlib handle the redirect with state management + return oauth.provider.authorize_redirect(redirect_uri) @blueprint.route("/api/2/sessions/callback") def oauth_callback(): require(SETTINGS.OAUTH) err = Unauthorized(gettext("Authentication has failed.")) - state = cache.get_complex(_oauth_session(request.args.get("state"))) - if state is None: - AUTH_ATTEMPTS.labels(method="oauth", result="failed").inc() - raise err try: - oauth.provider.framework.set_session_data(request, "state", state.get("state")) - uri = state.get("redirect_uri") - oauth_token = oauth.provider.authorize_access_token(redirect_uri=uri) - except AuthlibBaseError as err: + # Let Authlib handle the token exchange + oauth_token = oauth.provider.authorize_access_token() + except AuthlibBaseError as e: AUTH_ATTEMPTS.labels(method="oauth", result="failed").inc() - log.warning("Failed OAuth: %r", err) + log.warning("Failed OAuth: %r", e) raise err if oauth_token is None or isinstance(oauth_token, AuthlibBaseError): AUTH_ATTEMPTS.labels(method="oauth", result="failed").inc() @@ -158,7 +156,8 @@ def oauth_callback(): if id_token is not None: cache.set(_token_session(token), id_token, expires=SETTINGS.SESSION_EXPIRE) - next_path = get_url_path(state.get("next_url")) + # Get the next URL from the session + next_path = get_url_path(session.get("next_url")) next_url = ui_url("oauth", next=next_path) next_url = f"{next_url}#token={token}" session.clear() diff --git a/requirements.txt b/requirements.txt index 6d4b7e9465..bc465104f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ Flask-Babel==4.0.0 flask-talisman==1.1.0 SQLAlchemy==2.0.21 alembic==1.13.1 -authlib==0.15.5 +authlib==1.6.0 elasticsearch==7.17.0 marshmallow==3.21.1 From c6934bb93bbef8a1fe2b471ad8b554dbdceae8b1 Mon Sep 17 00:00:00 2001 From: Sergei Zhekpisov <7286747+vertrost@users.noreply.github.com> Date: Mon, 9 Jun 2025 14:23:26 +0100 Subject: [PATCH 2/3] restore parameter --- aleph/tests/test_sessions_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleph/tests/test_sessions_api.py b/aleph/tests/test_sessions_api.py index d5ec9ab266..7a36321609 100644 --- a/aleph/tests/test_sessions_api.py +++ b/aleph/tests/test_sessions_api.py @@ -361,7 +361,7 @@ def test_oauth_callback_sync_groups(self): def mock_oauth_token_exchange(name: str, email: str, groups: List[str] = []): patch_send = mock.patch("requests.sessions.Session.send") patch_parse = mock.patch( - "authlib.integrations.flask_client.remote_app.RemoteApp.parse_id_token" + "authlib.integrations.flask_client.remote_app.FlaskRemoteApp.parse_id_token" ) with patch_send as send_mock, patch_parse as parse_mock: From 18018ad78f44b935bf7287b9b03d415e64c98a02 Mon Sep 17 00:00:00 2001 From: Sergei Zhekpisov <7286747+vertrost@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:25:09 +0100 Subject: [PATCH 3/3] fix tests --- aleph/tests/test_sessions_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aleph/tests/test_sessions_api.py b/aleph/tests/test_sessions_api.py index 7a36321609..633c111703 100644 --- a/aleph/tests/test_sessions_api.py +++ b/aleph/tests/test_sessions_api.py @@ -360,14 +360,15 @@ def test_oauth_callback_sync_groups(self): @contextmanager def mock_oauth_token_exchange(name: str, email: str, groups: List[str] = []): patch_send = mock.patch("requests.sessions.Session.send") - patch_parse = mock.patch( - "authlib.integrations.flask_client.remote_app.FlaskRemoteApp.parse_id_token" + patch_parse = mock.patch.object( + oauth.provider, "parse_id_token" ) with patch_send as send_mock, patch_parse as parse_mock: send_mock.return_value = mock.Mock( spec=Response, json=lambda: {"id_token": "fake_token"}, + status_code=200, # Add the status_code attribute ) # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims