-
-
Notifications
You must be signed in to change notification settings - Fork 540
/
apple.py
171 lines (142 loc) · 5.52 KB
/
apple.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
"""
Sign In With Apple authentication backend.
Docs:
* https://developer.apple.com/documentation/signinwithapplerestapi
* https://developer.apple.com/documentation/signinwithapplerestapi/tokenresponse
Settings:
* `TEAM` - your team id;
* `KEY` - your key id;
* `CLIENT` - your client id;
* `AUDIENCE` - a list of authorized client IDs, defaults to [CLIENT].
Use this if you need to accept both service and bundle id to
be able to login both via iOS and ie a web form.
* `SECRET` - your secret key;
* `SCOPE` (optional) - e.g. `['name', 'email']`;
* `EMAIL_AS_USERNAME` - use apple email is username is set, use apple id
otherwise.
* `AppleIdAuth.TOKEN_TTL_SEC` - time before JWT token expiration, seconds.
* `SOCIAL_AUTH_APPLE_ID_INACTIVE_USER_LOGIN` - allow inactive users email to
login
"""
import json
import time
import jwt
from jwt.algorithms import RSAAlgorithm
from jwt.exceptions import PyJWTError
from social_core.backends.oauth import BaseOAuth2
from social_core.exceptions import AuthCanceled
class AppleIdAuth(BaseOAuth2):
name = 'apple-id'
JWK_URL = 'https://appleid.apple.com/auth/keys'
AUTHORIZATION_URL = 'https://appleid.apple.com/auth/authorize'
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
ACCESS_TOKEN_METHOD = 'POST'
RESPONSE_MODE = None
ID_KEY = 'sub'
TOKEN_KEY = 'id_token'
STATE_PARAMETER = True
REDIRECT_STATE = False
TOKEN_AUDIENCE = 'https://appleid.apple.com'
TOKEN_TTL_SEC = 6 * 30 * 24 * 60 * 60
def get_audience(self):
client_id = self.setting('CLIENT')
return self.setting('AUDIENCE', default=[client_id])
def auth_params(self, *args, **kwargs):
"""
Apple requires to set `response_mode` to `form_post` if `scope`
parameter is passed.
"""
params = super(AppleIdAuth, self).auth_params(*args, **kwargs)
if self.RESPONSE_MODE:
params['response_mode'] = self.RESPONSE_MODE
elif self.get_scope():
params['response_mode'] = 'form_post'
return params
def get_private_key(self):
"""
Return contents of the private key file. Override this method to provide
secret key from another source if needed.
"""
return self.setting("SECRET")
def generate_client_secret(self):
now = int(time.time())
client_id = self.setting('CLIENT')
team_id = self.setting('TEAM')
key_id = self.setting('KEY')
private_key = self.get_private_key()
headers = {'kid': key_id}
payload = {
'iss': team_id,
'iat': now,
'exp': now + self.TOKEN_TTL_SEC,
'aud': self.TOKEN_AUDIENCE,
'sub': client_id,
}
return jwt.encode(payload, key=private_key, algorithm='ES256',
headers=headers)
def get_key_and_secret(self):
client_id = self.setting('CLIENT')
client_secret = self.generate_client_secret()
return client_id, client_secret
def get_apple_jwk(self, kid=None):
"""
Return requested Apple public key or all available.
"""
keys = self.get_json(url=self.JWK_URL).get("keys")
if not isinstance(keys, list) or not keys:
raise AuthCanceled("Invalid jwk response")
if kid:
return json.dumps([key for key in keys if key['kid'] == kid][0])
else:
return (json.dumps(key) for key in keys)
def decode_id_token(self, id_token):
"""
Decode and validate JWT token from apple and return payload including
user data.
"""
if not id_token:
raise AuthCanceled("Missing id_token parameter")
kid = jwt.get_unverified_header(id_token).get('kid')
public_key = RSAAlgorithm.from_jwk(self.get_apple_jwk(kid))
try:
decoded = jwt.decode(
id_token,
key=public_key,
audience=self.get_audience(),
algorithm="RS256",
)
except PyJWTError:
raise AuthCanceled("Token validation failed")
return decoded
def get_user_details(self, response):
name = response.get('name') or {}
fullname, first_name, last_name = self.get_user_names(
fullname='',
first_name=name.get('firstName', ''),
last_name=name.get('lastName', '')
)
email = response.get('email', '')
apple_id = response.get(self.ID_KEY, '')
# prevent updating User with empty strings
user_details = {
'first_name': first_name or None,
'last_name': last_name or None,
'email': email,
}
if email and self.setting('EMAIL_AS_USERNAME'):
user_details['username'] = email
if apple_id and not self.setting('EMAIL_AS_USERNAME'):
user_details['username'] = apple_id
return user_details
def do_auth(self, access_token, *args, **kwargs):
response = kwargs.pop('response', None) or {}
jwt_string = response.get(self.TOKEN_KEY) or access_token
if not jwt_string:
raise AuthCanceled('Missing id_token parameter')
decoded_data = self.decode_id_token(jwt_string)
return super(AppleIdAuth, self).do_auth(
access_token,
response=decoded_data,
*args,
**kwargs
)