Skip to content

Commit 4fed399

Browse files
Merge pull request #408 from melanger/patch-3
feat: Collect user information when using the Apple backend
2 parents fbc42e7 + 58421c8 commit 4fed399

File tree

1 file changed

+69
-33
lines changed

1 file changed

+69
-33
lines changed

src/satosa/backends/apple.py

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,7 @@ def start_auth(self, context, request_info):
7676
"""
7777
oidc_nonce = rndstr()
7878
oidc_state = rndstr()
79-
state_data = {
80-
NONCE_KEY: oidc_nonce,
81-
STATE_KEY: oidc_state
82-
}
79+
state_data = {NONCE_KEY: oidc_nonce, STATE_KEY: oidc_state}
8380
context.state[self.name] = state_data
8481

8582
args = {
@@ -88,7 +85,7 @@ def start_auth(self, context, request_info):
8885
"client_id": self.client.client_id,
8986
"redirect_uri": self.client.registration_response["redirect_uris"][0],
9087
"state": oidc_state,
91-
"nonce": oidc_nonce
88+
"nonce": oidc_nonce,
9289
}
9390
args.update(self.config["client"]["auth_req_params"])
9491
auth_req = self.client.construct_AuthorizationRequest(request_args=args)
@@ -104,7 +101,9 @@ def register_endpoints(self):
104101
:return: A list that can be used to map the request to SATOSA to this endpoint.
105102
"""
106103
url_map = []
107-
redirect_path = urlparse(self.config["client"]["client_metadata"]["redirect_uris"][0]).path
104+
redirect_path = urlparse(
105+
self.config["client"]["client_metadata"]["redirect_uris"][0]
106+
).path
108107
if not redirect_path:
109108
raise SATOSAError("Missing path in redirect uri")
110109

@@ -122,10 +121,16 @@ def _verify_nonce(self, nonce, context):
122121
"""
123122
backend_state = context.state[self.name]
124123
if nonce != backend_state[NONCE_KEY]:
125-
msg = "Missing or invalid nonce in authn response for state: {}".format(backend_state)
126-
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
124+
msg = "Missing or invalid nonce in authn response for state: {}".format(
125+
backend_state
126+
)
127+
logline = lu.LOG_FMT.format(
128+
id=lu.get_session_id(context.state), message=msg
129+
)
127130
logger.debug(logline)
128-
raise SATOSAAuthenticationError(context.state, "Missing or invalid nonce in authn response")
131+
raise SATOSAAuthenticationError(
132+
context.state, "Missing or invalid nonce in authn response"
133+
)
129134

130135
def _get_tokens(self, authn_response, context):
131136
"""
@@ -142,22 +147,24 @@ def _get_tokens(self, authn_response, context):
142147
"client_secret": self.client.client_secret,
143148
"code": authn_response["code"],
144149
"grant_type": "authorization_code",
145-
"redirect_uri": self.client.registration_response['redirect_uris'][0],
150+
"redirect_uri": self.client.registration_response["redirect_uris"][0],
146151
}
147152

148153
token_resp = requests.post(
149154
"https://appleid.apple.com/auth/token",
150155
data=args,
151-
headers={"Content-Type": "application/x-www-form-urlencoded"}
152-
).json()
156+
headers={"Content-Type": "application/x-www-form-urlencoded"},
157+
).json()
153158

154159
logger.debug("apple response received")
155160
logger.debug(token_resp)
156161

157162
self._check_error_response(token_resp, context)
158163

159164
keyjar = self.client.keyjar
160-
id_token_claims = dict(Message().from_jwt(token_resp["id_token"], keyjar=keyjar))
165+
id_token_claims = dict(
166+
Message().from_jwt(token_resp["id_token"], keyjar=keyjar)
167+
)
161168

162169
return token_resp["access_token"], id_token_claims
163170

@@ -176,7 +183,9 @@ def _check_error_response(self, response, context):
176183
error=response["error"],
177184
description=response.get("error_description", ""),
178185
)
179-
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
186+
logline = lu.LOG_FMT.format(
187+
id=lu.get_session_id(context.state), message=msg
188+
)
180189
logger.debug(logline)
181190
raise SATOSAAuthenticationError(context.state, "Access denied")
182191

@@ -192,24 +201,44 @@ def response_endpoint(self, context, *args):
192201
:return:
193202
"""
194203
backend_state = context.state[self.name]
195-
authn_resp = self.client.parse_response(AuthorizationResponse, info=context.request, sformat="dict")
204+
205+
# Apple has no userinfo endpoint
206+
# but may send some user information via POST in the first request.
207+
#
208+
# References:
209+
# - https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple
210+
# - https://developer.apple.com/documentation/sign_in_with_apple/namei
211+
try:
212+
userdata = context.request.get("user", "{}")
213+
userinfo = json.load(userdata)
214+
except Exception as e:
215+
userinfo = {}
216+
217+
authn_resp = self.client.parse_response(
218+
AuthorizationResponse, info=context.request, sformat="dict"
219+
)
196220
if backend_state[STATE_KEY] != authn_resp["state"]:
197-
msg = "Missing or invalid state in authn response for state: {}".format(backend_state)
198-
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
221+
msg = "Missing or invalid state in authn response for state: {}".format(
222+
backend_state
223+
)
224+
logline = lu.LOG_FMT.format(
225+
id=lu.get_session_id(context.state), message=msg
226+
)
199227
logger.debug(logline)
200-
raise SATOSAAuthenticationError(context.state, "Missing or invalid state in authn response")
228+
raise SATOSAAuthenticationError(
229+
context.state, "Missing or invalid state in authn response"
230+
)
201231

202232
self._check_error_response(authn_resp, context)
203233
access_token, id_token_claims = self._get_tokens(authn_resp, context)
204234
if not id_token_claims:
205235
id_token_claims = {}
206236

207-
# Apple has no userinfo endpoint
208-
userinfo = {}
209-
210237
if not id_token_claims and not userinfo:
211238
msg = "No id_token or userinfo, nothing to do.."
212-
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
239+
logline = lu.LOG_FMT.format(
240+
id=lu.get_session_id(context.state), message=msg
241+
)
213242
logger.error(logline)
214243
raise SATOSAAuthenticationError(context.state, "No user info available.")
215244

@@ -218,7 +247,9 @@ def response_endpoint(self, context, *args):
218247
logline = lu.LOG_FMT.format(id=lu.get_session_id(context.state), message=msg)
219248
logger.debug(logline)
220249
del context.state[self.name]
221-
internal_resp = self._translate_response(all_user_claims, self.client.authorization_endpoint)
250+
internal_resp = self._translate_response(
251+
all_user_claims, self.client.authorization_endpoint
252+
)
222253
return self.auth_callback_func(context, internal_resp)
223254

224255
def _translate_response(self, response, issuer):
@@ -245,7 +276,9 @@ def get_metadata_desc(self):
245276
See satosa.backends.oauth.get_metadata_desc
246277
:rtype: satosa.metadata_creation.description.MetadataDescription
247278
"""
248-
return get_metadata_desc_for_oauth_backend(self.config["provider_metadata"]["issuer"], self.config)
279+
return get_metadata_desc_for_oauth_backend(
280+
self.config["provider_metadata"]["issuer"], self.config
281+
)
249282

250283

251284
def _create_client(provider_metadata, client_metadata, verify_ssl=True):
@@ -258,15 +291,15 @@ def _create_client(provider_metadata, client_metadata, verify_ssl=True):
258291
:return: client instance to use for communicating with the configured provider
259292
:rtype: oic.oic.Client
260293
"""
261-
client = oic.Client(
262-
client_authn_method=CLIENT_AUTHN_METHOD, verify_ssl=verify_ssl
263-
)
294+
client = oic.Client(client_authn_method=CLIENT_AUTHN_METHOD, verify_ssl=verify_ssl)
264295

265296
# Provider configuration information
266297
if "authorization_endpoint" in provider_metadata:
267298
# no dynamic discovery necessary
268-
client.handle_provider_config(ProviderConfigurationResponse(**provider_metadata),
269-
provider_metadata["issuer"])
299+
client.handle_provider_config(
300+
ProviderConfigurationResponse(**provider_metadata),
301+
provider_metadata["issuer"],
302+
)
270303
else:
271304
# do dynamic discovery
272305
client.provider_config(provider_metadata["issuer"])
@@ -277,9 +310,12 @@ def _create_client(provider_metadata, client_metadata, verify_ssl=True):
277310
client.store_registration_info(RegistrationRequest(**client_metadata))
278311
else:
279312
# do dynamic registration
280-
client.register(client.provider_info['registration_endpoint'],
281-
**client_metadata)
313+
client.register(
314+
client.provider_info["registration_endpoint"], **client_metadata
315+
)
282316

283-
client.subject_type = (client.registration_response.get("subject_type") or
284-
client.provider_info["subject_types_supported"][0])
317+
client.subject_type = (
318+
client.registration_response.get("subject_type")
319+
or client.provider_info["subject_types_supported"][0]
320+
)
285321
return client

0 commit comments

Comments
 (0)