Repository: python-social-auth/social-core
Pull request #1068 (2025-03-26 17:01:02)

Add nfdi ops
I'm adding a whole bunch of OPs for the german NFDI (national research data initiative).
Local tox did not work for me, so I try it this way.

My tests are not extensive so far. If initial feedback is positive, I'm willing to fix the tests, too.


bgruening: @marcvs thanks a lot. I think your local CI error is not happening here, which is good.

I'm not sure the types errors are related - to be they don't seem related. But lets wait for the maintainers to comment.

Can you maybe cleanup your commits a bit. There are many unrelated commits in this PR.
nijel: The type errors are not caused by your changes, but by pyright changes. I've pinned working version in https://github.com/python-social-auth/social-core/pull/1072, so if you merge current master, it should pass. I will sqash commits on merge, so don't worry much about messed up history here.
bgruening: @nijel thanks a lot!
marcvs: Yup. merged master and pushed to my PR branch.
Many thanks!
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/python-social-auth/social-core/pull/1068?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) Report
Attention: Patch coverage is `92.38095%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 77.56%. Comparing base [(`326d4b3`)](https://app.codecov.io/gh/python-social-auth/social-core/commit/326d4b3c4aa69e92c19b472b437dd718c5ee5bd8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) to head [(`4f62515`)](https://app.codecov.io/gh/python-social-auth/social-core/commit/4f62515e666e78e7b2fdb2ff012d9ce0ca41d250?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth).
> Report is 120 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/python-social-auth/social-core/pull/1068?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) | Patch % | Lines |
|---|---|---|
| [social\_core/backends/nfdi.py](https://app.codecov.io/gh/python-social-auth/social-core/pull/1068?src=pr&el=tree&filepath=social_core%2Fbackends%2Fnfdi.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth#diff-c29jaWFsX2NvcmUvYmFja2VuZHMvbmZkaS5weQ==) | 90.36% | [8 Missing :warning: ](https://app.codecov.io/gh/python-social-auth/social-core/pull/1068?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1068      +/-   ##
==========================================
- Coverage   77.88%   77.56%   -0.33%     
==========================================
  Files         347      348       +1     
  Lines       10669    10708      +39     
  Branches      504      460      -44     
==========================================
- Hits         8310     8306       -4     
- Misses       2200     2245      +45     
+ Partials      159      157       -2     
```

| [Flag](https://app.codecov.io/gh/python-social-auth/social-core/pull/1068/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/python-social-auth/social-core/pull/1068/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) | `77.56% <92.38%> (-0.33%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/python-social-auth/social-core/pull/1068?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/.github/workflows/pre-commit-shared.yml b/.github/workflows/pre-commit-shared.yml
index c06b4cb5..a7e6a8f4 100644
--- a/.github/workflows/pre-commit-shared.yml
+++ b/.github/workflows/pre-commit-shared.yml
@@ -21,7 +21,7 @@ jobs:
         key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml', 'requirements*.txt') }}
 
     - name: Setup Python
-      uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
+      uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
       with:
         python-version: 3.x
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4a4a209d..eaf82509 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,7 +17,7 @@ repos:
   - id: pretty-format-json
     args: [--no-sort-keys, --autofix, --no-ensure-ascii]
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.11.5
+  rev: v0.11.6
   hooks:
   - id: ruff
     args: [--fix, --exit-non-zero-on-fix]
@@ -57,6 +57,6 @@ repos:
   hooks:
   - id: actionlint
 - repo: https://github.com/woodruffw/zizmor-pre-commit
-  rev: v1.5.2
+  rev: v1.6.0
   hooks:
   - id: zizmor
diff --git a/social_core/backends/lifescience.py b/social_core/backends/lifescience.py
new file mode 100644
index 00000000..05a2ca2e
--- /dev/null
+++ b/social_core/backends/lifescience.py
@@ -0,0 +1,34 @@
+"""
+Backend for OpenID Connect Life Science AAI
+https://lifescience-ri.eu/ls-login.html
+"""
+
+from social_core.backends.open_id_connect import OpenIdConnectAuth
+
+
+class LifeScienceOpenIdConnect(OpenIdConnectAuth):
+    name = "life_science"
+    OIDC_ENDPOINT = "https://login.aai.lifescience-ri.eu/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
+    # Life science, see documentation at
+    # https://lifescience-ri.eu/ls-login.html
+    DEFAULT_SCOPE = ["openid", "email"]
+    JWT_DECODE_OPTIONS = {"verify_at_hash": 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/backends/nfdi.py b/social_core/backends/nfdi.py
new file mode 100644
index 00000000..72d1b64c
--- /dev/null
+++ b/social_core/backends/nfdi.py
@@ -0,0 +1,218 @@
+"""
+Backend for OpenID Connect OPs of the German NFDI initiative
+https://doc.nfdi-aai.de/
+
+This is conceptually based on the egi_checkin backend
+"""
+
+from social_core.backends.open_id_connect import OpenIdConnectAuth
+
+NFDI_ENDPOINTS = {
+    # AcademicID
+    "xcs": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud",
+    "textplus": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud",
+    # didmos
+    "mardi": "https://auth.didmos.nfdi-aai.de",
+    "objects": "https://auth.didmos.nfdi-aai.de",
+    "culture": "https://auth.aai.nfdi4culture.de",
+    # regapp
+    "cat": "https://regapp.nfdi-aai.de/oidc/realms/nfdi",
+    "chem": "https://regapp.nfdi-aai.de/oidc/realms/nfdi",
+    "datascience": "https://regapp.nfdi-aai.de/oidc/realms/nfdi",
+    "energy": "https://regapp.nfdi-aai.de/oidc/realms/nfdi",
+    "ing": "https://regapp.nfdi-aai.de/oidc/realms/nfdi",
+    "matWerk": "https://regapp.nfdi-aai.de/oidc/realms/nfdi",
+    # unity
+    "daphne": "https://login.helmholtz.de/oauth2",
+    "fairmat": "https://login.helmholtz.de/oauth2",
+    "immuno": "https://login.helmholtz.de/oauth2",
+    "punch": "https://login.helmholtz.de/punch-oauth2",
+    "helmholtz": "https://login.helmholtz.de/oauth2",
+    # infraproxy
+    "infraproxy-staging": "https://infraproxy-staging.nfdi-aai.dfn.de",
+    "infraproxy": "https://infraproxy.nfdi-aai.dfn.de",
+    # eduid
+    "eduid": "https://proxy.edu-id.dfn.de",
+    "eduid-staging": "",
+    # other
+    # DataPLANT
+    # GHGA
+    # NFDI4Biodiversity
+    # unassigned
+    # BERD@NFDI
+    # FAIRagro
+    # KonsortSWD
+    # NFDI4BIOIMAGE
+    # NFDI4Earth
+    # NFDI4Health
+    # NFDI4Memory
+    # NFDI4Microbiota
+}
+
+
+class NFDIOpenIdConnect(OpenIdConnectAuth):
+    name = "helmholtz"
+    OIDC_ENDPOINT = "https://login.helmholtz.de/oauth2"
+    # In order to get any scopes, you have to register your service with
+    # the OP
+    USERNAME_KEY = "preferred_username"
+    EXTRA_DATA = [
+        ("expires_in", "expires_in", True),
+        ("refresh_token", "refresh_token", True),
+        ("id_token", "id_token", True),
+    ]
+    DEFAULT_SCOPE = [
+        "openid",
+        "profile",
+        "email",
+        "voperson_id",
+        "eduperson_entitlement",
+        # "entitlement",
+        "eduperson_scoped_affiliation",
+        "voperson_external_affiliation",
+        "eduperson_assurance",
+        "orcid",
+        # "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 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
+
+
+# AcademicID
+class XcsOpenIdConnect(NFDIOpenIdConnect):
+    name = "xcs"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class TextplusOpenIdConnect(NFDIOpenIdConnect):
+    name = "textplus"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+# didmos
+class MardiOpenIdConnect(NFDIOpenIdConnect):
+    name = "mardi"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class ObjectsOpenIdConnect(NFDIOpenIdConnect):
+    name = "objects"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class CultureOpenIdConnect(NFDIOpenIdConnect):
+    name = "culture"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+    # regapp
+
+
+class CatOpenIdConnect(NFDIOpenIdConnect):
+    name = "cat"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class ChemOpenIdConnect(NFDIOpenIdConnect):
+    name = "chem"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class DatascienceOpenIdConnect(NFDIOpenIdConnect):
+    name = "datascience"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class EnergyOpenIdConnect(NFDIOpenIdConnect):
+    name = "energy"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class IngOpenIdConnect(NFDIOpenIdConnect):
+    name = "ing"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class MatWerkOpenIdConnect(NFDIOpenIdConnect):
+    name = "matWerk"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+# unity
+class DaphneOpenIdConnect(NFDIOpenIdConnect):
+    name = "daphne"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class FairmatOpenIdConnect(NFDIOpenIdConnect):
+    name = "fairmat"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class ImmunoOpenIdConnect(NFDIOpenIdConnect):
+    name = "immuno"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class PunchOpenIdConnect(NFDIOpenIdConnect):
+    name = "punch"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class HelmholtzOpenIdConnect(NFDIOpenIdConnect):
+    name = "helmholtz"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+# infraproxy
+class InfraproxyStagingOpenIdConnect(NFDIOpenIdConnect):
+    name = "infraproxy-staging"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class InfraproxyOpenIdConnect(NFDIOpenIdConnect):
+    name = "infraproxy"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+# eduid
+class EduidOpenIdConnect(NFDIOpenIdConnect):
+    name = "eduid"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]
+
+
+class EduidStagingOpenIdConnect(NFDIOpenIdConnect):
+    name = "eduid-staging"
+    OIDC_ENDPOINT = NFDI_ENDPOINTS[name]


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 792fb333..0d402d5b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,7 +24,7 @@ jobs:
         persist-credentials: false
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
+      uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
       with:
         python-version: ${{ matrix.python-version }}
 
@@ -61,7 +61,7 @@ jobs:
         persist-credentials: false
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
+      uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
       with:
         python-version: ${{ matrix.python-version }}
 
diff --git a/social_core/tests/backends/test_lifescience.py b/social_core/tests/backends/test_lifescience.py
new file mode 100644
index 00000000..272d87a4
--- /dev/null
+++ b/social_core/tests/backends/test_lifescience.py
@@ -0,0 +1,269 @@
+import json
+
+from .oauth import BaseAuthUrlTestMixin, OAuth2Test
+from .test_open_id_connect import OpenIdConnectTestMixin
+
+
+class LifeScienceOpenIdConnectTest(
+    OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin
+):
+    backend_path = "social_core.backends.lifescience.LifeScienceOpenIdConnect"
+    issuer = "https://login.aai.lifescience-ri.eu/oidc/"
+    user_data_url = "https://login.aai.lifescience-ri.eu/oidc/userinfo"
+    openid_config_body = """
+    {
+        "request_parameter_supported": true,
+        "claims_parameter_supported": false,
+        "introspection_endpoint": "https://login.aai.lifescience-ri.eu/oidc/introspect",
+        "scopes_supported": [
+            "openid",
+            "profile",
+            "email",
+            "address",
+            "phone",
+            "offline_access",
+            "perun_api",
+            "eduperson_principal_name",
+            "country",
+            "eduperson_assurance",
+            "negotiator_api",
+            "beacon_network_api",
+            "beacon_api",
+            "ssh_public_key",
+            "crypt4ghPublicKeys",
+            "gpgPublicKeys",
+            "negotiator_monitoring",
+            "eduperson_entitlement",
+            "voperson_external_affiliation",
+            "ga4gh_passport_v1",
+            "perun_admin",
+            "eduperson_orcid",
+            "elixir_eduperson_unique_id",
+            "elixir_eduperson_principal_name",
+            "schac_home_organization",
+            "eduperson_scoped_affiliation",
+            "voperson_current_external_affiliation",
+            "authenticating_entity",
+            "minio_policies",
+            "max_user_authentication_capability",
+            "voperson_external_id",
+            "eduperson_unique_id"
+        ],
+        "issuer": "https://login.aai.lifescience-ri.eu/oidc/",
+        "acr_values_supported": [
+            "urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified",
+            "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport",
+            "https://refeds.org/profile/sfa",
+            "https://refeds.org/profile/mfa"
+        ],
+        "userinfo_encryption_enc_values_supported": [
+            "XC20P",
+            "A256CBC+HS512",
+            "A256GCM",
+            "A192GCM",
+            "A128GCM",
+            "A128CBC-HS256",
+            "A192CBC-HS384",
+            "A256CBC-HS512",
+            "A128CBC+HS256"
+        ],
+        "id_token_encryption_enc_values_supported": [
+            "XC20P",
+            "A256CBC+HS512",
+            "A256GCM",
+            "A192GCM",
+            "A128GCM",
+            "A128CBC-HS256",
+            "A192CBC-HS384",
+            "A256CBC-HS512",
+            "A128CBC+HS256"
+        ],
+        "authorization_endpoint": "https://login.aai.lifescience-ri.eu/oidc/authorize",
+        "request_object_encryption_enc_values_supported": [
+            "XC20P",
+            "A256CBC+HS512",
+            "A256GCM",
+            "A192GCM",
+            "A128GCM",
+            "A128CBC-HS256",
+            "A192CBC-HS384",
+            "A256CBC-HS512",
+            "A128CBC+HS256"
+        ],
+        "device_authorization_endpoint": "https://login.aai.lifescience-ri.eu/oidc/devicecode",
+        "userinfo_signing_alg_values_supported": [
+            "HS256",
+            "HS384",
+            "HS512",
+            "RS256",
+            "RS384",
+            "RS512",
+            "ES256",
+            "ES384",
+            "ES512",
+            "PS256",
+            "PS384",
+            "PS512"
+        ],
+        "claims_supported": [
+            "schac_home_organization",
+            "sub",
+            "country",
+            "zoneinfo",
+            "negotiator_monitoring",
+            "voperson_external_affiliation",
+            "birthdate",
+            "beacon_api",
+            "elixir_eduperson_unique_id",
+            "gender",
+            "preferred_username",
+            "locale",
+            "eduperson_principal_name",
+            "eduperson_entitlement",
+            "elixir_eduperson_principal_name",
+            "minio_policies",
+            "updated_at",
+            "crypt4ghPublicKeys",
+            "nickname",
+            "eduperson_scoped_affiliation",
+            "eduperson_unique_id",
+            "voperson_current_external_affiliation",
+            "email",
+            "voperson_external_id",
+            "website",
+            "email_verified",
+            "address",
+            "profile",
+            "phone_number_verified",
+            "max_user_authentication_capability",
+            "given_name",
+            "middle_name",
+            "picture",
+            "ssh_public_key",
+            "authenticating_entity",
+            "beacon_network_api",
+            "name",
+            "phone_number",
+            "eduperson_assurance",
+            "perun_admin",
+            "family_name",
+            "ga4gh_passport_v1",
+            "perun_api",
+            "negotiator_api",
+            "gpgPublicKeys",
+            "eduperson_orcid"
+        ],
+        "claim_types_supported": [
+            "normal"
+        ],
+        "token_endpoint_auth_methods_supported": [
+            "client_secret_basic",
+            "client_secret_post",
+            "none"
+        ],
+        "token_endpoint": "https://login.aai.lifescience-ri.eu/oidc/token",
+        "response_types_supported": [
+            "code",
+            "token id_token"
+        ],
+            "request_uri_parameter_supported": false,
+            "userinfo_encryption_alg_values_supported": [
+            "RSA-OAEP-512",
+            "RSA-OAEP",
+            "RSA-OAEP-256",
+            "RSA1_5",
+            "RSA-OAEP-384"
+        ],
+        "grant_types_supported": [
+            "authorization_code",
+            "implicit",
+            "client_credentials",
+            "refresh_token",
+            "urn:ietf:params:oauth:grant-type:token-exchange",
+            "urn:ietf:params:oauth:grant-type:device_code"
+        ],
+        "end_session_endpoint": "https://login.aai.lifescience-ri.eu/oidc/endsession",
+        "revocation_endpoint": "https://login.aai.lifescience-ri.eu/oidc/revoke",
+        "userinfo_endpoint": "https://login.aai.lifescience-ri.eu/oidc/userinfo",
+        "token_endpoint_auth_signing_alg_values_supported": [
+            "HS256",
+            "HS384",
+            "HS512",
+            "RS256",
+            "RS384",
+            "RS512",
+            "ES256",
+            "ES384",
+            "ES512",
+            "PS256",
+            "PS384",
+            "PS512"
+        ],
+        "require_request_uri_registration": false,
+        "code_challenge_methods_supported": [
+            "plain",
+            "S256",
+            "none"
+        ],
+        "id_token_encryption_alg_values_supported": [
+            "RSA-OAEP-512",
+            "RSA-OAEP",
+            "RSA-OAEP-256",
+            "RSA1_5",
+            "RSA-OAEP-384"
+        ],
+        "jwks_uri": "https://login.aai.lifescience-ri.eu/oidc/jwk",
+        "subject_types_supported": [
+            "public"
+        ],
+        "id_token_signing_alg_values_supported": [
+            "HS256",
+            "HS384",
+            "HS512",
+            "RS256",
+            "RS384",
+            "RS512",
+            "ES256",
+            "ES384",
+            "ES512",
+            "PS256",
+            "PS384",
+            "PS512",
+            "none"
+        ],
+        "registration_endpoint": "https://login.aai.lifescience-ri.eu/oidc/register",
+        "request_object_signing_alg_values_supported": [
+            "HS256",
+            "HS384",
+            "HS512",
+            "RS256",
+            "RS384",
+            "RS512",
+            "ES256",
+            "ES384",
+            "ES512",
+            "PS256",
+            "PS384",
+            "PS512"
+        ],
+        "request_object_encryption_alg_values_supported": [
+            "RSA-OAEP-512",
+            "RSA-OAEP",
+            "RSA-OAEP-256",
+            "RSA1_5",
+            "RSA-OAEP-384"
+        ]
+    }
+    """
+    expected_username = "foo@lifescience-ri.eu"
+    access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"})
+    user_data_body = json.dumps(
+        {
+            "preferred_username": "foo@lifescience-ri.eu",
+            "email": "foo@bar.com",
+            "name": "Foo Bar",
+        }
+    )
+
+    def test_login(self):
+        self.do_login()
diff --git a/social_core/tests/backends/test_nfdi.py b/social_core/tests/backends/test_nfdi.py
new file mode 100644
index 00000000..a74dec8d
--- /dev/null
+++ b/social_core/tests/backends/test_nfdi.py
@@ -0,0 +1,691 @@
+# pyright: reportAttributeAccessIssue=false
+
+from .oauth import BaseAuthUrlTestMixin, OAuth2Test
+from .test_open_id_connect import OpenIdConnectTestMixin
+
+OIDC_CONFIG_ACADEMIC_ID = """
+    {
+      "issuer": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud",
+      "authorization_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/auth",
+      "token_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token",
+      "introspection_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token/introspect",
+      "userinfo_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/userinfo",
+      "end_session_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/logout",
+      "frontchannel_logout_session_supported": true,
+      "frontchannel_logout_supported": true,
+      "jwks_uri": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/certs",
+      "check_session_iframe": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/login-status-iframe.html",
+      "grant_types_supported": [
+        "authorization_code",
+        "implicit",
+        "refresh_token",
+        "password",
+        "client_credentials",
+        "urn:openid:params:grant-type:ciba",
+        "urn:ietf:params:oauth:grant-type:device_code"
+      ],
+      "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",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "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",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "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",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "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://keycloak.sso.gwdg.de/auth/realms/academiccloud/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",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "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",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "HS256",
+        "HS512",
+        "ES256",
+        "RS256",
+        "HS384",
+        "ES512",
+        "PS256",
+        "PS512",
+        "RS512"
+      ],
+      "authorization_signing_alg_values_supported": [
+        "PS384",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "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": [
+        "aud",
+        "sub",
+        "iss",
+        "auth_time",
+        "name",
+        "given_name",
+        "family_name",
+        "preferred_username",
+        "email",
+        "acr"
+      ],
+      "claim_types_supported": [
+        "normal"
+      ],
+      "claims_parameter_supported": true,
+      "scopes_supported": [
+        "initials",
+        "address",
+        "goeidToSub",
+        "MPGR_entryUUID_as_preferred_username",
+        "org",
+        "userservices",
+        "accounttype",
+        "oxUserId",
+        "basic",
+        "employeenumber",
+        "syncAndShareInstance",
+        "phone",
+        "roles",
+        "gwdg-internal-role",
+        "microprofile-jwt",
+        "oxContextId",
+        "emailCS",
+        "goesternMatrikelnummer",
+        "vpngroupsNetworkroles",
+        "subVPNUserType",
+        "acr",
+        "owncloud",
+        "displayName_to_preferred_username",
+        "usRoles",
+        "profile",
+        "goeId",
+        "offline_access",
+        "userContextId",
+        "goesternSAMAccountName",
+        "goesternQuellSystem",
+        "userServices_to_authorities",
+        "memberOfGoeId",
+        "openid",
+        "email",
+        "nfdi_attributes",
+        "memberofdisplayname",
+        "syncAttribute1",
+        "prefuid",
+        "AuthnContextClassRef",
+        "web-origins",
+        "mailAddresses",
+        "uniElectionHash"
+      ],
+      "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://keycloak.sso.gwdg.de/auth/realms/academiccloud/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",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "HS256",
+        "HS512",
+        "ES256",
+        "RS256",
+        "HS384",
+        "ES512",
+        "PS256",
+        "PS512",
+        "RS512"
+      ],
+      "backchannel_logout_supported": true,
+      "backchannel_logout_session_supported": true,
+      "device_authorization_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/auth/device",
+      "backchannel_token_delivery_modes_supported": [
+        "poll",
+        "ping"
+      ],
+      "backchannel_authentication_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/ciba/auth",
+      "backchannel_authentication_request_signing_alg_values_supported": [
+        "PS384",
+        "RS384",
+        "EdDSA",
+        "ES384",
+        "ES256",
+        "RS256",
+        "ES512",
+        "PS256",
+        "PS512",
+        "RS512"
+      ],
+      "require_pushed_authorization_requests": false,
+      "pushed_authorization_request_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/par/request",
+      "mtls_endpoint_aliases": {
+        "token_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token",
+        "revocation_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/revoke",
+        "introspection_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/token/introspect",
+        "device_authorization_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/auth/device",
+        "registration_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/clients-registrations/openid-connect",
+        "userinfo_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/userinfo",
+        "pushed_authorization_request_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/par/request",
+        "backchannel_authentication_endpoint": "https://keycloak.sso.gwdg.de/auth/realms/academiccloud/protocol/openid-connect/ext/ciba/auth"
+      },
+      "authorization_response_iss_parameter_supported": true
+    }
+"""
+
+OIDC_CONFIG_DIDMOS = """
+    {
+      "version": "3.0",
+      "token_endpoint_auth_methods_supported": [
+        "client_secret_basic",
+        "client_secret_post"
+      ],
+      "claims_parameter_supported": true,
+      "request_parameter_supported": false,
+      "request_uri_parameter_supported": false,
+      "require_request_uri_registration": false,
+      "grant_types_supported": [
+        "authorization_code",
+        "implicit"
+      ],
+      "frontchannel_logout_supported": false,
+      "frontchannel_logout_session_supported": false,
+      "backchannel_logout_supported": false,
+      "backchannel_logout_session_supported": false,
+      "issuer": "https://auth.didmos.nfdi-aai.de",
+      "authorization_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/authorization",
+      "jwks_uri": "https://auth.didmos.nfdi-aai.de/OIDC/jwks",
+      "response_types_supported": [
+        "code",
+        "id_token token"
+      ],
+      "id_token_signing_alg_values_supported": [
+        "RS256"
+      ],
+      "response_modes_supported": [
+        "fragment",
+        "query"
+      ],
+      "subject_types_supported": [
+        "public"
+      ],
+      "claim_types_supported": [
+        "normal"
+      ],
+      "claims_supported": [
+        "email",
+        "sub",
+        "preferred_username",
+        "name",
+        "given_name",
+        "family_name",
+        "schac_home_organization",
+        "eduperson_scoped_affiliation",
+        "voperson_external_affiliation",
+        "eduperson_assurance",
+        "voperson_id",
+        "eduperson_unique_id",
+        "voperson_verified_email",
+        "entitlements",
+        "voperson_policy_agreement",
+        "orcid",
+        "voperson_external_id",
+        "eduperson_principal_name",
+        "eduperson_target_id",
+        "session_id"
+      ],
+      "scopes_supported": [
+        "eduperson_assurance",
+        "schac_home_organization",
+        "profile",
+        "voperson_policy_agreement",
+        "voperson_external_affiliation",
+        "eduperson_target_id",
+        "eduperson_unique_id",
+        "voperson_id",
+        "email",
+        "voperson_verified_email",
+        "entitlements",
+        "openid",
+        "voperson_external_id",
+        "eduperson_scoped_affiliation",
+        "orcid"
+      ],
+      "token_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/token",
+      "registration_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/registration",
+      "end_session_endpoint": "https://auth.didmos.nfdi-aai.de/didmos/logout",
+      "code_challenge_methods_supported": [
+        "S256",
+        "S384",
+        "S512"
+      ],
+      "userinfo_endpoint": "https://auth.didmos.nfdi-aai.de/OIDC/userinfo"
+    }
+"""
+
+OIDC_CONFIG_REGAPP = """
+    {
+      "response_types_supported": [
+        "code",
+        "id_token",
+        "code id_token"
+      ],
+      "request_uri_parameter_supported": true,
+      "introspection_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/tokeninfo",
+      "grant_types_supported": [
+        "authorization_code",
+        "refresh_token"
+      ],
+      "scopes_supported": [
+        "openid",
+        "profile",
+        "email"
+      ],
+      "issuer": "https://regapp.nfdi-aai.de/oidc/realms/nfdi",
+      "authorization_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/auth",
+      "userinfo_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/userinfo",
+      "claims_supported": [
+        "sub",
+        "iss",
+        "aud",
+        "mail",
+        "name"
+      ],
+      "jwks_uri": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/certs",
+      "subject_types_supported": [
+        "pairwise",
+        "public"
+      ],
+      "id_token_signing_alg_values_supported": [
+        "RS256"
+      ],
+      "token_endpoint_auth_methods_supported": [
+        "client_secret_post",
+        "private_key_jwt",
+        "client_secret_basic"
+      ],
+      "response_modes_supported": [
+        "query",
+        "fragment"
+      ],
+      "token_endpoint": "https://regapp.nfdi-aai.de/oidc/realms/nfdi/protocol/openid-connect/token"
+    }
+"""
+
+OIDC_CONFIG_UNITY = """
+    {
+      "authorization_endpoint": "https://login.helmholtz.de/oauth2-as/oauth2-authz",
+      "token_endpoint": "https://login.helmholtz.de/oauth2/token",
+      "introspection_endpoint": "https://login.helmholtz.de/oauth2/introspect",
+      "revocation_endpoint": "https://login.helmholtz.de/oauth2/revoke",
+      "issuer": "https://login.helmholtz.de/oauth2",
+      "jwks_uri": "https://login.helmholtz.de/oauth2/jwk",
+      "scopes_supported": [
+        "openid",
+        "display_name",
+        "sn",
+        "single-logout",
+        "offline_access",
+        "voperson_id",
+        "voperson_external_affiliation",
+        "entitlements",
+        "org_domain",
+        "email",
+        "profile",
+        "credentials",
+        "eduperson_scoped_affiliation",
+        "eduperson_entitlement",
+        "eduperson_principal_name",
+        "eduperson_unique_id",
+        "eduperson_assurance",
+        "sys:scim:read_profile",
+        "sys:scim:read_memberships",
+        "sys:scim:read_self_group"
+      ],
+      "response_types_supported": [
+        "code",
+        "token",
+        "id_token",
+        "code id_token",
+        "id_token token",
+        "code token",
+        "code id_token token"
+      ],
+      "response_modes_supported": [
+        "query",
+        "fragment"
+      ],
+      "grant_types_supported": [
+        "authorization_code",
+        "implicit"
+      ],
+      "code_challenge_methods_supported": [
+        "plain",
+        "S256"
+      ],
+      "request_uri_parameter_supported": true,
+      "subject_types_supported": [
+        "public"
+      ],
+      "userinfo_endpoint": "https://login.helmholtz.de/oauth2/userinfo",
+      "id_token_signing_alg_values_supported": [
+        "RS256",
+        "ES256"
+      ]
+    }
+"""
+
+OIDC_CONFIG_UNITY_PUNCH = """
+    {
+      "authorization_endpoint": "https://login.helmholtz.de/punch-oauth2-as/oauth2-authz",
+      "token_endpoint": "https://login.helmholtz.de/punch-oauth2/token",
+      "introspection_endpoint": "https://login.helmholtz.de/punch-oauth2/introspect",
+      "revocation_endpoint": "https://login.helmholtz.de/punch-oauth2/revoke",
+      "issuer": "https://login.helmholtz.de/punch-oauth2",
+      "jwks_uri": "https://login.helmholtz.de/punch-oauth2/jwk",
+      "scopes_supported": [
+        "openid",
+        "display_name",
+        "sn",
+        "single-logout",
+        "offline_access",
+        "email",
+        "profile",
+        "credentials",
+        "eduperson_scoped_affiliation",
+        "eduperson_entitlement",
+        "eduperson_principal_name",
+        "eduperson_unique_id",
+        "eduperson_assurance",
+        "sys:scim:read_profile",
+        "sys:scim:read_memberships",
+        "sys:scim:read_self_group"
+      ],
+      "response_types_supported": [
+        "code",
+        "token",
+        "id_token",
+        "code id_token",
+        "id_token token",
+        "code token",
+        "code id_token token"
+      ],
+      "response_modes_supported": [
+        "query",
+        "fragment"
+      ],
+      "grant_types_supported": [
+        "authorization_code",
+        "implicit"
+      ],
+      "code_challenge_methods_supported": [
+        "plain",
+        "S256"
+      ],
+      "request_uri_parameter_supported": true,
+      "subject_types_supported": [
+        "public"
+      ],
+      "userinfo_endpoint": "https://login.helmholtz.de/punch-oauth2/userinfo",
+      "id_token_signing_alg_values_supported": [
+        "RS256",
+        "ES256"
+      ]
+    }
+"""
+
+
+class NFDIOpenIdConnectTest(OpenIdConnectTestMixin, OAuth2Test, BaseAuthUrlTestMixin):
+    backend_path = "social_core.backends.nfdi.HelmholtzOpenIdConnect"
+    issuer = "https://login.helmholtz.de/oauth2"
+    openid_config_body = OIDC_CONFIG_UNITY
+    user_data_url = "https://login.mouseton.edu/oauth2/userinfo"
+    user_data_body = {
+        "display_name": "Donald Duck",
+        "eduperson_assurance": [
+            "https://refeds.org/assurance",
+            "https://refeds.org/assurance/ID/unique",
+            "https://refeds.org/assurance/ID/eppn-unique-no-reassign",
+            "https://refeds.org/assurance/ATP/ePA-1d",
+            "https://refeds.org/assurance/ATP/ePA-1m",
+            "https://refeds.org/assurance/IAP/local-enterprise",
+            "https://refeds.org/assurance/IAP/low",
+            "https://refeds.org/assurance/IAP/medium",
+            "https://refeds.org/assurance/profile/cappuccino",
+            "https://aarc-project.eu/policy/authn-assurance/assam",
+        ],
+        "eduperson_entitlement": [
+            "urn:mace:dir:entitlement:common-lib-terms",
+            "http://bwidm.de/entitlement/bwLSDF-SyncShare",
+            "urn:canard:mouseton.edu:group:KIT#login.mouseton.edu",
+            "urn:canard:h-df.de:group:m-team:feudal-developers#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:Arbeitskreise#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:HIFIS:Associates#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:Arbeitskreise:AG IT Services#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:Helmholtz-member#login.mouseton.edu",
+            "urn:canard:h-df.de:group:m-team#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:Helmholtz-all#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:HIFIS#login.mouseton.edu",
+        ],
+        "eduperson_principal_name": "lo0018@duckburg.edu",
+        "eduperson_scoped_affiliation": [
+            "employee@login.mouseton.edu",
+            "member@login.mouseton.edu",
+        ],
+        "eduperson_unique_id": "42234223422342234223422342234223@login.mouseton.edu",
+        "email": "donald.hardt@duckburg.edu",
+        "email_verified": True,
+        "entitlements": [
+            "urn:mace:dir:entitlement:common-lib-terms",
+            "http://duckburg.edu/entitlement/sync-and-share",
+            "urn:canard:mouseton.edu:group:KIT#login.mouseton.edu",
+            "urn:canard:duckburg.edu:group:m-team:developers#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:Workgroup:IT Services#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:mouseton-member#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:m-team#login.mouseton.edu",
+            "urn:canard:mouseton.edu:group:mouseton-all#login.mouseton.edu",
+        ],
+        "family_name": "Duck",
+        "given_name": "Donald",
+        "iss": "https://login.mouseton.edu/oauth2",
+        "name": "Donald Duck",
+        "org_domain": "duckburg.edu",
+        "preferred_username": "donald",
+        "sn": "Duck",
+        "ssh_public_key": "ssh-ed25519 AAAAC3N4224224224224224224224224224224224224224224224224224224223ym/ donald@home\n",
+        "sub": "42234223-4223-4223-4223-422342234223",
+        "voperson_external_affiliation": [
+            "employee@duckburg.edu",
+            "faculty@duckburg.edu",
+            "member@duckburg.edu",
+        ],
+        "voperson_id": "42234223422342234223422342234223@login.mouseton.edu",
+    }
+
+    def test_do_not_override_endpoint(self):
+        self.backend.OIDC_ENDPOINT = self.issuer
+        self.assertEqual(self.backend.oidc_endpoint(), self.issuer)
+
+    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)
+
+    def test_get_user_details(self):
+        testdata = self.backend.get_user_details(self.user_data_body)
+        # print(F"{testdata=}")
+        self.assertEqual(testdata["username"], "donald")
+        self.assertEqual(testdata["email"], "donald.hardt@duckburg.edu")
+        self.assertEqual(testdata["fullname"], "Donald Duck")
+        self.assertEqual(testdata["first_name"], "Donald")
+        self.assertEqual(testdata["last_name"], "Duck")