From 4cdf89070cfb9c91cb7ea95fbc80a84477044964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enol=20Fern=C3=A1ndez?= Date: Tue, 17 Oct 2023 07:44:34 +0100 Subject: [PATCH] Add new backend for EGI Check-in (#836) * Add new backend for EGI Check-in Learn more at https://www.egi.eu/service/check-in/ * Fix json string in test * Rename to make EGI more prominent * Fix module name --- social_core/backends/egi_checkin.py | 79 +++++ .../tests/backends/test_egi_checkin.py | 325 ++++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 social_core/backends/egi_checkin.py create mode 100644 social_core/tests/backends/test_egi_checkin.py diff --git a/social_core/backends/egi_checkin.py b/social_core/backends/egi_checkin.py new file mode 100644 index 00000000..b2a6b5cb --- /dev/null +++ b/social_core/backends/egi_checkin.py @@ -0,0 +1,79 @@ +""" +Backend for OpenID Connect EGI Check-in +https://www.egi.eu/service/check-in/ +""" + +from social_core.backends.open_id_connect import OpenIdConnectAuth + +CHECKIN_ENV_ENDPOINTS = { + "prod": "https://aai.egi.eu/auth/realms/egi", + "demo": "https://aai-demo.egi.eu/auth/realms/egi", + "dev": "https://aai-dev.egi.eu/auth/realms/egi", +} + + +class EGICheckinOpenIdConnect(OpenIdConnectAuth): + name = "egi-checkin" + # Check-in provides 3 environments: production, demo and development + # Set the one to use as "prod", "demo" or "dev" + CHECKIN_ENV = "prod" + # This is a opaque and unique id for every user that looks like an email + # see https://docs.egi.eu/providers/check-in/sp/#1-community-user-identifier + USERNAME_KEY = "voperson_id" + EXTRA_DATA = [ + ("expires_in", "expires_in", True), + ("refresh_token", "refresh_token", True), + ("id_token", "id_token", True), + ] + # In order to get any scopes, you have to register your service with + # Check-in, see documentation at https://docs.egi.eu/providers/check-in/sp/ + DEFAULT_SCOPE = [ + "openid", + "profile", + "email", + "voperson_id", + "eduperson_entitlement", + "offline_access", + ] + # This is the list of entitlements that are allowed to login into the + # service. A user with any of these will be allowed. If empty, all + # users will be allowed + ALLOWED_ENTITLEMENTS = [] + + def oidc_endpoint(self): + endpoint = self.setting("OIDC_ENDPOINT", self.OIDC_ENDPOINT) + if endpoint: + return endpoint + checkin_env = self.setting("CHECKIN_ENV", self.CHECKIN_ENV) + return CHECKIN_ENV_ENDPOINTS.get(checkin_env, "") + + def get_user_details(self, response): + username_key = self.setting("USERNAME_KEY", default=self.USERNAME_KEY) + fullname, first_name, last_name = self.get_user_names( + response.get("name") or "", + response.get("given_name") or "", + response.get("family_name") or "", + ) + return { + "username": response.get(username_key), + "email": response.get("email"), + "fullname": fullname, + "first_name": first_name, + "last_name": last_name, + } + + def entitlement_allowed(self, user_entitlements): + allowed = True + allowed_ent = self.setting("ALLOWED_ENTITLEMENTS", self.ALLOWED_ENTITLEMENTS) + if allowed_ent: + allowed = any(e in user_entitlements for e in allowed_ent) + return allowed + + def auth_allowed(self, response, details): + """Check-in promotes the use of eduperson_entitlements for AuthZ, if + ALLOWED_ENTITLEMENTS is defined then use them to allow or not users""" + allowed = super().auth_allowed(response, details) + if allowed: + user_entitlements = response.get("eduperson_entitlement") or [] + allowed = self.entitlement_allowed(user_entitlements) + return allowed diff --git a/social_core/tests/backends/test_egi_checkin.py b/social_core/tests/backends/test_egi_checkin.py new file mode 100644 index 00000000..bb514208 --- /dev/null +++ b/social_core/tests/backends/test_egi_checkin.py @@ -0,0 +1,325 @@ +from .oauth import OAuth2Test +from .test_open_id_connect import OpenIdConnectTestMixin + + +class EGICheckinOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test): + backend_path = "social_core.backends.egi_checkin.EGICheckinOpenIdConnect" + issuer = "https://aai.egi.eu/auth/realms/egi" + openid_config_body = """ + { + "issuer": "https://aai.egi.eu/auth/realms/egi", + "authorization_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/auth", + "token_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token", + "introspection_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token/introspect", + "userinfo_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/userinfo", + "end_session_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/logout", + "frontchannel_logout_session_supported": true, + "frontchannel_logout_supported": true, + "jwks_uri": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/certs", + "check_session_iframe": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/login-status-iframe.html", + "grant_types_supported": [ + "authorization_code", + "implicit", + "refresh_token", + "password", + "client_credentials", + "urn:ietf:params:oauth:grant-type:device_code", + "urn:openid:params:grant-type:ciba", + "urn:ietf:params:oauth:grant-type:token-exchange" + ], + "acr_values_supported": ["0", "1"], + "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" + ], + "userinfo_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "userinfo_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "request_object_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512", + "none" + ], + "request_object_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "request_object_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "response_modes_supported": [ + "query", + "fragment", + "form_post", + "query.jwt", + "fragment.jwt", + "form_post.jwt", + "jwt" + ], + "registration_endpoint": "https://aai.egi.eu/auth/realms/egi/clients-registrations/openid-connect", + "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" + ], + "introspection_endpoint_auth_methods_supported": [ + "private_key_jwt", + "client_secret_basic", + "client_secret_post", + "tls_client_auth", + "client_secret_jwt" + ], + "introspection_endpoint_auth_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "authorization_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "HS256", + "HS512", + "ES256", + "RS256", + "HS384", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "authorization_encryption_alg_values_supported": [ + "RSA-OAEP", + "RSA-OAEP-256", + "RSA1_5" + ], + "authorization_encryption_enc_values_supported": [ + "A256GCM", + "A192GCM", + "A128GCM", + "A128CBC-HS256", + "A192CBC-HS384", + "A256CBC-HS512" + ], + "claims_supported": [ + "acr", + "cert_entitlement", + "eduperson_assurance", + "eduperson_entitlement", + "eduperson_scoped_affiliation", + "eduperson_unique_id", + "email", + "email_verified", + "family_name", + "given_name", + "name", + "orcid", + "preferred_username", + "ssh_public_key", + "sub", + "voperson_external_affiliation", + "voperson_id", + "voperson_verified_email" + ], + "claim_types_supported": ["normal"], + "claims_parameter_supported": true, + "scopes_supported": [ + "openid", + "voperson_external_affiliation", + "email", + "orcid", + "aarc", + "cert_entitlement", + "eduperson_scoped_affiliation", + "voperson_id", + "ssh_public_key", + "profile", + "offline_access", + "eduperson_unique_id", + "eduperson_entitlement" + ], + "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": "https://aai.egi.eu/auth/realms/egi/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, + "device_authorization_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/auth/device", + "backchannel_token_delivery_modes_supported": ["poll", "ping"], + "backchannel_authentication_request_signing_alg_values_supported": [ + "PS384", + "ES384", + "RS384", + "ES256", + "RS256", + "ES512", + "PS256", + "PS512", + "RS512" + ], + "require_pushed_authorization_requests": false, + "mtls_endpoint_aliases": { + "token_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token", + "revocation_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/revoke", + "introspection_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/token/introspect", + "device_authorization_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/auth/device", + "registration_endpoint": "https://aai.egi.eu/auth/realms/egi/clients-registrations/openid-connect", + "userinfo_endpoint": "https://aai.egi.eu/auth/realms/egi/protocol/openid-connect/userinfo" + } + } + """ + + def test_do_not_override_endpoint(self): + self.backend.OIDC_ENDPOINT = self.issuer + self.assertEqual(self.backend.oidc_endpoint(), self.issuer) + + def test_checkin_env_prod(self): + self.assertEqual( + self.backend.oidc_endpoint(), "https://aai.egi.eu/auth/realms/egi" + ) + + def test_checkin_env_demo(self): + self.backend.CHECKIN_ENV = "demo" + self.assertEqual( + self.backend.oidc_endpoint(), "https://aai-demo.egi.eu/auth/realms/egi" + ) + + def test_checkin_env_dev(self): + self.backend.CHECKIN_ENV = "dev" + self.assertEqual( + self.backend.oidc_endpoint(), "https://aai-dev.egi.eu/auth/realms/egi" + ) + + def test_entitlements_empty(self): + self.assertEqual(self.backend.entitlement_allowed([]), True) + + def test_entitlements_allowed(self): + self.backend.ALLOWED_ENTITLEMENTS = ["foo", "baz"] + self.assertEqual(self.backend.entitlement_allowed(["foo", "bar"]), True) + + def test_entitlements_not_allowed(self): + self.backend.ALLOWED_ENTITLEMENTS = ["baz"] + self.assertEqual(self.backend.entitlement_allowed(["foo"]), False)