Repository: python-social-auth/social-core
Pull request #1127 (2025-04-24 11:34:52)

Add support to SOCIAL_AUTH_OIDC_PROMPT and others
This is related to https://github.com/python-social-auth/social-core/issues/1124.

This is my first PR to this project. Feedback is welcome, specially regarding

- [ ] how to trigger a logging warning?
- [ ] how to add a test to this new feature?

If this PR looks good, I will submit others to complete https://github.com/python-social-auth/social-core/issues/1124.



rgaiacs: A PR with documentation was sent, see https://github.com/python-social-auth/social-docs/pull/288.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/python-social-auth/social-core/pull/1127?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 `44.44444%` with `5 lines` in your changes missing coverage. Please review.
> Project coverage is 77.99%. 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 [(`ea244d6`)](https://app.codecov.io/gh/python-social-auth/social-core/commit/ea244d63158ec833d9a0a7f57846d8a876b22a1b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth).
> Report is 154 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/python-social-auth/social-core/pull/1127?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/open\_id\_connect.py](https://app.codecov.io/gh/python-social-auth/social-core/pull/1127?src=pr&el=tree&filepath=social_core%2Fbackends%2Fopen_id_connect.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth#diff-c29jaWFsX2NvcmUvYmFja2VuZHMvb3Blbl9pZF9jb25uZWN0LnB5) | 44.44% | [3 Missing and 2 partials :warning: ](https://app.codecov.io/gh/python-social-auth/social-core/pull/1127?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    #1127      +/-   ##
==========================================
+ Coverage   77.88%   77.99%   +0.10%     
==========================================
  Files         347      353       +6     
  Lines       10669    10802     +133     
  Branches      504      466      -38     
==========================================
+ Hits         8310     8425     +115     
- Misses       2200     2214      +14     
- Partials      159      163       +4     
```

| [Flag](https://app.codecov.io/gh/python-social-auth/social-core/pull/1127/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/1127/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=python-social-auth) | `77.99% <44.44%> (+0.10%)` | :arrow_up: |

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/1127?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>
rgaiacs: Thanks for the feedback. I expanded to match the list of parameters explicitly mentioned in the [Authentication Request section](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).

I don't implemented all parameters because of my limited knowledge of OpenID.

Patch:
diff --git a/.github/workflows/pre-commit-shared.yml b/.github/workflows/pre-commit-shared.yml
index e0b45224..94a1c1ab 100644
--- a/.github/workflows/pre-commit-shared.yml
+++ b/.github/workflows/pre-commit-shared.yml
@@ -25,7 +25,7 @@ jobs:
       with:
         python-version: 3.x
 
-    - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
+    - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
 
     - run: uvx pre-commit run --all
       env:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1cd4056a..174c3bd7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,7 +16,7 @@ jobs:
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       with:
         persist-credentials: false
-    - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
+    - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
 
     - run: uv build
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 98158e73..09a13dea 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.8
+  rev: v0.11.11
   hooks:
   - id: ruff
     args: [--fix, --exit-non-zero-on-fix]
@@ -59,6 +59,6 @@ repos:
   hooks:
   - id: actionlint
 - repo: https://github.com/woodruffw/zizmor-pre-commit
-  rev: v1.6.0
+  rev: v1.8.0
   hooks:
   - id: zizmor
diff --git a/pyproject.toml b/pyproject.toml
index eb041bb6..f6cf3eab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,16 +8,16 @@ dev = [
   "flake8>=5.0.4",
   "google-auth-stubs==0.3.0",
   "mypy==1.15.0",
-  "pyright==1.1.400",
+  "pyright==1.1.401",
   "pytest-cov>=2.7.1",
   "pytest-xdist>=3.6.1",
   "pytest>=4.5",
   "responses==0.25.7",
-  "types-defusedxml==0.7.0.20240218",
-  "types-oauthlib==3.2.0.20250408",
-  "types-python-jose==3.4.0.20250224",
-  "types-requests-oauthlib==2.0.0.20250306",
-  "types-requests==2.32.0.20250328"
+  "types-defusedxml==0.7.0.20250516",
+  "types-oauthlib==3.2.0.20250516",
+  "types-python-jose==3.4.0.20250516",
+  "types-requests-oauthlib==2.0.0.20250516",
+  "types-requests==2.32.0.20250515"
 ]
 
 [project]
@@ -76,19 +76,19 @@ dev = [
   "flake8>=5.0.4",
   "google-auth-stubs==0.3.0",
   "mypy==1.15.0",
-  "pyright==1.1.400",
+  "pyright==1.1.401",
   "pytest-cov>=2.7.1",
   "pytest-xdist>=3.6.1",
   "pytest>=4.5",
   "responses==0.25.7",
-  "types-defusedxml==0.7.0.20240218",
-  "types-oauthlib==3.2.0.20250408",
-  "types-python-jose==3.4.0.20250224",
-  "types-requests-oauthlib==2.0.0.20250306",
-  "types-requests==2.32.0.20250328"
+  "types-defusedxml==0.7.0.20250516",
+  "types-oauthlib==3.2.0.20250516",
+  "types-python-jose==3.4.0.20250516",
+  "types-requests-oauthlib==2.0.0.20250516",
+  "types-requests==2.32.0.20250515"
 ]
 google-onetap = [
-  "google-auth~=2.39.0"
+  "google-auth~=2.40.0"
 ]
 ping = [
   "python-jose>=3.4.0,<4.0"
diff --git a/social_core/backends/linkedin.py b/social_core/backends/linkedin.py
index dcc49bb8..d3c6ac76 100644
--- a/social_core/backends/linkedin.py
+++ b/social_core/backends/linkedin.py
@@ -48,13 +48,13 @@ class LinkedinOAuth2(BaseOAuth2):
     name = "linkedin-oauth2"
     AUTHORIZATION_URL = "https://www.linkedin.com/oauth/v2/authorization"
     ACCESS_TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken"
-    USER_DETAILS_URL = "https://api.linkedin.com/v2/userinfo?projection=({projection})"
+    USER_DETAILS_URL = "https://api.linkedin.com/v2/me?projection=({projection})"
     USER_EMAILS_URL = (
         "https://api.linkedin.com/v2/emailAddress"
         "?q=members&projection=(elements*(handle~))"
     )
     REDIRECT_STATE = False
-    DEFAULT_SCOPE = ["email", "profile", "openid"]
+    DEFAULT_SCOPE = ["r_liteprofile"]
     EXTRA_DATA = [
         ("id", "id"),
         ("expires_in", "expires"),
@@ -65,7 +65,14 @@ class LinkedinOAuth2(BaseOAuth2):
     ]
 
     def user_details_url(self):
-        return self.USER_DETAILS_URL
+        # use set() since LinkedIn fails when values are duplicated
+        fields_selectors = list(
+            {"id", "firstName", "lastName", *self.setting("FIELD_SELECTORS", [])}
+        )
+        # user sort to ease the tests URL mocking
+        fields_selectors.sort()
+        fields_selectors = ",".join(fields_selectors)
+        return self.USER_DETAILS_URL.format(projection=fields_selectors)
 
     def user_emails_url(self):
         return self.USER_EMAILS_URL
@@ -75,10 +82,10 @@ class LinkedinOAuth2(BaseOAuth2):
             self.user_details_url(), headers=self.user_data_headers(access_token)
         )
 
-        if "email" in set(self.setting("FIELD_SELECTORS", [])):
+        if "emailAddress" in set(self.setting("FIELD_SELECTORS", [])):
             emails = self.email_data(access_token, *args, **kwargs)
             if emails:
-                response["email"] = emails[0]
+                response["emailAddress"] = emails[0]
 
         return response
 
@@ -88,20 +95,38 @@ class LinkedinOAuth2(BaseOAuth2):
         )
         email_addresses = []
         for element in response.get("elements", []):
-            email_address = element.get("handle~", {}).get("email")
+            email_address = element.get("handle~", {}).get("emailAddress")
             email_addresses.append(email_address)
         return list(filter(None, email_addresses))
 
     def get_user_details(self, response):
         """Return user details from Linkedin account"""
-        response = self.user_data(access_token=response["access_token"])
+
+        def get_localized_name(name):
+            """
+            FirstName & Last Name object
+            {
+                  'localized': {
+                     'en_US': 'Smith'
+                  },
+                  'preferredLocale': {
+                     'country': 'US',
+                     'language': 'en'
+                  }
+            }
+            :return the localizedName from the lastName object
+            """
+            locale = "{}_{}".format(
+                name["preferredLocale"]["language"], name["preferredLocale"]["country"]
+            )
+            return name["localized"].get(locale, "")
+
         fullname, first_name, last_name = self.get_user_names(
-            first_name=response["given_name"],
-            last_name=response["family_name"],
+            first_name=get_localized_name(response["firstName"]),
+            last_name=get_localized_name(response["lastName"]),
         )
-        email = response.get("email", "")
+        email = response.get("emailAddress", "")
         return {
-            "id": response.get("sub", ""),
             "username": first_name + last_name,
             "fullname": fullname,
             "first_name": first_name,
diff --git a/social_core/backends/okta.py b/social_core/backends/okta.py
index 87c31e25..8b06186c 100644
--- a/social_core/backends/okta.py
+++ b/social_core/backends/okta.py
@@ -3,7 +3,7 @@ Okta OAuth2 and OpenIdConnect:
     https://python-social-auth.readthedocs.io/en/latest/backends/okta.html
 """
 
-from urllib.parse import urljoin
+from urllib.parse import urljoin, urlparse, urlunparse
 
 from ..utils import append_slash
 from .oauth import BaseOAuth2
@@ -22,15 +22,26 @@ class OktaMixin(BaseOAuth2):
     def _url(self, path):
         return urljoin(append_slash(self.setting("API_URL")), path)
 
-    def oidc_config(self):
-        return self.get_json(
-            self._url(
-                "/.well-known/openid-configuration?client_id={}".format(
-                    self.setting("KEY")
-                )
-            )
+    def oidc_config_url(self):
+        # https://developer.okta.com/docs/reference/api/oidc/#well-known-openid-configuration
+        url = urlparse(self.api_url())
+
+        # If the URL path does not contain an authorizedServerId, we need
+        # to truncate the path in order to generate a proper openid-configuration
+        # URL.
+        if url.path == "/oauth2/":
+            url = url._replace(path="")
+
+        return urljoin(
+            urlunparse(url),
+            "./.well-known/openid-configuration?client_id={}".format(
+                self.setting("KEY")
+            ),
         )
 
+    def oidc_config(self):
+        return self.get_json(self.oidc_config_url())
+
 
 class OktaOAuth2(OktaMixin, BaseOAuth2):
     """Okta OAuth authentication backend"""
diff --git a/social_core/backends/open_id_connect.py b/social_core/backends/open_id_connect.py
index c740fcfd..013e2a12 100644
--- a/social_core/backends/open_id_connect.py
+++ b/social_core/backends/open_id_connect.py
@@ -16,7 +16,12 @@ from jwt import (
 from jwt.utils import base64url_decode
 
 from social_core.backends.oauth import BaseOAuth2
-from social_core.exceptions import AuthMissingParameter, AuthTokenError
+from social_core.exceptions import (
+    AuthInvalidParameter,
+    AuthMissingParameter,
+    AuthNotImplementedParameter,
+    AuthTokenError,
+)
 from social_core.utils import cache
 
 
@@ -64,6 +69,14 @@ class OpenIdConnectAuth(BaseOAuth2):
     USERINFO_URL = ""
     JWKS_URI = ""
     TOKEN_ENDPOINT_AUTH_METHOD = ""
+    # Optional parameters for Authentication Request
+    DISPLAY = None
+    PROMPT = None
+    MAX_AGE = None
+    UI_LOCALES = None
+    ID_TOKEN_HINT = None
+    LOGIN_HINT = None
+    ACR_VALUES = None
 
     def __init__(self, *args, **kwargs):
         self.id_token = None
@@ -136,10 +149,59 @@ class OpenIdConnectAuth(BaseOAuth2):
         response = self.request(self.jwks_uri())
         return json.loads(response.text)["keys"]
 
-    def auth_params(self, state=None):
+    def auth_params(self, state=None):  # noqa: C901
         """Return extra arguments needed on auth process."""
         params = super().auth_params(state)
         params["nonce"] = self.get_and_store_nonce(self.authorization_url(), state)
+
+        display = self.setting("DISPLAY", default=self.DISPLAY)
+        if display is not None:
+            if not display:
+                raise AuthMissingParameter(
+                    self, "OpenID Connect display value cannot be empty string."
+                )
+
+            if display not in ("page", "popup", "touch", "wap"):
+                raise AuthMissingParameter(
+                    self, f"Invalid OpenID Connect display value: {display}"
+                )
+
+            params["display"] = display
+
+        prompt = self.setting("PROMPT", default=self.PROMPT)
+        if prompt is not None:
+            if not prompt:
+                raise AuthInvalidParameter(self, "prompt")
+
+            for prompt_token in prompt.split():
+                if prompt_token not in ("none", "login", "consent", "select_account"):
+                    raise AuthInvalidParameter(self, "prompt")
+
+            params["prompt"] = prompt
+
+        max_age = self.setting("MAX_AGE", default=self.MAX_AGE)
+        if max_age is not None:
+            if max_age < 0:
+                raise AuthInvalidParameter(self, "max_age")
+
+            params["max_age"] = max_age
+
+        ui_locales = self.setting("UI_LOCALES", default=self.UI_LOCALES)
+        if ui_locales is not None:
+            raise AuthNotImplementedParameter(self, "ui_locales")
+
+        id_token_hint = self.setting("ID_TOKEN_HINT", default=self.ID_TOKEN_HINT)
+        if id_token_hint is not None:
+            raise AuthNotImplementedParameter(self, "id_token_hint")
+
+        login_hint = self.setting("LOGIN_HINT", default=self.LOGIN_HINT)
+        if login_hint is not None:
+            raise AuthNotImplementedParameter(self, "login_hint")
+
+        acr_values = self.setting("ACR_VALUES", default=self.ACR_VALUES)
+        if acr_values is not None:
+            raise AuthNotImplementedParameter(self, "acr_values")
+
         return params
 
     def get_and_store_nonce(self, url, state):
diff --git a/social_core/exceptions.py b/social_core/exceptions.py
index c5a70d54..00a48f2d 100644
--- a/social_core/exceptions.py
+++ b/social_core/exceptions.py
@@ -89,6 +89,20 @@ class AuthMissingParameter(AuthException):
         return f"Missing needed parameter {self.parameter}"
 
 
+class AuthInvalidParameter(AuthMissingParameter):
+    """Invalid value for parameter to start or complete the process."""
+
+    def __str__(self):
+        return f"Invalid value for parameter {self.parameter}"
+
+
+class AuthNotImplementedParameter(AuthMissingParameter):
+    """Optional parameter not implemented to start or complete the process."""
+
+    def __str__(self):
+        return f"Not implemented parameter {self.parameter}"
+
+
 class AuthStateMissing(AuthException):
     """State parameter is incorrect."""
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f4aa375f..fe26b205 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -28,7 +28,7 @@ jobs:
       with:
         python-version: ${{ matrix.python-version }}
 
-    - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
+    - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
       with:
         cache-suffix: ${{ matrix.python-version }}
 
@@ -65,7 +65,7 @@ jobs:
       with:
         python-version: ${{ matrix.python-version }}
 
-    - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
+    - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0
       with:
         cache-suffix: ${{ matrix.python-version }}
 
@@ -77,7 +77,7 @@ jobs:
     - name: Test with tox
       run: uvx tox -e "py${PYTHON_VERSION/\./}"
 
-    - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
+    - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
       with:
         flags: unittests
         name: Python ${{ matrix.python-version }}
diff --git a/social_core/tests/backends/test_linkedin.py b/social_core/tests/backends/test_linkedin.py
index 6a90d489..0f3bcff0 100644
--- a/social_core/tests/backends/test_linkedin.py
+++ b/social_core/tests/backends/test_linkedin.py
@@ -41,23 +41,24 @@ class LinkedinOpenIdConnectTest(OpenIdConnectTest, BaseAuthUrlTestMixin):
 
 
 class BaseLinkedinTest:
-    user_data_url = "https://api.linkedin.com/v2/userinfo"
+    user_data_url = "https://api.linkedin.com/v2/me?projection=(firstName,id,lastName)"
     expected_username = "FooBar"
     access_token_body = json.dumps({"access_token": "foobar", "token_type": "bearer"})
 
     # Reference:
-    # https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self
-    # -serve/sign-in-with-linkedin-v2#response-body-schema
+    # https://docs.microsoft.com/en-us/linkedin/consumer/integrations/self
+    # -serve/sign-in-with-linkedin?context=linkedin/consumer/context#api-request
     user_data_body = json.dumps(
         {
-            "sub": "782bbtaQ",
-            "name": "FooBar",
-            "given_name": "Foo",
-            "family_name": "Bar",
-            "picture": "https://media.licdn-ei.com/dms/image/C5F03AQHqK8v7tB1HCQ/profile-displayphoto-shrink_100_100/0/",
-            "locale": "en-US",
-            "email": "doe@email.com",
-            "email_verified": True,
+            "id": "1010101010",
+            "firstName": {
+                "localized": {"en_US": "Foo"},
+                "preferredLocale": {"country": "US", "language": "en"},
+            },
+            "lastName": {
+                "localized": {"en_US": "Bar"},
+                "preferredLocale": {"country": "US", "language": "en"},
+            },
         }
     )
 
diff --git a/social_core/tests/backends/test_okta.py b/social_core/tests/backends/test_okta.py
index 7635b38a..a3191ec8 100644
--- a/social_core/tests/backends/test_okta.py
+++ b/social_core/tests/backends/test_okta.py
@@ -187,3 +187,24 @@ class OktaOpenIdConnectTest(OpenIdConnectTest):
 
     def test_everything_works(self):
         self.do_login()
+
+    def test_okta_oidc_config(self):
+        # With no custom authorization server
+        self.strategy.set_settings(
+            {
+                "SOCIAL_AUTH_OKTA_OPENIDCONNECT_API_URL": "https://dev-000000.oktapreview.com/oauth2",
+            }
+        )
+        self.assertEqual(
+            self.backend.oidc_config_url(),
+            "https://dev-000000.oktapreview.com/.well-known/openid-configuration?client_id=a-key",
+        )
+        self.strategy.set_settings(
+            {
+                "SOCIAL_AUTH_OKTA_OPENIDCONNECT_API_URL": "https://dev-000000.oktapreview.com/oauth2/id-123456",
+            }
+        )
+        self.assertEqual(
+            self.backend.oidc_config_url(),
+            "https://dev-000000.oktapreview.com/oauth2/id-123456/.well-known/openid-configuration?client_id=a-key",
+        )