184 lines
6.4 KiB
Python
184 lines
6.4 KiB
Python
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)
|