diff --git a/swh/auth/tests/conftest.py b/swh/auth/tests/conftest.py index 2a622cd..151c53f 100644 --- a/swh/auth/tests/conftest.py +++ b/swh/auth/tests/conftest.py @@ -1,39 +1,157 @@ -# Copyright (C) 2021 The Software Heritage developers +# Copyright (C) 2020-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +from copy import copy +from datetime import datetime, timezone +from typing import Optional +from unittest.mock import Mock + +from keycloak.exceptions import KeycloakError import pytest from swh.auth import KeycloakOpenIDConnect -from .sample_data import OIDC_PROFILE, REALM, SERVER_URL, USER_INFO, WELL_KNOWN - +from .sample_data import ( + OIDC_PROFILE, + RAW_REALM_PUBLIC_KEY, + REALM, + SERVER_URL, + USER_INFO, +) -@pytest.fixture -def keycloak_open_id_connect(): - return KeycloakOpenIDConnect( - server_url=SERVER_URL, realm_name=REALM, client_id="client-id", - ) +class KeycloackOpenIDConnectMock(KeycloakOpenIDConnect): + """Mock KeycloakOpenIDConnect class to allow testing -@pytest.fixture -def mock_keycloak(requests_mock): - """Keycloak with most endpoints available. + Args: + auth_success: boolean flag to simulate authentication success or failure + exp: expiration + user_groups: user groups configuration (if any) + user_permissions: user permissions configuration (if any) """ - requests_mock.get(WELL_KNOWN["well-known"], json=WELL_KNOWN) - requests_mock.post(WELL_KNOWN["token_endpoint"], json=OIDC_PROFILE) - requests_mock.get(WELL_KNOWN["userinfo_endpoint"], json=USER_INFO) - requests_mock.post(WELL_KNOWN["end_session_endpoint"], status_code=204) - return requests_mock + def __init__( + self, + server_url: str, + realm_name: str, + client_id: str, + auth_success: bool = True, + exp: Optional[int] = None, + user_groups=[], + user_permissions=[], + ): + super().__init__( + server_url=server_url, realm_name=realm_name, client_id=client_id + ) + self.exp = exp + self.user_groups = user_groups + self.user_permissions = user_permissions + self._keycloak.public_key = lambda: RAW_REALM_PUBLIC_KEY + self._keycloak.well_know = lambda: { + "issuer": f"{self.server_url}realms/{self.realm_name}", + "authorization_endpoint": ( + f"{self.server_url}realms/" + f"{self.realm_name}/protocol/" + "openid-connect/auth" + ), + "token_endpoint": ( + f"{self.server_url}realms/{self.realm_name}/" + "protocol/openid-connect/token" + ), + "token_introspection_endpoint": ( + f"{self.server_url}realms/" + f"{self.realm_name}/protocol/" + "openid-connect/token/" + "introspect" + ), + "userinfo_endpoint": ( + f"{self.server_url}realms/{self.realm_name}/" + "protocol/openid-connect/userinfo" + ), + "end_session_endpoint": ( + f"{self.server_url}realms/" + f"{self.realm_name}/protocol/" + "openid-connect/logout" + ), + "jwks_uri": ( + f"{self.server_url}realms/{self.realm_name}/" + "protocol/openid-connect/certs" + ), + } + self.set_auth_success(auth_success) + + def decode_token(self, token): + options = {} + if self.auth_success: + # skip signature expiration check as we use a static oidc_profile + # for the tests with expired tokens in it + options["verify_exp"] = False + decoded = super().decode_token(token, options) + # tweak auth and exp time for tests + expire_in = decoded["exp"] - decoded["auth_time"] + if self.exp is not None: + decoded["exp"] = self.exp + decoded["auth_time"] = self.exp - expire_in + else: + decoded["auth_time"] = int(datetime.now(tz=timezone.utc).timestamp()) + decoded["exp"] = decoded["auth_time"] + expire_in + decoded["groups"] = self.user_groups + if self.user_permissions: + decoded["resource_access"][self.client_id] = { + "roles": self.user_permissions + } + return decoded + def set_auth_success(self, auth_success: bool) -> None: + # following type ignore because mypy is not too happy about affecting mock to + # method "Cannot assign to a method affecting mock". Ignore for now. + self.authorization_code = Mock() # type: ignore + self.refresh_token = Mock() # type: ignore + self.userinfo = Mock() # type: ignore + self.logout = Mock() # type: ignore + self.auth_success = auth_success + if auth_success: + self.authorization_code.return_value = copy(OIDC_PROFILE) + self.refresh_token.return_value = copy(OIDC_PROFILE) + self.userinfo.return_value = copy(USER_INFO) + else: + self.authorization_url = Mock() # type: ignore + exception = KeycloakError( + error_message="Authentication failed", response_code=401 + ) + self.authorization_code.side_effect = exception + self.authorization_url.side_effect = exception + self.refresh_token.side_effect = exception + self.userinfo.side_effect = exception + self.logout.side_effect = exception -@pytest.fixture -def mock_keycloak_refused_auth(requests_mock): - """Keycloak with token endpoint refusing authentication. + +def keycloak_mock_factory( + server_url=SERVER_URL, + realm_name=REALM, + client_id="swh-client-id", + auth_success=True, + exp=None, + user_groups=[], + user_permissions=[], +): + """Keycloak mock fixture factory """ - requests_mock.post(WELL_KNOWN["token_endpoint"], status_code=401) - return requests_mock + + @pytest.fixture + def keycloak_open_id_connect(): + return KeycloackOpenIDConnectMock( + server_url=server_url, + realm_name=realm_name, + client_id=client_id, + auth_success=auth_success, + exp=exp, + user_groups=user_groups, + user_permissions=user_permissions, + ) + + return keycloak_open_id_connect diff --git a/swh/auth/tests/sample_data.py b/swh/auth/tests/sample_data.py index 77e4602..3e20e14 100644 --- a/swh/auth/tests/sample_data.py +++ b/swh/auth/tests/sample_data.py @@ -1,284 +1,142 @@ # Copyright (C) 2020-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information SERVER_URL = "http://keycloak:8080/keycloak/auth/" REALM = "SoftwareHeritage" +CLIENT_ID = "swh-web" -WELL_KNOWN = { - "issuer": f"{SERVER_URL}realms/SoftwareHeritage", - "well-known": f"{SERVER_URL}realms/{REALM}/.well-known/openid-configuration", - "authorization_endpoint": f"{SERVER_URL}realms/{REALM}/protocol/openid-connect/auth", # noqa - "token_endpoint": f"{SERVER_URL}realms/{REALM}/protocol/openid-connect/token", - "introspection_endpoint": f"{SERVER_URL}realms/{REALM}/protocol/openid-connect/token/introspect", # noqa - "userinfo_endpoint": f"{SERVER_URL}realms/{REALM}/protocol/openid-connect/userinfo", - "end_session_endpoint": f"{SERVER_URL}realms/{REALM}/protocol/openid-connect/logout", # noqa - "jwks_uri": "{SERVER_URL}realms/{REALM}/protocol/openid-connect/certs", - "check_session_iframe": "{SERVER_URL}realms/{REALM}/protocol/openid-connect/login-status-iframe.html", # noqa - "grant_types_supported": [ - "authorization_code", - "implicit", - "refresh_token", - "password", - "client_credentials", - ], - "response_types_supported": [ - "code", - "none", - "id_token", - "token", - "id_token token", - "code id_token", - "code token", - "code id_token token", - ], - "subject_types_supported": ["public", "pairwise"], - "id_token_signing_alg_values_supported": [ - "PS384", - "ES384", - "RS384", - "HS256", - "HS512", - "ES256", - "RS256", - "HS384", - "ES512", - "PS256", - "PS512", - "RS512", - ], - "id_token_encryption_alg_values_supported": ["RSA-OAEP", "RSA-OAEP-256", "RSA1_5"], - "id_token_encryption_enc_values_supported": [ - "A256GCM", - "A192GCM", - "A128GCM", - "A128CBC-HS256", - "A192CBC-HS384", - "A256CBC-HS512", - ], - "userinfo_signing_alg_values_supported": [ - "PS384", - "ES384", - "RS384", - "HS256", - "HS512", - "ES256", - "RS256", - "HS384", - "ES512", - "PS256", - "PS512", - "RS512", - "none", - ], - "request_object_signing_alg_values_supported": [ - "PS384", - "ES384", - "RS384", - "HS256", - "HS512", - "ES256", - "RS256", - "HS384", - "ES512", - "PS256", - "PS512", - "RS512", - "none", - ], - "response_modes_supported": ["query", "fragment", "form_post"], - "registration_endpoint": "{SERVER_URL}realms/{REALM}/clients-registrations/openid-connect", # noqa - "token_endpoint_auth_methods_supported": [ - "private_key_jwt", - "client_secret_basic", - "client_secret_post", - "tls_client_auth", - "client_secret_jwt", - ], - "token_endpoint_auth_signing_alg_values_supported": [ - "PS384", - "ES384", - "RS384", - "HS256", - "HS512", - "ES256", - "RS256", - "HS384", - "ES512", - "PS256", - "PS512", - "RS512", - ], - "claims_supported": [ - "aud", - "sub", - "iss", - "auth_time", - "name", - "given_name", - "family_name", - "preferred_username", - "email", - "acr", - ], - "claim_types_supported": ["normal"], - "claims_parameter_supported": True, - "scopes_supported": [ - "openid", - "microprofile-jwt", - "web-origins", - "roles", - "phone", - "address", - "email", - "profile", - "offline_access", - ], - "request_parameter_supported": True, - "request_uri_parameter_supported": True, - "require_request_uri_registration": True, - "code_challenge_methods_supported": ["plain", "S256"], - "tls_client_certificate_bound_access_tokens": True, - "revocation_endpoint": "{SERVER_URL}realms/{REALM}/protocol/openid-connect/revoke", - "revocation_endpoint_auth_methods_supported": [ - "private_key_jwt", - "client_secret_basic", - "client_secret_post", - "tls_client_auth", - "client_secret_jwt", - ], - "revocation_endpoint_auth_signing_alg_values_supported": [ - "PS384", - "ES384", - "RS384", - "HS256", - "HS512", - "ES256", - "RS256", - "HS384", - "ES512", - "PS256", - "PS512", - "RS512", - ], - "backchannel_logout_supported": True, - "backchannel_logout_session_supported": True, +# Decoded token (out of the access token) +DECODED_TOKEN = { + "jti": "31fc50b7-bbe5-4f51-91ef-8e3eec51331e", + "exp": 1614787019, + "nbf": 0, + "iat": 1582723101, + "iss": "http://localhost:8080/auth/realms/SoftwareHeritage", + "aud": [CLIENT_ID, "account"], + "sub": "feacd344-b468-4a65-a236-14f61e6b7200", + "typ": "Bearer", + "azp": CLIENT_ID, + "auth_time": 1614786418, + "session_state": "d82b90d1-0a94-4e74-ad66-dd95341c7b6d", + "acr": "1", + "allowed-origins": ["*"], + "realm_access": {"roles": ["offline_access", "uma_authorization"]}, + "resource_access": { + "account": {"roles": ["manage-account", "manage-account-links", "view-profile"]} + }, + "scope": "openid email profile", + "email_verified": False, + "name": "John Doe", + "groups": [], + "preferred_username": "johndoe", + "given_name": "John", + "family_name": "Doe", + "email": "john.doe@example.com", } - # Authentication response is an oidc profile dict OIDC_PROFILE = { "access_token": ( - # decoded token: - # {'acr': '1', - # 'allowed-origins': ['*'], - # 'aud': ['swh-web', 'account'], - # 'auth_time': 1592395601, - # 'azp': 'swh-web', - # 'email': 'john.doe@example.com', - # 'email_verified': False, - # 'exp': 1592396202, - # 'family_name': 'Doe', - # 'given_name': 'John', - # 'groups': ['/staff'], - # 'iat': 1582723101, - # 'iss': 'http://localhost:8080/auth/realms/SoftwareHeritage', - # 'jti': '31fc50b7-bbe5-4f51-91ef-8e3eec51331e', - # 'name': 'John Doe', - # 'nbf': 0, - # 'preferred_username': 'johndoe', - # 'realm_access': {'roles': ['offline_access', 'uma_authorization']}, - # 'resource_access': {'account': {'roles': ['manage-account', - # 'manage-account-links', - # 'view-profile']}}, - # 'scope': 'openid email profile', - # 'session_state': 'd82b90d1-0a94-4e74-ad66-dd95341c7b6d', - # 'sub': 'feacd344-b468-4a65-a236-14f61e6b7200', - # 'typ': 'Bearer' - # } "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJPSnhV" "Q0p0TmJQT0NOUGFNNmc3ZU1zY2pqTXhoem9vNGxZaFhsa1c2TWhBIn0." "eyJqdGkiOiIzMWZjNTBiNy1iYmU1LTRmNTEtOTFlZi04ZTNlZWM1MTMz" "MWUiLCJleHAiOjE1ODI3MjM3MDEsIm5iZiI6MCwiaWF0IjoxNTgyNzIz" "MTAxLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFs" "bXMvU29mdHdhcmVIZXJpdGFnZSIsImF1ZCI6WyJzd2gtd2ViIiwiYWNj" "b3VudCJdLCJzdWIiOiJmZWFjZDM0NC1iNDY4LTRhNjUtYTIzNi0xNGY2" "MWU2YjcyMDAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJzd2gtd2ViIiwi" "YXV0aF90aW1lIjoxNTgyNzIzMTAwLCJzZXNzaW9uX3N0YXRlIjoiZDgy" "YjkwZDEtMGE5NC00ZTc0LWFkNjYtZGQ5NTM0MWM3YjZkIiwiYWNyIjoi" "MSIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6" "eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0" "aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xl" "cyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtz" "Iiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJvcGVuaWQgZW1haWwg" "cHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6Ikpv" "aG4gRG9lIiwiZ3JvdXBzIjpbXSwicHJlZmVycmVkX3VzZXJuYW1lIjoi" "am9obmRvZSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUi" "OiJEb2UiLCJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIn0.neJ-" "Pmd87J6Gt0fzDqmXFeoy34Iqb5vNNEEgIKqtqg3moaVkbXrO_9R37DJB" "AgdFv0owVONK3GbqPOEICePgG6RFtri999DetNE-O5sB4fwmHPWcHPlO" "kcPLbVJqu6zWo-2AzlfAy5bCNvj_wzs2tjFjLeHcRgR1a1WY3uTp5EWc" "HITCWQZzZWFGZTZCTlGkpdyJTqxGBdSHRB4NlIVGpYSTBsBsxttFEetl" "rpcNd4-5AteFprIr9hn9VasIIF8WdFdtC2e8xGMJW5Q0M3G3Iu-LLNmE" "oTIDqtbJ7OrIcGBIwsc3seCV3eCG6kOYwz5w-f8DeOpwcDX58yYPmapJ" "6A" ), "expires_in": 600, "id_token": ( "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJPSnhVQ0p0" "TmJQT0NOUGFNNmc3ZU1zY2pqTXhoem9vNGxZaFhsa1c2TWhBIn0.eyJqdGki" "OiI0NDRlYzU1My1iYzhiLTQ2YjYtOTlmYS0zOTc3YTJhZDY1ZmEiLCJleHAi" "OjE1ODI3MjM3MDEsIm5iZiI6MCwiaWF0IjoxNTgyNzIzMTAxLCJpc3MiOiJo" "dHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvU29mdHdhcmVIZXJp" "dGFnZSIsImF1ZCI6InN3aC13ZWIiLCJzdWIiOiJmZWFjZDM0NC1iNDY4LTRh" "NjUtYTIzNi0xNGY2MWU2YjcyMDAiLCJ0eXAiOiJJRCIsImF6cCI6InN3aC13" "ZWIiLCJhdXRoX3RpbWUiOjE1ODI3MjMxMDAsInNlc3Npb25fc3RhdGUiOiJk" "ODJiOTBkMS0wYTk0LTRlNzQtYWQ2Ni1kZDk1MzQxYzdiNmQiLCJhY3IiOiIx" "IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiSm9obiBEb2UiLCJn" "cm91cHMiOltdLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqb2huZG9lIiwiZ2l2" "ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6IkRvZSIsImVtYWlsIjoi" "am9obi5kb2VAZXhhbXBsZS5jb20ifQ.YB7bxlz_wgLJSkylVjmqedxQgEMee" "JOdi9CFHXV4F3ZWsEZ52CGuJXsozkX2oXvgU06MzzLNEK8ojgrPSNzjRkutL" "aaLq_YUzv4iV8fmKUS_aEyiYZbfoBe3Y4dwv2FoPEPCt96iTwpzM5fg_oYw_" "PHCq-Yl5SulT1nTrJZpntkf0hRjmxlDO06JMp0aZ8xS8RYJqH48xCRf_DARE" "0jJV2-UuzOWI6xBATwFfP44kV6wFmErLN5txMgwZzCSB2OCe5Cl1il0eTQTN" "ybeSYZeZE61QtuTRUHeP1D1qSbJGy5g_S67SdTkS-hQFvfrrD84qGflIEqnX" "ZbYnitD1Typ6Q" ), "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": ( "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmNjM" "zMDE5MS01YTU4LTQxMDAtOGIzYS00ZDdlM2U1NjA3MTgifQ.eyJqdGk" "iOiIxYWI5ZWZmMS0xZWZlLTQ3MDMtOGQ2YS03Nzg1NWUwYzQyYTYiLC" "JleHAiOjE1ODI3MjQ5MDEsIm5iZiI6MCwiaWF0IjoxNTgyNzIzMTAxL" "CJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMv" "U29mdHdhcmVIZXJpdGFnZSIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q" "6ODA4MC9hdXRoL3JlYWxtcy9Tb2Z0d2FyZUhlcml0YWdlIiwic3ViIj" "oiZmVhY2QzNDQtYjQ2OC00YTY1LWEyMzYtMTRmNjFlNmI3MjAwIiwid" "HlwIjoiUmVmcmVzaCIsImF6cCI6InN3aC13ZWIiLCJhdXRoX3RpbWUi" "OjAsInNlc3Npb25fc3RhdGUiOiJkODJiOTBkMS0wYTk0LTRlNzQtYWQ" "2Ni1kZDk1MzQxYzdiNmQiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOl" "sib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwic" "mVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFu" "YWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXc" "tcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIGVtYWlsIHByb2ZpbG" "UifQ.xQYrl2CMP_GQ_TFqhsTz-rTs3WuZz5I37toi1eSsDMI" ), "scope": "openid email profile", "session_state": "d82b90d1-0a94-4e74-ad66-dd95341c7b6d", "token_type": "bearer", } - USER_INFO = { "email": "john.doe@example.com", "email_verified": False, "family_name": "Doe", "given_name": "John", "groups": ["/staff"], "name": "John Doe", "preferred_username": "johndoe", "sub": "feacd344-b468-4a65-a236-14f61e6b7200", } + +RAW_REALM_PUBLIC_KEY = ( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnqF4xvGjaI54P6WtJvyGayxP8A93u" + "NcA3TH6jitwmyAalj8dN8/NzK9vrdlSA3Ibvp/XQujPSOP7a35YiYFscEJnogTXQpE/FhZrUY" + "y21U6ezruVUv4z/ER1cYLb+q5ZI86nXSTNCAbH+lw7rQjlvcJ9KvgHEeA5ALXJ1r55zUmNvuy" + "5o6ke1G3fXbNSXwF4qlWAzo1o7Ms8qNrNyOG8FPx24dvm9xMH7/08IPvh9KUqlnP8h6olpxHr" + "drX/q4E+Nzj8Tr8p7Z5CimInls40QuOTIhs6C2SwFHUgQgXl9hB9umiZJlwYEpDv0/LO2zYie" + "Hl5Lv7Iig4FOIXIVCaDGQIDAQAB" +) + +REALM_PUBLIC_KEY = { + "realm": REALM, + "public_key": RAW_REALM_PUBLIC_KEY, + "token-service": f"{SERVER_URL}realms/{REALM}/protocol/openid-connect", + "account-service": f"{SERVER_URL}realms/{REALM}/account", + "tokens-not-before": 0, +} diff --git a/swh/auth/tests/test_auth.py b/swh/auth/tests/test_auth.py index 54d56d9..442c916 100644 --- a/swh/auth/tests/test_auth.py +++ b/swh/auth/tests/test_auth.py @@ -1,83 +1,86 @@ # Copyright (C) 2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information +from copy import copy from urllib.parse import parse_qs, urlparse -from keycloak.exceptions import KeycloakAuthenticationError, KeycloakConnectionError +from keycloak.exceptions import KeycloakError import pytest -from .sample_data import OIDC_PROFILE, USER_INFO, WELL_KNOWN +from swh.auth.tests.conftest import keycloak_mock_factory +from swh.auth.tests.sample_data import CLIENT_ID, DECODED_TOKEN, OIDC_PROFILE, USER_INFO +# dataset we have here is bound to swh-web +keycloak_mock = keycloak_mock_factory(client_id=CLIENT_ID) -def test_auth_connection_failure(keycloak_open_id_connect): - with pytest.raises(KeycloakConnectionError): - keycloak_open_id_connect.well_known() - -def test_auth_well_known(mock_keycloak, keycloak_open_id_connect): - well_known_result = keycloak_open_id_connect.well_known() - assert well_known_result is not None - assert well_known_result == WELL_KNOWN - - assert mock_keycloak.called +def test_auth_well_known(keycloak_mock): + well_known_result = keycloak_mock.well_known() + assert set(well_known_result.keys()) == { + "issuer", + "authorization_endpoint", + "token_endpoint", + "userinfo_endpoint", + "end_session_endpoint", + "jwks_uri", + "token_introspection_endpoint", + } -def test_auth_authorization_url(mock_keycloak, keycloak_open_id_connect): - actual_auth_uri = keycloak_open_id_connect.authorization_url( - "http://redirect-uri", foo="bar" - ) +def test_auth_authorization_url(keycloak_mock): + actual_auth_uri = keycloak_mock.authorization_url("http://redirect-uri", foo="bar") - expected_auth_url = WELL_KNOWN["authorization_endpoint"] + expected_auth_url = keycloak_mock.well_known()["authorization_endpoint"] parsed_result = urlparse(actual_auth_uri) assert expected_auth_url.endswith(parsed_result.path) parsed_query = parse_qs(parsed_result.query) assert parsed_query == { - "client_id": ["client-id"], + "client_id": [CLIENT_ID], "response_type": ["code"], "redirect_uri": ["http://redirect-uri"], "foo": ["bar"], } - assert mock_keycloak.called - -def test_auth_authorization_code_fail( - mock_keycloak_refused_auth, keycloak_open_id_connect -): - with pytest.raises(KeycloakAuthenticationError): - keycloak_open_id_connect.authorization_code("auth-code", "redirect-uri") +def test_auth_authorization_code_fail(keycloak_mock): + "Authorization failure raise error" + # Simulate failed authentication with Keycloak + keycloak_mock.set_auth_success(False) - assert mock_keycloak_refused_auth.called + with pytest.raises(KeycloakError): + keycloak_mock.authorization_code("auth-code", "redirect-uri") -def test_auth_authorization_code(mock_keycloak, keycloak_open_id_connect): - actual_response = keycloak_open_id_connect.authorization_code( - "auth-code", "redirect-uri" - ) - +def test_auth_authorization_code(keycloak_mock): + actual_response = keycloak_mock.authorization_code("auth-code", "redirect-uri") assert actual_response == OIDC_PROFILE - assert mock_keycloak.called +def test_auth_refresh_token(keycloak_mock): + actual_result = keycloak_mock.refresh_token("refresh-token") + assert actual_result == OIDC_PROFILE -def test_auth_refresh_token(mock_keycloak, keycloak_open_id_connect): - actual_result = keycloak_open_id_connect.refresh_token("refresh-token") - assert actual_result is not None - assert mock_keycloak.called +def test_auth_userinfo(keycloak_mock): + actual_user_info = keycloak_mock.userinfo("refresh-token") + assert actual_user_info == USER_INFO -def test_auth_userinfo(mock_keycloak, keycloak_open_id_connect): - actual_user_info = keycloak_open_id_connect.userinfo("refresh-token") - assert actual_user_info == USER_INFO +def test_auth_logout(keycloak_mock): + """Login out does not raise""" + keycloak_mock.logout("refresh-token") - assert mock_keycloak.called +def test_auth_decode_token(keycloak_mock): + actual_decoded_data = keycloak_mock.decode_token(OIDC_PROFILE["access_token"]) -def test_auth_logout(mock_keycloak, keycloak_open_id_connect): - keycloak_open_id_connect.logout("refresh-token") + actual_decoded_data2 = copy(actual_decoded_data) + expected_decoded_token = copy(DECODED_TOKEN) + for dynamic_valued_key in ["exp", "auth_time"]: + actual_decoded_data2.pop(dynamic_valued_key) + expected_decoded_token.pop(dynamic_valued_key) - assert mock_keycloak.called + assert actual_decoded_data2 == expected_decoded_token