From ec13956dd221c0258ada4906b7ec97725fa2d6dd Mon Sep 17 00:00:00 2001 From: Pavel Vyskocil Date: Thu, 17 Sep 2026 19:41:27 +0200 Subject: [PATCH] feat: add new backend cesid --- social_core/backends/cesid.py | 32 ++ social_core/tests/backends/test_cesid.py | 419 +++++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 social_core/backends/cesid.py create mode 100644 social_core/tests/backends/test_cesid.py diff --git a/social_core/backends/cesid.py b/social_core/backends/cesid.py new file mode 100644 index 000000000..608c1b3c7 --- /dev/null +++ b/social_core/backends/cesid.py @@ -0,0 +1,32 @@ +""" +Backend for OpenID Connect CESiD AAI - Czech Educational and Scientific Identification +""" + +from social_core.backends.open_id_connect import OpenIdConnectAuth + + +class CesidOpenIdConnect(OpenIdConnectAuth): + name = "cesid" + OIDC_ENDPOINT = "https://login.cesid.cesnet.cz/cas/oidc" + EXTRA_DATA = [ + ("expires_in", "expires_in", True), + ("refresh_token", "refresh_token", True), + ("id_token", "id_token", True), + ("other_tokens", "other_tokens", True), + ] + # In order to get any scopes, you have to register your service with + # CESiD AAI at https://services.cesid.cesnet.cz/ + DEFAULT_SCOPE = ["openid", "email"] + VALIDATE_AT_HASH: bool = False + + def get_user_details(self, response): + username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) + name = response.get("name") or "" + fullname, first_name, last_name = self.get_user_names(name) + return { + "username": response.get(username_key), + "email": response.get("email"), + "fullname": fullname, + "first_name": first_name, + "last_name": last_name, + } diff --git a/social_core/tests/backends/test_cesid.py b/social_core/tests/backends/test_cesid.py new file mode 100644 index 000000000..1e926e2e6 --- /dev/null +++ b/social_core/tests/backends/test_cesid.py @@ -0,0 +1,419 @@ +import json + +from .oauth import BaseAuthUrlTestMixin +from .open_id_connect import OpenIdConnectTest + + +class CesidOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin): + backend_path = "social_core.backends.cesid.CesidOpenIdConnect" + issuer = "https://login.cesid.cesnet.cz/cas/oidc" + openid_config_body = """ + { + "DPopSigningAlgValuesSupported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512" + ], + "acr_values_supported": [], + "authorization_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/oidcAuthorize", + "authorization_response_iss_parameter_supported": false, + "backchannel_authentication_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/oidcCiba", + "backchannel_authentication_request_signing_alg_values_supported": [ + "none", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES512", + "HS256", + "HS384", + "HS512" + ], + "backchannel_logout_session_supported": true, + "backchannel_logout_supported": true, + "backchannel_token_delivery_modes_supported": [ + "poll", + "ping", + "push" + ], + "backchannel_user_code_parameter_supported": false, + "claim_types_supported": [ + "normal" + ], + "claims_in_verified_claims_supported": null, + "claims_parameter_supported": true, + "claims_supported": [ + "sub", + "email", + "email_verified", + "name", + "family_name", + "given_name", + "preferred_username", + "perun_api", + "perun_admin", + "perun_sub", + "eduperson_entitlement", + "fromidp_voPersonUniqueID", + "fromidp_eduPersonUniqueId", + "fromidp_eduPersonPrincipalName", + "idp_identifier", + "entitlements", + "eduperson_entitlement_extended" + ], + "code_challenge_methods_supported": [ + "plain", + "S256" + ], + "device_authorization_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/oidcAccessToken", + "documents_supported": null, + "documents_validation_methods_supported": null, + "documents_verification_methods_supported": null, + "dpop_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "ES512" + ], + "electronic_records_supported": null, + "end_session_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/oidcLogout", + "evidence_supported": null, + "frontchannel_logout_session_supported": true, + "frontchannel_logout_supported": true, + "grant_types_supported": [ + "authorization_code", + "password", + "client_credentials", + "refresh_token", + "urn:openid:params:grant-type:ciba", + "urn:ietf:params:oauth:grant-type:pre-authorized_code", + "urn:ietf:params:oauth:grant-type:jwt-bearer", + "urn:ietf:params:oauth:grant-type:token-exchange", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:ietf:params:oauth:grant-type:uma-ticket" + ], + "id_token_encryption_alg_values_supported": [ + "RSA1_5", + "RSA-OAEP", + "RSA-OAEP-256", + "A128KW", + "A192KW", + "A256KW", + "A128GCMKW", + "A192GCMKW", + "A256GCMKW", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW" + ], + "id_token_encryption_enc_values_supported": [ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512", + "A128GCM", + "A192GCM", + "A256GCM" + ], + "id_token_signing_alg_values_supported": [ + "none", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES512", + "HS256", + "HS384", + "HS512" + ], + "introspection_encryption_alg_values_supported": [ + "RSA1_5", + "RSA-OAEP", + "RSA-OAEP-256", + "A128KW", + "A192KW", + "A256KW", + "A128GCMKW", + "A192GCMKW", + "A256GCMKW", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW" + ], + "introspection_encryption_enc_values_supported": [ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512", + "A128GCM", + "A192GCM", + "A256GCM" + ], + "introspection_endpoint": "https://tip.login.cesid.cesnet.cz/", + "introspection_endpoint_auth_methods_supported": [ + "client_secret_basic" + ], + "introspection_signing_alg_values_supported": [ + "none", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES512", + "HS256", + "HS384", + "HS512" + ], + "issuer": "https://login.cesid.cesnet.cz/cas/oidc", + "jwks_uri": "https://login.cesid.cesnet.cz/cas/oidc/jwks", + "native_sso_supported": true, + "prompt_values_supported": [ + "none", + "login", + "consent" + ], + "pushed_authorization_request_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/oidcPushAuthorize", + "registration_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/register", + "request_object_encryption_alg_values_supported": [ + "RSA1_5", + "RSA-OAEP", + "RSA-OAEP-256", + "A128KW", + "A192KW", + "A256KW", + "A128GCMKW", + "A192GCMKW", + "A256GCMKW", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW" + ], + "request_object_encryption_enc_values_supported": [ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512", + "A128GCM", + "A192GCM", + "A256GCM" + ], + "request_object_signing_alg_values_supported": [ + "none", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES512", + "HS256", + "HS384", + "HS512" + ], + "request_parameter_supported": true, + "request_uri_parameter_supported": true, + "require_pushed_authorization_requests": false, + "response_modes_supported": [ + "query", + "fragment", + "form_post", + "query.jwt", + "form_post.jwt", + "fragment.jwt" + ], + "response_types_supported": [ + "code", + "token", + "id_token", + "id_token token", + "device_code" + ], + "revocation_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/revoke", + "scopes_supported": [ + "openid", + "profile", + "email", + "address", + "phone", + "offline_access", + "device_sso", + "client_configuration_scope", + "uma_authorization", + "uma_protection", + "client_registration_scope", + "perun_api", + "perun_admin", + "perun_sub", + "eduperson_entitlement", + "eduperson_entitlement_extended", + "entitlements", + "all_attributes" + ], + "subject_types_supported": [ + "public", + "pairwise" + ], + "tls_client_certificate_bound_access_tokens": false, + "token_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/oidcAccessToken", + "token_endpoint_auth_methods_supported": [ + "client_secret_basic", + "client_secret_post", + "client_secret_jwt", + "private_key_jwt", + "tls_client_auth" + ], + "trust_frameworks_supported": null, + "userinfo_encryption_alg_values_supported": [ + "RSA1_5", + "RSA-OAEP", + "RSA-OAEP-256", + "A128KW", + "A192KW", + "A256KW", + "A128GCMKW", + "A192GCMKW", + "A256GCMKW", + "ECDH-ES", + "ECDH-ES+A128KW", + "ECDH-ES+A192KW", + "ECDH-ES+A256KW" + ], + "userinfo_encryption_enc_values_supported": [ + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512", + "A128GCM", + "A192GCM", + "A256GCM" + ], + "userinfo_endpoint": "https://login.cesid.cesnet.cz/cas/oidc/oidcProfile", + "userinfo_signing_alg_values_supported": [ + "none", + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES384", + "ES512", + "HS256", + "HS384", + "HS512" + ], + "verified_claims_supported": true +} + """ + skip_invalid_at_hash = allow_invalid_at_hash = True + expected_username = "cesiduser" + user_data_url = "https://login.cesid.cesnet.cz/cas/oidc/oidcProfile" + user_data_body = json.dumps( + { + "preferred_username": "cesiduser", + "email": "email@example.com", + "name": "Jan Novak", + } + ) + + def test_login(self) -> None: + self.do_login() + + def test_get_user_details(self) -> None: + response = { + "preferred_username": "cesiduser", + "email": "email@example.com", + "name": "Jan Novak", + } + details = self.backend.get_user_details(response) + self.assertEqual( + details, + { + "username": "cesiduser", + "email": "email@example.com", + "fullname": "Jan Novak", + "first_name": "Jan", + "last_name": "Novak", + }, + ) + + def test_get_user_details_empty_name(self) -> None: + response = { + "preferred_username": "cesiduser", + "email": "email@example.com", + } + details = self.backend.get_user_details(response) + self.assertEqual( + details, + { + "username": "cesiduser", + "email": "email@example.com", + "fullname": "", + "first_name": "", + "last_name": "", + }, + ) + + def test_get_user_details_custom_username_key(self) -> None: + self.strategy.set_settings( + {"SOCIAL_AUTH_CESID_USERNAME_KEY": "preferred_username"} + ) + response = { + "eduperson_unique_id": "12345@cesnet.cz", + "preferred_username": "cesiduser", + "email": "email@example.com", + "name": "Jan Novak", + } + details = self.backend.get_user_details(response) + self.assertEqual(details["username"], "cesiduser") + + def test_extra_data(self) -> None: + self.backend.id_token = { + "iss": self.issuer, + "sub": "12345", + "aud": self.client_key, + } + response = { + "expires_in": 3600, + "refresh_token": "refresh-123", + "id_token": "id-token-xyz", + "other_tokens": ["token1", "token2"], + "ignored_field": "ignore_me", + } + extra = self.backend.extra_data( + user=None, + uid="12345", + response=response, + details={}, + pipeline_kwargs={"is_new": True}, + ) + self.assertEqual(extra["expires_in"], 3600) + self.assertEqual(extra["refresh_token"], "refresh-123") + self.assertEqual(extra["id_token"], "id-token-xyz") + self.assertEqual(extra["other_tokens"], ["token1", "token2"]) + self.assertNotIn("ignored_field", extra) + + def test_default_scope(self) -> None: + self.assertEqual(self.backend.get_scope(), ["openid", "email"]) + + def test_validate_at_hash(self) -> None: + self.assertFalse(self.backend.VALIDATE_AT_HASH)