This commit is contained in:
Iliyan Angelov
2025-09-14 23:24:25 +03:00
commit c67067a2a4
71311 changed files with 6800714 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
import importlib
from collections import OrderedDict
from django.apps import apps
from django.conf import settings
from allauth.utils import import_attribute
class ProviderRegistry(object):
def __init__(self):
self.provider_map = OrderedDict()
self.loaded = False
def get_class_list(self):
self.load()
return list(self.provider_map.values())
def register(self, cls):
self.provider_map[cls.id] = cls
def get_class(self, id):
return self.provider_map.get(id)
def as_choices(self):
self.load()
for provider_cls in self.provider_map.values():
yield (provider_cls.id, provider_cls.name)
def load(self):
# TODO: Providers register with the provider registry when
# loaded. Here, we build the URLs for all registered providers. So, we
# really need to be sure all providers did register, which is why we're
# forcefully importing the `provider` modules here. The overall
# mechanism is way to magical and depends on the import order et al, so
# all of this really needs to be revisited.
if not self.loaded:
for app_config in apps.get_app_configs():
try:
provider_module = importlib.import_module(
app_config.name + ".provider"
)
except ImportError:
pass
else:
provider_settings = getattr(settings, "SOCIALACCOUNT_PROVIDERS", {})
for cls in getattr(provider_module, "provider_classes", []):
provider_class = provider_settings.get(cls.id, {}).get(
"provider_class"
)
if provider_class:
cls = import_attribute(provider_class)
self.register(cls)
self.loaded = True
registry = ProviderRegistry()

View File

@@ -0,0 +1,39 @@
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class AgaveAccount(ProviderAccount):
def get_profile_url(self):
return self.account.extra_data.get("web_url", "dflt")
def get_avatar_url(self):
return self.account.extra_data.get("avatar_url", "dflt")
def to_str(self):
dflt = super(AgaveAccount, self).to_str()
return self.account.extra_data.get("name", dflt)
class AgaveProvider(OAuth2Provider):
id = "agave"
name = "Agave"
account_class = AgaveAccount
def extract_uid(self, data):
return str(data.get("create_time"))
def extract_common_fields(self, data):
return dict(
email=data.get("email"),
username=data.get("username", ""),
name=(
(data.get("first_name", "") + " " + data.get("last_name", "")).strip()
),
)
def get_default_scope(self):
scope = ["PRODUCTION"]
return scope
provider_classes = [AgaveProvider]

View File

@@ -0,0 +1,31 @@
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from .provider import AgaveProvider
class AgaveTests(OAuth2TestsMixin, TestCase):
provider_id = AgaveProvider.id
def get_mocked_response(self):
return MockedResponse(
200,
"""
{
"status": "success",
"message": "User details retrieved successfully.",
"version": "2.0.0-SNAPSHOT-rc3fad",
"result": {
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"email": "jon@doe.edu",
"phone": "",
"mobile_phone": "",
"status": "Active",
"create_time": "20180322043812Z",
"username": "jdoe"
}
}
""",
)

View File

@@ -0,0 +1,5 @@
from allauth.socialaccount.providers.agave.provider import AgaveProvider
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
urlpatterns = default_urlpatterns(AgaveProvider)

View File

@@ -0,0 +1,39 @@
import requests
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.agave.provider import AgaveProvider
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
class AgaveAdapter(OAuth2Adapter):
provider_id = AgaveProvider.id
settings = app_settings.PROVIDERS.get(provider_id, {})
provider_base_url = settings.get("API_URL", "https://public.agaveapi.co")
access_token_url = "{0}/token".format(provider_base_url)
authorize_url = "{0}/authorize".format(provider_base_url)
profile_url = "{0}/profiles/v2/me".format(provider_base_url)
def complete_login(self, request, app, token, response):
extra_data = requests.get(
self.profile_url,
params={"access_token": token.token},
headers={
"Authorization": "Bearer " + token.token,
},
)
user_profile = (
extra_data.json()["result"] if "result" in extra_data.json() else {}
)
return self.get_provider().sociallogin_from_response(request, user_profile)
oauth2_login = OAuth2LoginView.adapter_view(AgaveAdapter)
oauth2_callback = OAuth2CallbackView.adapter_view(AgaveAdapter)

View File

@@ -0,0 +1,33 @@
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class AmazonAccount(ProviderAccount):
def to_str(self):
return self.account.extra_data.get("name", super(AmazonAccount, self).to_str())
class AmazonProvider(OAuth2Provider):
id = "amazon"
name = "Amazon"
account_class = AmazonAccount
def get_default_scope(self):
return ["profile"]
def extract_uid(self, data):
return str(data["user_id"])
def extract_common_fields(self, data):
# Hackish way of splitting the fullname.
# Assumes no middlenames.
name = data.get("name", "")
first_name, last_name = name, ""
if name and " " in name:
first_name, last_name = name.split(" ", 1)
return dict(
email=data.get("email", ""), last_name=last_name, first_name=first_name
)
provider_classes = [AmazonProvider]

View File

@@ -0,0 +1,21 @@
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from .provider import AmazonProvider
class AmazonTests(OAuth2TestsMixin, TestCase):
provider_id = AmazonProvider.id
def get_mocked_response(self):
return MockedResponse(
200,
"""
{
"Profile":{
"CustomerId":"amzn1.account.K2LI23KL2LK2",
"Name":"John Doe",
"PrimaryEmail":"johndoe@example.com"
}
}""",
)

View File

@@ -0,0 +1,6 @@
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import AmazonProvider
urlpatterns = default_urlpatterns(AmazonProvider)

View File

@@ -0,0 +1,32 @@
import requests
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
from .provider import AmazonProvider
class AmazonOAuth2Adapter(OAuth2Adapter):
provider_id = AmazonProvider.id
access_token_url = "https://api.amazon.com/auth/o2/token"
authorize_url = "http://www.amazon.com/ap/oa"
profile_url = "https://api.amazon.com/user/profile"
supports_state = False
def complete_login(self, request, app, token, **kwargs):
response = requests.get(self.profile_url, params={"access_token": token})
extra_data = response.json()
if "Profile" in extra_data:
extra_data = {
"user_id": extra_data["Profile"]["CustomerId"],
"name": extra_data["Profile"]["Name"],
"email": extra_data["Profile"]["PrimaryEmail"],
}
return self.get_provider().sociallogin_from_response(request, extra_data)
oauth2_login = OAuth2LoginView.adapter_view(AmazonOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(AmazonOAuth2Adapter)

View File

@@ -0,0 +1,78 @@
from allauth.account.models import EmailAddress
from allauth.socialaccount.providers.amazon_cognito.utils import (
convert_to_python_bool_if_value_is_json_string_bool,
)
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class AmazonCognitoAccount(ProviderAccount):
def to_str(self):
dflt = super(AmazonCognitoAccount, self).to_str()
return self.account.extra_data.get("username", dflt)
def get_avatar_url(self):
return self.account.extra_data.get("picture")
def get_profile_url(self):
return self.account.extra_data.get("profile")
class AmazonCognitoProvider(OAuth2Provider):
id = "amazon_cognito"
name = "Amazon Cognito"
account_class = AmazonCognitoAccount
def extract_uid(self, data):
return str(data["sub"])
def extract_common_fields(self, data):
return {
"email": data.get("email"),
"first_name": data.get("given_name"),
"last_name": data.get("family_name"),
}
def get_default_scope(self):
return ["openid", "profile", "email"]
def extract_email_addresses(self, data):
email = data.get("email")
verified = convert_to_python_bool_if_value_is_json_string_bool(
data.get("email_verified", False)
)
return (
[EmailAddress(email=email, verified=verified, primary=True)]
if email
else []
)
def extract_extra_data(self, data):
return {
"address": data.get("address"),
"birthdate": data.get("birthdate"),
"gender": data.get("gender"),
"locale": data.get("locale"),
"middlename": data.get("middlename"),
"nickname": data.get("nickname"),
"phone_number": data.get("phone_number"),
"phone_number_verified": convert_to_python_bool_if_value_is_json_string_bool(
data.get("phone_number_verified")
),
"picture": data.get("picture"),
"preferred_username": data.get("preferred_username"),
"profile": data.get("profile"),
"website": data.get("website"),
"zoneinfo": data.get("zoneinfo"),
}
@classmethod
def get_slug(cls):
# IMPORTANT: Amazon Cognito does not support `_` characters
# as part of their redirect URI.
return super(AmazonCognitoProvider, cls).get_slug().replace("_", "-")
provider_classes = [AmazonCognitoProvider]

View File

@@ -0,0 +1,69 @@
import json
from django.test import override_settings
from allauth.account.models import EmailAddress
from allauth.socialaccount.models import SocialAccount
from allauth.socialaccount.providers.amazon_cognito.provider import (
AmazonCognitoProvider,
)
from allauth.socialaccount.providers.amazon_cognito.views import (
AmazonCognitoOAuth2Adapter,
)
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
def _get_mocked_claims():
return {
"sub": "4993b410-8a1b-4c36-b843-a9c1a697e6b7",
"given_name": "John",
"family_name": "Doe",
"email": "jdoe@example.com",
"username": "johndoe",
}
@override_settings(
SOCIALACCOUNT_PROVIDERS={
"amazon_cognito": {"DOMAIN": "https://domain.auth.us-east-1.amazoncognito.com"}
}
)
class AmazonCognitoTestCase(OAuth2TestsMixin, TestCase):
provider_id = AmazonCognitoProvider.id
def get_mocked_response(self):
mocked_payload = json.dumps(_get_mocked_claims())
return MockedResponse(status_code=200, content=mocked_payload)
@override_settings(SOCIALACCOUNT_PROVIDERS={"amazon_cognito": {}})
def test_oauth2_adapter_raises_if_domain_settings_is_missing(
self,
):
mocked_response = self.get_mocked_response()
with self.assertRaises(
ValueError,
msg=AmazonCognitoOAuth2Adapter.DOMAIN_KEY_MISSING_ERROR,
):
self.login(mocked_response)
def test_saves_email_as_verified_if_email_is_verified_in_cognito(
self,
):
mocked_claims = _get_mocked_claims()
mocked_claims["email_verified"] = True
mocked_payload = json.dumps(mocked_claims)
mocked_response = MockedResponse(status_code=200, content=mocked_payload)
self.login(mocked_response)
user_id = SocialAccount.objects.get(uid=mocked_claims["sub"]).user_id
email_address = EmailAddress.objects.get(user_id=user_id)
self.assertEqual(email_address.email, mocked_claims["email"])
self.assertTrue(email_address.verified)
def test_provider_slug_replaces_underscores_with_hyphens(self):
self.assertTrue("_" not in self.provider.get_slug())

View File

@@ -0,0 +1,7 @@
from allauth.socialaccount.providers.amazon_cognito.provider import (
AmazonCognitoProvider,
)
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
urlpatterns = default_urlpatterns(AmazonCognitoProvider)

View File

@@ -0,0 +1,7 @@
def convert_to_python_bool_if_value_is_json_string_bool(s):
if s == "true":
return True
elif s == "false":
return False
return s

View File

@@ -0,0 +1,57 @@
import requests
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.amazon_cognito.provider import (
AmazonCognitoProvider,
)
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
class AmazonCognitoOAuth2Adapter(OAuth2Adapter):
provider_id = AmazonCognitoProvider.id
DOMAIN_KEY_MISSING_ERROR = (
'"DOMAIN" key is missing in Amazon Cognito configuration.'
)
@property
def settings(self):
return app_settings.PROVIDERS.get(self.provider_id, {})
@property
def domain(self):
domain = self.settings.get("DOMAIN")
if domain is None:
raise ValueError(self.DOMAIN_KEY_MISSING_ERROR)
return domain
@property
def access_token_url(self):
return "{}/oauth2/token".format(self.domain)
@property
def authorize_url(self):
return "{}/oauth2/authorize".format(self.domain)
@property
def profile_url(self):
return "{}/oauth2/userInfo".format(self.domain)
def complete_login(self, request, app, access_token, **kwargs):
headers = {
"Authorization": "Bearer {}".format(access_token),
}
extra_data = requests.get(self.profile_url, headers=headers)
extra_data.raise_for_status()
return self.get_provider().sociallogin_from_response(request, extra_data.json())
oauth2_login = OAuth2LoginView.adapter_view(AmazonCognitoOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(AmazonCognitoOAuth2Adapter)

View File

@@ -0,0 +1,33 @@
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class AngelListAccount(ProviderAccount):
def get_profile_url(self):
return self.account.extra_data.get("angellist_url")
def get_avatar_url(self):
return self.account.extra_data.get("image")
def to_str(self):
dflt = super(AngelListAccount, self).to_str()
return self.account.extra_data.get("name", dflt)
class AngelListProvider(OAuth2Provider):
id = "angellist"
name = "AngelList"
account_class = AngelListAccount
def extract_uid(self, data):
return str(data["id"])
def extract_common_fields(self, data):
return dict(
email=data.get("email"),
username=data.get("angellist_url").split("/")[-1],
name=data.get("name"),
)
provider_classes = [AngelListProvider]

View File

@@ -0,0 +1,25 @@
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from .provider import AngelListProvider
class AngelListTests(OAuth2TestsMixin, TestCase):
provider_id = AngelListProvider.id
def get_mocked_response(self):
return MockedResponse(
200,
"""
{"name":"pennersr","id":424732,"bio":"","follower_count":0,
"angellist_url":"https://angel.co/dsxtst",
"image":"https://angel.co/images/shared/nopic.png",
"email":"raymond.penners@example.com","blog_url":null,
"online_bio_url":null,"twitter_url":"https://twitter.com/dsxtst",
"facebook_url":null,"linkedin_url":null,"aboutme_url":null,
"github_url":null,"dribbble_url":null,"behance_url":null,
"what_ive_built":null,"locations":[],"roles":[],"skills":[],
"investor":false,"scopes":["message","talent","dealflow","comment",
"email"]}
""",
)

View File

@@ -0,0 +1,6 @@
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import AngelListProvider
urlpatterns = default_urlpatterns(AngelListProvider)

View File

@@ -0,0 +1,26 @@
import requests
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
from .provider import AngelListProvider
class AngelListOAuth2Adapter(OAuth2Adapter):
provider_id = AngelListProvider.id
access_token_url = "https://angel.co/api/oauth/token/"
authorize_url = "https://angel.co/api/oauth/authorize/"
profile_url = "https://api.angel.co/1/me/"
supports_state = False
def complete_login(self, request, app, token, **kwargs):
resp = requests.get(self.profile_url, params={"access_token": token.token})
extra_data = resp.json()
return self.get_provider().sociallogin_from_response(request, extra_data)
oauth2_login = OAuth2LoginView.adapter_view(AngelListOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(AngelListOAuth2Adapter)

View File

@@ -0,0 +1,8 @@
from allauth.socialaccount.sessions import LoginSession
APPLE_SESSION_COOKIE_NAME = "apple-login-session"
def get_apple_session(request):
return LoginSession(request, "apple_login_session", APPLE_SESSION_COOKIE_NAME)

View File

@@ -0,0 +1,98 @@
import requests
from datetime import datetime, timedelta
from urllib.parse import parse_qsl, quote, urlencode
from django.core.exceptions import ImproperlyConfigured
import jwt
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.providers.oauth2.client import (
OAuth2Client,
OAuth2Error,
)
def jwt_encode(*args, **kwargs):
resp = jwt.encode(*args, **kwargs)
if isinstance(resp, bytes):
# For PyJWT <2
resp = resp.decode("utf-8")
return resp
class Scope(object):
EMAIL = "email"
NAME = "name"
class AppleOAuth2Client(OAuth2Client):
"""
Custom client because `Sign In With Apple`:
* requires `response_mode` field in redirect_url
* requires special `client_secret` as JWT
"""
def generate_client_secret(self):
"""Create a JWT signed with an apple provided private key"""
now = datetime.utcnow()
app = get_adapter(self.request).get_app(self.request, "apple")
if not app.key:
raise ImproperlyConfigured("Apple 'key' missing")
if not app.certificate_key:
raise ImproperlyConfigured("Apple 'certificate_key' missing")
claims = {
"iss": app.key,
"aud": "https://appleid.apple.com",
"sub": self.get_client_id(),
"iat": now,
"exp": now + timedelta(hours=1),
}
headers = {"kid": self.consumer_secret, "alg": "ES256"}
client_secret = jwt_encode(
payload=claims, key=app.certificate_key, algorithm="ES256", headers=headers
)
return client_secret
def get_client_id(self):
"""We support multiple client_ids, but use the first one for api calls"""
return self.consumer_key.split(",")[0]
def get_access_token(self, code, pkce_code_verifier=None):
url = self.access_token_url
client_secret = self.generate_client_secret()
data = {
"client_id": self.get_client_id(),
"code": code,
"grant_type": "authorization_code",
"redirect_uri": self.callback_url,
"client_secret": client_secret,
}
if pkce_code_verifier:
data["code_verifier"] = pkce_code_verifier
self._strip_empty_keys(data)
resp = requests.request(
self.access_token_method, url, data=data, headers=self.headers
)
access_token = None
if resp.status_code in [200, 201]:
try:
access_token = resp.json()
except ValueError:
access_token = dict(parse_qsl(resp.text))
if not access_token or "access_token" not in access_token:
raise OAuth2Error("Error retrieving access token: %s" % resp.content)
return access_token
def get_redirect_url(self, authorization_url, extra_params):
params = {
"client_id": self.get_client_id(),
"redirect_uri": self.callback_url,
"response_mode": "form_post",
"scope": self.scope,
"response_type": "code id_token",
}
if self.state:
params["state"] = self.state
params.update(extra_params)
return "%s?%s" % (authorization_url, urlencode(params, quote_via=quote))

View File

@@ -0,0 +1,49 @@
from allauth.account.models import EmailAddress
from allauth.socialaccount.app_settings import QUERY_EMAIL
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class AppleProvider(OAuth2Provider):
id = "apple"
name = "Apple"
account_class = ProviderAccount
def extract_uid(self, data):
return str(data["sub"])
def extract_common_fields(self, data):
fields = {"email": data.get("email")}
# If the name was provided
name = data.get("name")
if name:
fields["first_name"] = name.get("firstName", "")
fields["last_name"] = name.get("lastName", "")
return fields
def extract_email_addresses(self, data):
ret = []
email = data.get("email")
verified = data.get("email_verified")
if isinstance(verified, str):
verified = verified.lower() == "true"
if email:
ret.append(
EmailAddress(
email=email,
verified=verified,
primary=True,
)
)
return ret
def get_default_scope(self):
scopes = ["name"]
if QUERY_EMAIL:
scopes.append("email")
return scopes
provider_classes = [AppleProvider]

View File

@@ -0,0 +1,253 @@
import json
from datetime import datetime, timedelta
from importlib import import_module
from urllib.parse import parse_qs, urlparse
from django.conf import settings
from django.test.utils import override_settings
from django.urls import reverse
from django.utils.http import urlencode
import jwt
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase, mocked_response
from .apple_session import APPLE_SESSION_COOKIE_NAME
from .client import jwt_encode
from .provider import AppleProvider
# Generated on https://mkjwk.org/, used to sign and verify the apple id_token
TESTING_JWT_KEYSET = {
"p": (
"4ADzS5jKx_kdQihyOocVS0Qwwo7m0f7Ow56EadySJ-cmnwoHHF3AxgRaq-h-KwybSphv"
"dc-X7NbS79-b9dumHKyt1MeVLAsDZD1a-uQCEneY1g9LsQkscNr7OggcpvMg5UUFwv6A"
"kavu8cB0iyhNdha5_AWX27K5lNebvpaXEJ8"
),
"kty": "RSA",
"q": (
"yy5UvMjrvZyO1Os_nxXIugCa3NyWOkC8oMppPvr1Bl5AnF_xwXN2n9ozPd9Nb3Q3n-om"
"NgLayyUxhwIjWDlI67Vbx-ESuff8ZEBKuTK0Gdmr4C_QU_j0gvvNMNJweSPxDdRmIUgO"
"njTVNWmdqFTZs43jXAT4J519rgveNLAkGNE"
),
"d": (
"riPuGIDde88WS03CVbo_mZ9toFWPyTxvuz8VInJ9S1ZxULo-hQWDBohWGYwvg8cgfXck"
"cqWt5OBqNvPYdLgwb84uVi2JeEHmhcQSc_x0zfRTau5HVE2KdR-gWxQjPWoaBHeDVqwo"
"PKaU2XYxa-gYDXcuSJWHz3BX13oInDEFCXr6VwiLiwLBFsb63EEHwyWXJbTpoar7AARW"
"kz76qtngDkk4t9gk_Q0L1y1qf1GeWiAL7xWb-bdptma4-1ui-R2219-1ONEZ41v_jsIS"
"_z8ooXmVCbUsHV4Z1UDpRvpORVE3u57WK3qXUdAtZsXjaIwkdItbDmL1jFUgefwfO91Y"
"YQ"
),
"e": "AQAB",
"use": "sig",
"kid": "testkey",
"qi": (
"R0Hu4YmpHzw3SKWGYuAcAo6B97-JlN2fXiTjZ2g8eHGQX7LSoKEu0Hmu5hcBZYSgOuor"
"IPsPUu3mNtx3pjLMOaJRk34VwcYu7h23ogEKGcPUt1c4tTotFDdw8WFptDOw4ow31Tml"
"BPExLqzzGjJeQSNULB1bExuuhYMWx6wBXo8"
),
"dp": (
"WBaHlnbjZ3hDVTzqjrGIYizSr-_aPUJitPKlR6wBncd8nJYo7bLAmB4mOewXkX5HozIG"
"wuF78RsZoFLi1fAmhqgxQ7eopcU-9DBcksUPO4vkgmlJbrkYzNiQauW9vrllekOGXIQQ"
"szhVoqP4MLEMpR-Sy9S3PyItcKbJDE3T4ik"
),
"alg": "RS256",
"dq": (
"Ar5kbIw2CsBzeVKX8FkF9eUOMk9URAMdyPoSw8P1zRk2vCXbiOY7Qttad8ptLEUgfytV"
"SsNtGvMsoQsZWRak8nHnhGJ4s0QzB1OK7sdNgU_cL1HV-VxSSPaHhdJBrJEcrzggDPEB"
"KYfDHU6Iz34d1nvjBxoWE8rfqJsGbCW4xxE"
),
"n": (
"sclLPioUv4VOcOZWAKoRhcvwIH2jOhoHhSI_Cj5c5zSp7qaK8jCU6T7-GObsgrhpty-k"
"26ZuqRdgu9d-62WO8OBGt1e0wxbTh128-nTTrOESHUlV_K1wpJmXOxNpJiybcgzZNbAm"
"ACmsHfxZvN9bt7gKPXxf3-_zFAf12PbYMrOionAJ1N_4HxL7fz3xkr5C87Av06QNilIC"
"-mA-4n9Eqw_R2DYNpE3RYMdWtwKqBwJC8qs3677RpG9vcc-yZ_97pEiytd2FBJ8uoTwH"
"d3DHJB8UVgBSh1kMUpSdoM7HxVzKx732nx6Kusln79LrsfOzrXF4enkfKJYI40-uwT95"
"zw"
),
}
# Mocked version of the test data from https://appleid.apple.com/auth/keys
KEY_SERVER_RESP_JSON = json.dumps(
{
"keys": [
{
"kty": TESTING_JWT_KEYSET["kty"],
"kid": TESTING_JWT_KEYSET["kid"],
"use": TESTING_JWT_KEYSET["use"],
"alg": TESTING_JWT_KEYSET["alg"],
"n": TESTING_JWT_KEYSET["n"],
"e": TESTING_JWT_KEYSET["e"],
}
]
}
)
def sign_id_token(payload):
"""
Sign a payload as apple normally would for the id_token.
"""
signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(TESTING_JWT_KEYSET))
return jwt_encode(
payload,
signing_key,
algorithm="RS256",
headers={"kid": TESTING_JWT_KEYSET["kid"]},
)
@override_settings(
SOCIALACCOUNT_STORE_TOKENS=False,
SOCIALACCOUNT_PROVIDERS={
"apple": {
"APP": {
"client_id": "app123id",
"key": "apple",
"secret": "dummy",
"certificate_key": """-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg2+Eybl8ojH4wB30C
3/iDkpsrxuPfs3DZ+3nHNghBOpmhRANCAAQSpo1eQ+EpNgQQyQVs/F27dkq3gvAI
28m95JEk26v64YAea5NTH56mru30RDqTKPgRVi5qRu3XGyqy3mdb8gMy
-----END PRIVATE KEY-----
""",
}
}
},
)
class AppleTests(OAuth2TestsMixin, TestCase):
provider_id = AppleProvider.id
def get_apple_id_token_payload(self):
now = datetime.utcnow()
return {
"iss": "https://appleid.apple.com",
"aud": "app123id", # Matches `setup_app`
"exp": now + timedelta(hours=1),
"iat": now,
"sub": "000313.c9720f41e9434e18987a.1218",
"at_hash": "CkaUPjk4MJinaAq6Z0tGUA",
"email": "test@privaterelay.appleid.com",
"email_verified": "true",
"is_private_email": "true",
"auth_time": 1234345345, # not converted automatically by pyjwt
}
def get_login_response_json(self, with_refresh_token=True):
"""
`with_refresh_token` is not optional for apple, so it's ignored.
"""
id_token = sign_id_token(self.get_apple_id_token_payload())
return json.dumps(
{
"access_token": "testac", # Matches OAuth2TestsMixin value
"expires_in": 3600,
"id_token": id_token,
"refresh_token": "testrt", # Matches OAuth2TestsMixin value
"token_type": "Bearer",
}
)
def get_mocked_response(self):
"""
Apple is unusual in that the `id_token` contains all the user info
so no profile info request is made. However, it does need the
public key verification, so this mocked response is the public
key request in order to verify the authenticity of the id_token.
"""
return MockedResponse(
200, KEY_SERVER_RESP_JSON, {"content-type": "application/json"}
)
def get_complete_parameters(self, auth_request_params):
"""
Add apple specific response parameters which they include in the
form_post response.
https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms
"""
params = super().get_complete_parameters(auth_request_params)
params.update(
{
"id_token": sign_id_token(self.get_apple_id_token_payload()),
"user": json.dumps(
{
"email": "private@appleid.apple.com",
"name": {
"firstName": "A",
"lastName": "B",
},
}
),
}
)
return params
def login(self, resp_mock, process="login", with_refresh_token=True):
resp = self.client.post(
reverse(self.provider.id + "_login")
+ "?"
+ urlencode(dict(process=process))
)
p = urlparse(resp["location"])
q = parse_qs(p.query)
complete_url = reverse(self.provider.id + "_callback")
self.assertGreater(q["redirect_uri"][0].find(complete_url), 0)
response_json = self.get_login_response_json(
with_refresh_token=with_refresh_token
)
with mocked_response(
MockedResponse(200, response_json, {"content-type": "application/json"}),
resp_mock,
):
resp = self.client.post(
complete_url,
data=self.get_complete_parameters(q),
)
assert reverse("apple_finish_callback") in resp.url
# Follow the redirect
resp = self.client.get(resp.url)
return resp
def test_authentication_error(self):
"""Override base test because apple posts errors"""
resp = self.client.post(
reverse(self.provider.id + "_callback"),
data={"error": "misc", "state": "testingstate123"},
)
assert reverse("apple_finish_callback") in resp.url
# Follow the redirect
resp = self.client.get(resp.url)
self.assertTemplateUsed(
resp,
"socialaccount/authentication_error.%s"
% getattr(settings, "ACCOUNT_TEMPLATE_EXTENSION", "html"),
)
def test_apple_finish(self):
resp = self.login(self.get_mocked_response())
# Check request generating the response
finish_url = reverse("apple_finish_callback")
self.assertEqual(resp.request["PATH_INFO"], finish_url)
self.assertTrue("state" in resp.request["QUERY_STRING"])
self.assertTrue("code" in resp.request["QUERY_STRING"])
# Check have cookie containing apple session
self.assertTrue(APPLE_SESSION_COOKIE_NAME in self.client.cookies)
# Session should have been cleared
apple_session_cookie = self.client.cookies.get(APPLE_SESSION_COOKIE_NAME)
engine = import_module(settings.SESSION_ENGINE)
SessionStore = engine.SessionStore
apple_login_session = SessionStore(apple_session_cookie.value)
self.assertEqual(len(apple_login_session.keys()), 0)
# Check cookie path was correctly set
self.assertEqual(apple_session_cookie.get("path"), finish_url)

View File

@@ -0,0 +1,16 @@
from django.urls import path
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import AppleProvider
from .views import oauth2_finish_login
urlpatterns = default_urlpatterns(AppleProvider)
urlpatterns += [
path(
AppleProvider.get_slug() + "/login/callback/finish/",
oauth2_finish_login,
name="apple_finish_callback",
),
]

View File

@@ -0,0 +1,183 @@
import json
import requests
from datetime import timedelta
from django.http import HttpResponseNotAllowed, HttpResponseRedirect
from django.urls import reverse
from django.utils import timezone
from django.utils.http import urlencode
from django.views.decorators.csrf import csrf_exempt
import jwt
from allauth.socialaccount.adapter import get_adapter
from allauth.socialaccount.models import SocialToken
from allauth.socialaccount.providers.oauth2.client import OAuth2Error
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
from allauth.utils import build_absolute_uri, get_request_param
from .apple_session import get_apple_session
from .client import AppleOAuth2Client
from .provider import AppleProvider
class AppleOAuth2Adapter(OAuth2Adapter):
client_class = AppleOAuth2Client
provider_id = AppleProvider.id
access_token_url = "https://appleid.apple.com/auth/token"
authorize_url = "https://appleid.apple.com/auth/authorize"
public_key_url = "https://appleid.apple.com/auth/keys"
def _get_apple_public_key(self, kid):
response = requests.get(self.public_key_url)
response.raise_for_status()
try:
data = response.json()
except json.JSONDecodeError as e:
raise OAuth2Error("Error retrieving apple public key.") from e
for d in data["keys"]:
if d["kid"] == kid:
return d
def get_public_key(self, id_token):
"""
Get the public key which matches the `kid` in the id_token header.
"""
kid = jwt.get_unverified_header(id_token)["kid"]
apple_public_key = self._get_apple_public_key(kid=kid)
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(apple_public_key))
return public_key
def get_client_id(self, provider):
app = get_adapter().get_app(request=None, provider=self.provider_id)
return [aud.strip() for aud in app.client_id.split(",")]
def get_verified_identity_data(self, id_token):
provider = self.get_provider()
allowed_auds = self.get_client_id(provider)
try:
public_key = self.get_public_key(id_token)
identity_data = jwt.decode(
id_token,
public_key,
algorithms=["RS256"],
audience=allowed_auds,
issuer="https://appleid.apple.com",
)
return identity_data
except jwt.PyJWTError as e:
raise OAuth2Error("Invalid id_token") from e
def parse_token(self, data):
token = SocialToken(
token=data["access_token"],
)
token.token_secret = data.get("refresh_token", "")
expires_in = data.get(self.expires_in_key)
if expires_in:
token.expires_at = timezone.now() + timedelta(seconds=int(expires_in))
# `user_data` is a big flat dictionary with the parsed JWT claims
# access_tokens, and user info from the apple post.
identity_data = self.get_verified_identity_data(data["id_token"])
token.user_data = {**data, **identity_data}
return token
def complete_login(self, request, app, token, **kwargs):
extra_data = token.user_data
login = self.get_provider().sociallogin_from_response(
request=request, response=extra_data
)
login.state["id_token"] = token.user_data
# We can safely remove the apple login session now
# Note: The cookie will remain, but it's set to delete on browser close
get_apple_session(request).delete()
return login
def get_user_scope_data(self, request):
user_scope_data = request.apple_login_session.get("user", "")
try:
return json.loads(user_scope_data)
except json.JSONDecodeError:
# We do not care much about user scope data as it maybe blank
# so return blank dictionary instead
return {}
def get_access_token_data(self, request, app, client):
"""We need to gather the info from the apple specific login"""
apple_session = get_apple_session(request)
# Exchange `code`
code = get_request_param(request, "code")
pkce_code_verifier = request.session.pop("pkce_code_verifier", None)
access_token_data = client.get_access_token(
code, pkce_code_verifier=pkce_code_verifier
)
id_token = access_token_data.get("id_token", None)
# In case of missing id_token in access_token_data
if id_token is None:
id_token = apple_session.store.get("id_token")
return {
**access_token_data,
**self.get_user_scope_data(request),
"id_token": id_token,
}
@csrf_exempt
def apple_post_callback(request, finish_endpoint_name="apple_finish_callback"):
"""
Apple uses a `form_post` response type, which due to
CORS/Samesite-cookie rules means this request cannot access
the request since the session cookie is unavailable.
We work around this by storing the apple response in a
separate, temporary session and redirecting to a more normal
oauth flow.
args:
finish_endpoint_name (str): The name of a defined URL, which can be
overridden in your url configuration if you have more than one
callback endpoint.
"""
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
apple_session = get_apple_session(request)
# Add regular OAuth2 params to the URL - reduces the overrides required
keys_to_put_in_url = ["code", "state", "error"]
url_params = {}
for key in keys_to_put_in_url:
value = get_request_param(request, key, "")
if value:
url_params[key] = value
# Add other params to the apple_login_session
keys_to_save_to_session = ["user", "id_token"]
for key in keys_to_save_to_session:
apple_session.store[key] = get_request_param(request, key, "")
url = build_absolute_uri(request, reverse(finish_endpoint_name))
response = HttpResponseRedirect(
"{url}?{query}".format(url=url, query=urlencode(url_params))
)
apple_session.save(response)
return response
oauth2_login = OAuth2LoginView.adapter_view(AppleOAuth2Adapter)
oauth2_callback = apple_post_callback
oauth2_finish_login = OAuth2CallbackView.adapter_view(AppleOAuth2Adapter)

View File

@@ -0,0 +1,21 @@
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class AsanaAccount(ProviderAccount):
pass
class AsanaProvider(OAuth2Provider):
id = "asana"
name = "Asana"
account_class = AsanaAccount
def extract_uid(self, data):
return str(data["id"])
def extract_common_fields(self, data):
return dict(email=data.get("email"), name=data.get("name"))
provider_classes = [AsanaProvider]

View File

@@ -0,0 +1,17 @@
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from .provider import AsanaProvider
class AsanaTests(OAuth2TestsMixin, TestCase):
provider_id = AsanaProvider.id
def get_mocked_response(self):
return MockedResponse(
200,
"""
{"data": {"photo": null, "workspaces": [{"id": 31337, "name": "example.com"},
{"id": 3133777, "name": "Personal Projects"}], "email": "test@example.com",
"name": "Test Name", "id": 43748387}}""",
)

View File

@@ -0,0 +1,6 @@
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import AsanaProvider
urlpatterns = default_urlpatterns(AsanaProvider)

View File

@@ -0,0 +1,25 @@
import requests
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
from .provider import AsanaProvider
class AsanaOAuth2Adapter(OAuth2Adapter):
provider_id = AsanaProvider.id
access_token_url = "https://app.asana.com/-/oauth_token"
authorize_url = "https://app.asana.com/-/oauth_authorize"
profile_url = "https://app.asana.com/api/1.0/users/me"
def complete_login(self, request, app, token, **kwargs):
resp = requests.get(self.profile_url, params={"access_token": token.token})
extra_data = resp.json()["data"]
return self.get_provider().sociallogin_from_response(request, extra_data)
oauth2_login = OAuth2LoginView.adapter_view(AsanaOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(AsanaOAuth2Adapter)

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class Auth0Account(ProviderAccount):
def get_avatar_url(self):
return self.account.extra_data.get("picture")
def to_str(self):
dflt = super(Auth0Account, self).to_str()
return self.account.extra_data.get("name", dflt)
class Auth0Provider(OAuth2Provider):
id = "auth0"
name = "Auth0"
account_class = Auth0Account
def get_default_scope(self):
return ["openid", "profile", "email"]
def extract_uid(self, data):
return str(data["sub"])
def extract_common_fields(self, data):
return dict(
email=data.get("email"),
username=data.get("username"),
name=data.get("name"),
)
provider_classes = [Auth0Provider]

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from allauth.socialaccount.providers.auth0.provider import Auth0Provider
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
class Auth0Tests(OAuth2TestsMixin, TestCase):
provider_id = Auth0Provider.id
def get_mocked_response(self):
return MockedResponse(
200,
"""
{
"picture": "https://secure.gravatar.com/avatar/123",
"email": "mr.bob@your.Auth0.server.example.com",
"id": 2,
"sub": 2,
"identities": [],
"name": "Mr Bob"
}
""",
)

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from allauth.socialaccount.providers.auth0.provider import Auth0Provider
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
urlpatterns = default_urlpatterns(Auth0Provider)

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
import requests
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.auth0.provider import Auth0Provider
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
class Auth0OAuth2Adapter(OAuth2Adapter):
provider_id = Auth0Provider.id
supports_state = True
settings = app_settings.PROVIDERS.get(provider_id, {})
provider_base_url = settings.get("AUTH0_URL")
access_token_url = "{0}/oauth/token".format(provider_base_url)
authorize_url = "{0}/authorize".format(provider_base_url)
profile_url = "{0}/userinfo".format(provider_base_url)
def complete_login(self, request, app, token, response):
extra_data = requests.get(
self.profile_url, params={"access_token": token.token}
).json()
return self.get_provider().sociallogin_from_response(request, extra_data)
oauth2_login = OAuth2LoginView.adapter_view(Auth0OAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(Auth0OAuth2Adapter)

View File

@@ -0,0 +1,110 @@
from allauth.account.models import EmailAddress
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.base import AuthAction, ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class Scope(object):
NAME = "aq:name"
EMAIL = "email"
PHONE = "phone"
ADDRESS = "address"
LOCATION = "aq:location"
PUSH = "aq:push"
IDENTITY_CLAIMS = frozenset(
[
"sub",
"name",
"given_name",
"family_name",
"middle_name",
"nickname",
"preferred_username",
"profile",
"picture",
"website",
"email",
"email_verified",
"gender",
"birthdate",
"zoneinfo",
"locale",
"phone_number",
"phone_number_verified",
"address",
"updated_at",
"aq:location",
]
)
class AuthentiqAccount(ProviderAccount):
def get_profile_url(self):
return self.account.extra_data.get("profile")
def get_avatar_url(self):
return self.account.extra_data.get("picture")
def to_str(self):
dflt = super(AuthentiqAccount, self).to_str()
return self.account.extra_data.get("name", dflt)
class AuthentiqProvider(OAuth2Provider):
id = "authentiq"
name = "Authentiq"
account_class = AuthentiqAccount
def get_scope(self, request):
scope = set(super(AuthentiqProvider, self).get_scope(request))
scope.add("openid")
if Scope.EMAIL in scope:
modifiers = ""
if app_settings.EMAIL_REQUIRED:
modifiers += "r"
if app_settings.EMAIL_VERIFICATION:
modifiers += "s"
if modifiers:
scope.add(Scope.EMAIL + "~" + modifiers)
scope.remove(Scope.EMAIL)
return list(scope)
def get_default_scope(self):
scope = [Scope.NAME, Scope.PUSH]
if app_settings.QUERY_EMAIL:
scope.append(Scope.EMAIL)
return scope
def get_auth_params(self, request, action):
ret = super(AuthentiqProvider, self).get_auth_params(request, action)
if action == AuthAction.REAUTHENTICATE:
ret["prompt"] = "select_account"
return ret
def extract_uid(self, data):
return str(data["sub"])
def extract_common_fields(self, data):
return dict(
username=data.get("preferred_username", data.get("given_name")),
email=data.get("email"),
name=data.get("name"),
first_name=data.get("given_name"),
last_name=data.get("family_name"),
)
def extract_extra_data(self, data):
return {k: v for k, v in data.items() if k in IDENTITY_CLAIMS}
def extract_email_addresses(self, data):
ret = []
email = data.get("email")
if email and data.get("email_verified"):
ret.append(EmailAddress(email=email, verified=True, primary=True))
return ret
provider_classes = [AuthentiqProvider]

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import json
from django.test.client import RequestFactory
from django.test.utils import override_settings
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from .provider import AuthentiqProvider
from .views import AuthentiqOAuth2Adapter
class AuthentiqTests(OAuth2TestsMixin, TestCase):
provider_id = AuthentiqProvider.id
def get_mocked_response(self):
return MockedResponse(
200,
json.dumps(
{
"sub": "ZLARGMFT1M",
"email": "jane@email.invalid",
"email_verified": True,
"given_name": "Jane",
"family_name": "Doe",
}
),
)
def test_default_scopes_no_email(self):
scopes = self.provider.get_default_scope()
self.assertIn("aq:name", scopes)
self.assertNotIn("email", scopes)
@override_settings(
SOCIALACCOUNT_QUERY_EMAIL=True,
)
def test_default_scopes_email(self):
scopes = self.provider.get_default_scope()
self.assertIn("aq:name", scopes)
self.assertIn("email", scopes)
def test_scopes(self):
request = RequestFactory().get(AuthentiqOAuth2Adapter.authorize_url)
scopes = self.provider.get_scope(request)
self.assertIn("openid", scopes)
self.assertIn("aq:name", scopes)
def test_dynamic_scopes(self):
request = RequestFactory().get(
AuthentiqOAuth2Adapter.authorize_url, dict(scope="foo")
)
scopes = self.provider.get_scope(request)
self.assertIn("openid", scopes)
self.assertIn("aq:name", scopes)
self.assertIn("foo", scopes)
@override_settings(
SOCIALACCOUNT_QUERY_EMAIL=True,
SOCIALACCOUNT_EMAIL_REQUIRED=True,
SOCIALACCOUNT_EMAIL_VERIFICATION=True,
)
def test_scopes_required_verified_email(self):
request = RequestFactory().get(AuthentiqOAuth2Adapter.authorize_url)
scopes = self.provider.get_scope(request)
self.assertIn("email~rs", scopes)
self.assertNotIn("email", scopes)
@override_settings(
SOCIALACCOUNT_QUERY_EMAIL=True,
SOCIALACCOUNT_EMAIL_REQUIRED=False,
SOCIALACCOUNT_EMAIL_VERIFICATION=True,
)
def test_scopes_optional_verified_email(self):
request = RequestFactory().get(AuthentiqOAuth2Adapter.authorize_url)
scopes = self.provider.get_scope(request)
self.assertIn("email~s", scopes)
self.assertNotIn("email", scopes)
@override_settings(
SOCIALACCOUNT_QUERY_EMAIL=True,
SOCIALACCOUNT_EMAIL_REQUIRED=True,
SOCIALACCOUNT_EMAIL_VERIFICATION=False,
)
def test_scopes_required_email(self):
request = RequestFactory().get(AuthentiqOAuth2Adapter.authorize_url)
scopes = self.provider.get_scope(request)
self.assertIn("email~r", scopes)
self.assertNotIn("email", scopes)
@override_settings(
SOCIALACCOUNT_QUERY_EMAIL=True,
SOCIALACCOUNT_EMAIL_REQUIRED=False,
SOCIALACCOUNT_EMAIL_VERIFICATION=False,
)
def test_scopes_optional_email(self):
request = RequestFactory().get(AuthentiqOAuth2Adapter.authorize_url)
scopes = self.provider.get_scope(request)
self.assertIn("email", scopes)

View File

@@ -0,0 +1,6 @@
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from .provider import AuthentiqProvider
urlpatterns = default_urlpatterns(AuthentiqProvider)

View File

@@ -0,0 +1,37 @@
import requests
from urllib.parse import urljoin
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.oauth2.views import (
OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView,
)
from .provider import AuthentiqProvider
class AuthentiqOAuth2Adapter(OAuth2Adapter):
provider_id = AuthentiqProvider.id
settings = app_settings.PROVIDERS.get(provider_id, {})
provider_url = settings.get("PROVIDER_URL", "https://connect.authentiq.io/")
if not provider_url.endswith("/"):
provider_url += "/"
access_token_url = urljoin(provider_url, "token")
authorize_url = urljoin(provider_url, "authorize")
profile_url = urljoin(provider_url, "userinfo")
def complete_login(self, request, app, token, **kwargs):
auth = {"Authorization": "Bearer " + token.token}
resp = requests.get(self.profile_url, headers=auth)
resp.raise_for_status()
extra_data = resp.json()
login = self.get_provider().sociallogin_from_response(request, extra_data)
return login
oauth2_login = OAuth2LoginView.adapter_view(AuthentiqOAuth2Adapter)
oauth2_callback = OAuth2CallbackView.adapter_view(AuthentiqOAuth2Adapter)

View File

@@ -0,0 +1 @@
# Create your models here.

View File

@@ -0,0 +1,51 @@
from __future__ import unicode_literals
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class AzureAccount(ProviderAccount):
# TODO:
# - avatar_url:
# https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/api/profilephoto_get # noqa
def get_username(self):
return self.account.extra_data["email"]
def to_str(self):
name = "{0} {1}".format(
self.account.extra_data.get("first_name", ""),
self.account.extra_data.get("last_name", ""),
)
if name.strip() != "":
return name
return super(AzureAccount, self).to_str()
class AzureProvider(OAuth2Provider):
id = str("azure")
name = "Azure"
account_class = AzureAccount
def get_default_scope(self):
"""
Doc on scopes available at
https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-scopes # noqa
"""
return ["User.Read", "openid"]
def extract_uid(self, data):
return str(data["id"])
def extract_common_fields(self, data):
email = data.get("mail")
if not email and "userPrincipalName" in data:
email = data.get("userPrincipalName")
return dict(
email=email,
username=email,
last_name=data.get("surname"),
first_name=data.get("givenName"),
)
provider_classes = [AzureProvider]

View File

@@ -0,0 +1,23 @@
from allauth.socialaccount.tests import OAuth2TestsMixin
from allauth.tests import MockedResponse, TestCase
from .provider import AzureProvider
class AzureTests(OAuth2TestsMixin, TestCase):
provider_id = AzureProvider.id
def get_mocked_response(self):
return MockedResponse(
200,
"""
{"displayName": "John Smith", "mobilePhone": null,
"preferredLanguage": "en-US", "jobTitle": "Director",
"userPrincipalName": "john@smith.com",
"@odata.context":
"https://graph.microsoft.com/v1.0/$metadata#users/$entity",
"officeLocation": "Paris", "businessPhones": [],
"mail": "john@smith.com", "surname": "Smith",
"givenName": "John", "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"}
""",
)

Some files were not shown because too many files have changed in this diff Show More