2
2
Apple backend module.
3
3
"""
4
4
import logging
5
- from datetime import datetime
6
- from urllib .parse import urlparse
7
-
5
+ from .openid_connect import OpenIDConnectBackend , STATE_KEY
8
6
from oic .oauth2 .message import Message
9
- from oic import oic
10
- from oic import rndstr
11
7
from oic .oic .message import AuthorizationResponse
12
- from oic .oic .message import ProviderConfigurationResponse
13
- from oic .oic .message import RegistrationRequest
14
- from oic .utils .authn .authn_context import UNSPECIFIED
15
- from oic .utils .authn .client import CLIENT_AUTHN_METHOD
16
-
17
8
import satosa .logging_util as lu
18
- from satosa .internal import AuthenticationInformation
19
- from satosa .internal import InternalData
20
- from .base import BackendModule
21
- from .oauth import get_metadata_desc_for_oauth_backend
22
- from ..exception import SATOSAAuthenticationError , SATOSAError
23
- from ..response import Redirect
24
-
9
+ from ..exception import SATOSAAuthenticationError
25
10
import json
26
11
import requests
27
12
28
13
29
14
logger = logging .getLogger (__name__ )
30
15
31
- NONCE_KEY = "oidc_nonce"
32
- STATE_KEY = "oidc_state"
33
16
34
17
# https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
35
- class AppleBackend (BackendModule ):
18
+ class AppleBackend (OpenIDConnectBackend ):
36
19
"""Sign in with Apple backend"""
37
20
38
- def __init__ (self , auth_callback_func , internal_attributes , config , base_url , name ):
39
- """
40
- Sign in with Apple backend module.
41
- :param auth_callback_func: Callback should be called by the module after the authorization
42
- in the backend is done.
43
- :param internal_attributes: Mapping dictionary between SATOSA internal attribute names and
44
- the names returned by underlying IdP's/OP's as well as what attributes the calling SP's and
45
- RP's expects namevice.
46
- :param config: Configuration parameters for the module.
47
- :param base_url: base url of the service
48
- :param name: name of the plugin
49
-
50
- :type auth_callback_func:
51
- (satosa.context.Context, satosa.internal.InternalData) -> satosa.response.Response
52
- :type internal_attributes: dict[string, dict[str, str | list[str]]]
53
- :type config: dict[str, dict[str, str] | list[str]]
54
- :type base_url: str
55
- :type name: str
56
- """
57
- super ().__init__ (auth_callback_func , internal_attributes , base_url , name )
58
- self .auth_callback_func = auth_callback_func
59
- self .config = config
60
- self .client = _create_client (
61
- config ["provider_metadata" ],
62
- config ["client" ]["client_metadata" ],
63
- config ["client" ].get ("verify_ssl" , True ),
64
- )
65
- if "scope" not in config ["client" ]["auth_req_params" ]:
66
- config ["auth_req_params" ]["scope" ] = "openid"
67
- if "response_type" not in config ["client" ]["auth_req_params" ]:
68
- config ["auth_req_params" ]["response_type" ] = "code"
69
-
70
- def start_auth (self , context , request_info ):
71
- """
72
- See super class method satosa.backends.base#start_auth
73
- :type context: satosa.context.Context
74
- :type request_info: satosa.internal.InternalData
75
- """
76
- oidc_nonce = rndstr ()
77
- oidc_state = rndstr ()
78
- state_data = {NONCE_KEY : oidc_nonce , STATE_KEY : oidc_state }
79
- context .state [self .name ] = state_data
80
-
81
- args = {
82
- "scope" : self .config ["client" ]["auth_req_params" ]["scope" ],
83
- "response_type" : self .config ["client" ]["auth_req_params" ]["response_type" ],
84
- "client_id" : self .client .client_id ,
85
- "redirect_uri" : self .client .registration_response ["redirect_uris" ][0 ],
86
- "state" : oidc_state ,
87
- "nonce" : oidc_nonce ,
88
- }
89
- args .update (self .config ["client" ]["auth_req_params" ])
90
- auth_req = self .client .construct_AuthorizationRequest (request_args = args )
91
- login_url = auth_req .request (self .client .authorization_endpoint )
92
- return Redirect (login_url )
93
-
94
- def register_endpoints (self ):
95
- """
96
- Creates a list of all the endpoints this backend module needs to listen to. In this case
97
- it's the authentication response from the underlying OP that is redirected from the OP to
98
- the proxy.
99
- :rtype: Sequence[(str, Callable[[satosa.context.Context], satosa.response.Response]]
100
- :return: A list that can be used to map the request to SATOSA to this endpoint.
101
- """
102
- url_map = []
103
- redirect_path = urlparse (
104
- self .config ["client" ]["client_metadata" ]["redirect_uris" ][0 ]
105
- ).path
106
- if not redirect_path :
107
- raise SATOSAError ("Missing path in redirect uri" )
108
-
109
- url_map .append (("^%s$" % redirect_path .lstrip ("/" ), self .response_endpoint ))
110
- return url_map
111
-
112
- def _verify_nonce (self , nonce , context ):
113
- """
114
- Verify the received OIDC 'nonce' from the ID Token.
115
- :param nonce: OIDC nonce
116
- :type nonce: str
117
- :param context: current request context
118
- :type context: satosa.context.Context
119
- :raise SATOSAAuthenticationError: if the nonce is incorrect
120
- """
121
- backend_state = context .state [self .name ]
122
- if nonce != backend_state [NONCE_KEY ]:
123
- msg = "Missing or invalid nonce in authn response for state: {}" .format (
124
- backend_state
125
- )
126
- logline = lu .LOG_FMT .format (
127
- id = lu .get_session_id (context .state ), message = msg
128
- )
129
- logger .debug (logline )
130
- raise SATOSAAuthenticationError (
131
- context .state , "Missing or invalid nonce in authn response"
132
- )
133
-
134
21
def _get_tokens (self , authn_response , context ):
135
22
"""
136
23
:param authn_response: authentication response from OP
@@ -169,25 +56,6 @@ def _get_tokens(self, authn_response, context):
169
56
170
57
return authn_response .get ("access_token" ), authn_response .get ("id_token" )
171
58
172
- def _check_error_response (self , response , context ):
173
- """
174
- Check if the response is an OAuth error response.
175
- :param response: the OIDC response
176
- :type response: oic.oic.message
177
- :raise SATOSAAuthenticationError: if the response is an OAuth error response
178
- """
179
- if "error" in response :
180
- msg = "{name} error: {error} {description}" .format (
181
- name = type (response ).__name__ ,
182
- error = response ["error" ],
183
- description = response .get ("error_description" , "" ),
184
- )
185
- logline = lu .LOG_FMT .format (
186
- id = lu .get_session_id (context .state ), message = msg
187
- )
188
- logger .debug (logline )
189
- raise SATOSAAuthenticationError (context .state , "Access denied" )
190
-
191
59
def response_endpoint (self , context , * args ):
192
60
"""
193
61
Handles the authentication response from the OP.
@@ -209,8 +77,8 @@ def response_endpoint(self, context, *args):
209
77
# - https://developer.apple.com/documentation/sign_in_with_apple/namei
210
78
try :
211
79
userdata = context .request .get ("user" , "{}" )
212
- userinfo = json .load (userdata )
213
- except Exception :
80
+ userinfo = json .loads (userdata )
81
+ except json . JSONDecodeError :
214
82
userinfo = {}
215
83
216
84
authn_resp = self .client .parse_response (
@@ -242,78 +110,19 @@ def response_endpoint(self, context, *args):
242
110
raise SATOSAAuthenticationError (context .state , "No user info available." )
243
111
244
112
all_user_claims = dict (list (userinfo .items ()) + list (id_token_claims .items ()))
113
+
114
+ # convert "string or Boolean" claims to actual booleans
115
+ for bool_claim_name in ["email_verified" , "is_private_email" ]:
116
+ userinfo [bool_claim_name ] = (
117
+ True
118
+ if userinfo [bool_claim_name ] == "true"
119
+ else False
120
+ )
121
+
245
122
msg = "UserInfo: {}" .format (all_user_claims )
246
123
logline = lu .LOG_FMT .format (id = lu .get_session_id (context .state ), message = msg )
247
124
logger .debug (logline )
248
125
internal_resp = self ._translate_response (
249
126
all_user_claims , self .client .authorization_endpoint
250
127
)
251
128
return self .auth_callback_func (context , internal_resp )
252
-
253
- def _translate_response (self , response , issuer ):
254
- """
255
- Translates oidc response to SATOSA internal response.
256
- :type response: dict[str, str]
257
- :type issuer: str
258
- :type subject_type: str
259
- :rtype: InternalData
260
-
261
- :param response: Dictioary with attribute name as key.
262
- :param issuer: The oidc op that gave the repsonse.
263
- :param subject_type: public or pairwise according to oidc standard.
264
- :return: A SATOSA internal response.
265
- """
266
- auth_info = AuthenticationInformation (UNSPECIFIED , str (datetime .now ()), issuer )
267
- internal_resp = InternalData (auth_info = auth_info )
268
- internal_resp .attributes = self .converter .to_internal ("openid" , response )
269
- internal_resp .subject_id = response ["sub" ]
270
- return internal_resp
271
-
272
- def get_metadata_desc (self ):
273
- """
274
- See satosa.backends.oauth.get_metadata_desc
275
- :rtype: satosa.metadata_creation.description.MetadataDescription
276
- """
277
- return get_metadata_desc_for_oauth_backend (
278
- self .config ["provider_metadata" ]["issuer" ], self .config
279
- )
280
-
281
-
282
- def _create_client (provider_metadata , client_metadata , verify_ssl = True ):
283
- """
284
- Create a pyoidc client instance.
285
- :param provider_metadata: provider configuration information
286
- :type provider_metadata: Mapping[str, Union[str, Sequence[str]]]
287
- :param client_metadata: client metadata
288
- :type client_metadata: Mapping[str, Union[str, Sequence[str]]]
289
- :return: client instance to use for communicating with the configured provider
290
- :rtype: oic.oic.Client
291
- """
292
- client = oic .Client (client_authn_method = CLIENT_AUTHN_METHOD , verify_ssl = verify_ssl )
293
-
294
- # Provider configuration information
295
- if "authorization_endpoint" in provider_metadata :
296
- # no dynamic discovery necessary
297
- client .handle_provider_config (
298
- ProviderConfigurationResponse (** provider_metadata ),
299
- provider_metadata ["issuer" ],
300
- )
301
- else :
302
- # do dynamic discovery
303
- client .provider_config (provider_metadata ["issuer" ])
304
-
305
- # Client information
306
- if "client_id" in client_metadata :
307
- # static client info provided
308
- client .store_registration_info (RegistrationRequest (** client_metadata ))
309
- else :
310
- # do dynamic registration
311
- client .register (
312
- client .provider_info ["registration_endpoint" ], ** client_metadata
313
- )
314
-
315
- client .subject_type = (
316
- client .registration_response .get ("subject_type" )
317
- or client .provider_info ["subject_types_supported" ][0 ]
318
- )
319
- return client
0 commit comments