From c86bf2a7252064612a95f2703fa96f0e6136901c Mon Sep 17 00:00:00 2001 From: stealthycole Date: Mon, 9 Jun 2025 15:18:55 -0700 Subject: [PATCH 1/6] Create azuread_oid backend This is just a slightly modified version of the azure_tenant backend that uses OID instead of sub claim as the UID. Allows admins to more-safely switch to a version of Azure SSO that uses OID. Issue: https://github.com/python-social-auth/social-core/issues/684 --- social_core/backends/azuread_oid | 137 +++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 social_core/backends/azuread_oid diff --git a/social_core/backends/azuread_oid b/social_core/backends/azuread_oid new file mode 100644 index 000000000..c6883aa8f --- /dev/null +++ b/social_core/backends/azuread_oid @@ -0,0 +1,137 @@ +import base64 + +from cryptography.hazmat.backends import default_backend +from cryptography.x509 import load_der_x509_certificate +from jwt import DecodeError, ExpiredSignatureError, get_unverified_header +from jwt import decode as jwt_decode + +from social_core.exceptions import AuthTokenError + +from .azuread import AzureADOAuth2 + +""" +Copyright (c) 2015 Microsoft Open Technologies, Inc. + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +""" +Azure AD OAuth2 backend, docs at: + https://python-social-auth.readthedocs.io/en/latest/backends/azuread.html + +See https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx +for verifying JWT tokens. +""" + + +class AzureADOIDOAuth2(AzureADOAuth2): + name = "azuread-oid-oauth2" + OPENID_CONFIGURATION_URL = "{base_url}/.well-known/openid-configuration{appid}" + JWKS_URL = "{base_url}/discovery/keys{appid}" + + @property + def tenant_id(self): + return self.setting("TENANT_ID", "common") + + def openid_configuration_url(self): + return self.OPENID_CONFIGURATION_URL.format( + base_url=self.base_url, appid=self._appid() + ) + + def jwks_url(self): + return self.JWKS_URL.format(base_url=self.base_url, appid=self._appid()) + + def _appid(self) -> str: + return ( + f"?appid={self.setting('KEY')}" if self.setting("KEY") is not None else "" + ) + + def get_certificate(self, kid): + # retrieve keys from jwks_url + resp = self.request(self.jwks_url(), method="GET") + resp.raise_for_status() + + # find the proper key for the kid + for key in resp.json()["keys"]: + if key["kid"] == kid: + x5c = key["x5c"][0] + break + else: + raise DecodeError(f"Cannot find kid={kid}") + + return load_der_x509_certificate(base64.b64decode(x5c), default_backend()) + + def get_user_id(self, details, response): + """Use account oid as unique id.""" + return response.get("oid") + + def user_data(self, access_token, *args, **kwargs): + response = kwargs.get("response") + if response and response.get("id_token"): + id_token = response.get("id_token") + else: + id_token = access_token + + # get key id and algorithm + key_id = get_unverified_header(id_token)["kid"] + + try: + # retrieve certificate for key_id + certificate = self.get_certificate(key_id) + + return jwt_decode( + id_token, + key=certificate.public_key(), # type: ignore[reportArgumentType] + algorithms=["RS256"], + audience=self.setting("KEY"), + ) + except (DecodeError, ExpiredSignatureError) as error: + raise AuthTokenError(self, error) + + +class AzureADV2OIDOAuth2(AzureADOIDOAuth2): + name = "azuread-v2-OID-oauth2" + OPENID_CONFIGURATION_URL = "{base_url}/v2.0/.well-known/openid-configuration{appid}" + AUTHORIZATION_URL = "{base_url}/oauth2/v2.0/authorize" + ACCESS_TOKEN_URL = "{base_url}/oauth2/v2.0/token" + JWKS_URL = "{base_url}/discovery/v2.0/keys{appid}" + DEFAULT_SCOPE = ["openid", "profile", "offline_access"] + + def get_user_id(self, details, response): + """Use upn as unique id""" + return response.get("preferred_username") + + def get_user_details(self, response): + """Return user details from Azure AD account""" + fullname, first_name, last_name = ( + response.get("name", ""), + response.get("given_name", ""), + response.get("family_name", ""), + ) + return { + "username": fullname, + "email": response.get("preferred_username"), + "fullname": fullname, + "first_name": first_name, + "last_name": last_name, + } From d806425f7e3a8b4fae3a56558cd1a2e1b616472e Mon Sep 17 00:00:00 2001 From: stealthycole Date: Mon, 9 Jun 2025 15:27:53 -0700 Subject: [PATCH 2/6] Update CHANGELOG.md for Azure OID backend --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad46d35c9..095e41ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +### Changed + +- Added Azure OID backend + ## [4.6.1](https://github.com/python-social-auth/social-core/releases/tag/4.6.1) - 2025-04-28 ### Changed From e5eb6ece02c0703ecb4d240315531d5b7af6bf7a Mon Sep 17 00:00:00 2001 From: stealthycole Date: Mon, 9 Jun 2025 15:31:01 -0700 Subject: [PATCH 3/6] Add semantic version and tag --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 095e41ee2..6dc4b31f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [4.6.2](https://github.com/python-social-auth/social-core/releases/tag/4.6.2) - 2025-06-09 + ### Changed - Added Azure OID backend From bbaaf47c889f6b4548665945b982b467e14669a7 Mon Sep 17 00:00:00 2001 From: stealthycole Date: Fri, 13 Jun 2025 07:46:40 -0700 Subject: [PATCH 4/6] Trimming duplicate class definitions --- social_core/backends/azuread_oid | 60 +------------------------------- 1 file changed, 1 insertion(+), 59 deletions(-) diff --git a/social_core/backends/azuread_oid b/social_core/backends/azuread_oid index c6883aa8f..0bcb9c30e 100644 --- a/social_core/backends/azuread_oid +++ b/social_core/backends/azuread_oid @@ -44,71 +44,13 @@ for verifying JWT tokens. """ -class AzureADOIDOAuth2(AzureADOAuth2): +class AzureADOIDOAuth2(AzureADTenantOAuth2): name = "azuread-oid-oauth2" - OPENID_CONFIGURATION_URL = "{base_url}/.well-known/openid-configuration{appid}" - JWKS_URL = "{base_url}/discovery/keys{appid}" - - @property - def tenant_id(self): - return self.setting("TENANT_ID", "common") - - def openid_configuration_url(self): - return self.OPENID_CONFIGURATION_URL.format( - base_url=self.base_url, appid=self._appid() - ) - - def jwks_url(self): - return self.JWKS_URL.format(base_url=self.base_url, appid=self._appid()) - - def _appid(self) -> str: - return ( - f"?appid={self.setting('KEY')}" if self.setting("KEY") is not None else "" - ) - - def get_certificate(self, kid): - # retrieve keys from jwks_url - resp = self.request(self.jwks_url(), method="GET") - resp.raise_for_status() - - # find the proper key for the kid - for key in resp.json()["keys"]: - if key["kid"] == kid: - x5c = key["x5c"][0] - break - else: - raise DecodeError(f"Cannot find kid={kid}") - - return load_der_x509_certificate(base64.b64decode(x5c), default_backend()) def get_user_id(self, details, response): """Use account oid as unique id.""" return response.get("oid") - def user_data(self, access_token, *args, **kwargs): - response = kwargs.get("response") - if response and response.get("id_token"): - id_token = response.get("id_token") - else: - id_token = access_token - - # get key id and algorithm - key_id = get_unverified_header(id_token)["kid"] - - try: - # retrieve certificate for key_id - certificate = self.get_certificate(key_id) - - return jwt_decode( - id_token, - key=certificate.public_key(), # type: ignore[reportArgumentType] - algorithms=["RS256"], - audience=self.setting("KEY"), - ) - except (DecodeError, ExpiredSignatureError) as error: - raise AuthTokenError(self, error) - - class AzureADV2OIDOAuth2(AzureADOIDOAuth2): name = "azuread-v2-OID-oauth2" OPENID_CONFIGURATION_URL = "{base_url}/v2.0/.well-known/openid-configuration{appid}" From 2d466eaafa7b1664009a1683617efecdac74f1af Mon Sep 17 00:00:00 2001 From: stealthycole Date: Fri, 13 Jun 2025 08:48:16 -0700 Subject: [PATCH 5/6] Fixing filename, removing extra imports, fixing v2 class --- .../backends/{azuread_oid => azuread_oid.py} | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) rename social_core/backends/{azuread_oid => azuread_oid.py} (86%) diff --git a/social_core/backends/azuread_oid b/social_core/backends/azuread_oid.py similarity index 86% rename from social_core/backends/azuread_oid rename to social_core/backends/azuread_oid.py index 0bcb9c30e..9f3b1cadf 100644 --- a/social_core/backends/azuread_oid +++ b/social_core/backends/azuread_oid.py @@ -1,13 +1,4 @@ -import base64 - -from cryptography.hazmat.backends import default_backend -from cryptography.x509 import load_der_x509_certificate -from jwt import DecodeError, ExpiredSignatureError, get_unverified_header -from jwt import decode as jwt_decode - -from social_core.exceptions import AuthTokenError - -from .azuread import AzureADOAuth2 +from .azuread_tenant import AzureADTenantOAuth2 """ Copyright (c) 2015 Microsoft Open Technologies, Inc. @@ -60,8 +51,8 @@ class AzureADV2OIDOAuth2(AzureADOIDOAuth2): DEFAULT_SCOPE = ["openid", "profile", "offline_access"] def get_user_id(self, details, response): - """Use upn as unique id""" - return response.get("preferred_username") + """Use oid as unique id""" + return response.get("oid") def get_user_details(self, response): """Return user details from Azure AD account""" From 782eb7ed80b17ad60c00c10a1f4010afe504c897 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:09:54 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- social_core/backends/azuread_oid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/social_core/backends/azuread_oid.py b/social_core/backends/azuread_oid.py index 9f3b1cadf..cdef3adc9 100644 --- a/social_core/backends/azuread_oid.py +++ b/social_core/backends/azuread_oid.py @@ -42,6 +42,7 @@ def get_user_id(self, details, response): """Use account oid as unique id.""" return response.get("oid") + class AzureADV2OIDOAuth2(AzureADOIDOAuth2): name = "azuread-v2-OID-oauth2" OPENID_CONFIGURATION_URL = "{base_url}/v2.0/.well-known/openid-configuration{appid}"