diff --git a/swh/auth/django/backends.py b/swh/auth/django/backends.py --- a/swh/auth/django/backends.py +++ b/swh/auth/django/backends.py @@ -106,7 +106,8 @@ def get_user(self, user_id: int) -> Optional[OIDCUser]: # get oidc profile from cache oidc_client = keycloak_oidc_client() - oidc_profile = cache.get(oidc_profile_cache_key(oidc_client, user_id)) + cache_key = oidc_profile_cache_key(oidc_client, user_id) + oidc_profile = cache.get(cache_key) if oidc_profile: try: user = oidc_user_from_profile(oidc_client, oidc_profile) @@ -115,6 +116,12 @@ # restore auth backend setattr(user, "backend", f"{__name__}.{self.__class__.__name__}") return user + except KeycloakError as ke: + error_msg = keycloak_error_message(ke) + if error_msg == "invalid_grant: Session not active": + # user session no longer active, remove oidc profile from cache + cache.delete(cache_key) + return None except Exception as e: sentry_sdk.capture_exception(e) return None diff --git a/swh/auth/tests/django/test_backends.py b/swh/auth/tests/django/test_backends.py --- a/swh/auth/tests/django/test_backends.py +++ b/swh/auth/tests/django/test_backends.py @@ -4,17 +4,19 @@ # See top-level LICENSE file for more information from datetime import datetime, timedelta +import json from unittest.mock import Mock from django.conf import settings from django.contrib.auth import authenticate, get_backends +from django.core.cache import cache import pytest from rest_framework.exceptions import AuthenticationFailed from swh.auth.django.backends import OIDCBearerTokenAuthentication from swh.auth.django.models import OIDCUser -from swh.auth.django.utils import reverse -from swh.auth.keycloak import ExpiredSignatureError +from swh.auth.django.utils import oidc_profile_cache_key, reverse +from swh.auth.keycloak import ExpiredSignatureError, KeycloakError def _authenticate_user(request_factory): @@ -123,18 +125,43 @@ """ Checks access token renewal failure using refresh token. """ + + # authenticate user + user = _authenticate_user(request_factory) + assert user is not None + # OIDC profile should be in cache + cache_key = oidc_profile_cache_key(keycloak_oidc, user.id) + assert cache.get(cache_key) is not None + + # simulate terminated OIDC session keycloak_oidc.decode_token = Mock() keycloak_oidc.decode_token.side_effect = ExpiredSignatureError( "access token token has expired" ) - keycloak_oidc.refresh_token.side_effect = Exception("OIDC session has expired") - user = _authenticate_user(request_factory) + kc_error_dict = { + "error": "invalid_grant", + "error_description": "Session not active", + } + keycloak_oidc.refresh_token.side_effect = KeycloakError( + error_message=json.dumps(kc_error_dict).encode(), response_code=400 + ) + + backend_path = "swh.auth.django.backends.OIDCAuthorizationCodePKCEBackend" + assert user.backend == backend_path + backend_idx = settings.AUTHENTICATION_BACKENDS.index(backend_path) + + # try to authenticate user again from its id and cached OIDC profile + user = get_backends()[backend_idx].get_user(user.id) + # it should have tried to refresh token oidc_profile = keycloak_oidc.login() keycloak_oidc.refresh_token.assert_called_with(oidc_profile["refresh_token"]) + # authentication failed assert user is None + # invalid OIDC profile should have been removed from cache + assert cache.get(cache_key) is None @pytest.mark.django_db