22Apple backend module.
33"""
44import logging
5- from datetime import datetime
6- from urllib .parse import urlparse
7-
5+ from .openid_connect import OpenIDConnectBackend , STATE_KEY
86from oic .oauth2 .message import Message
9- from oic import oic
10- from oic import rndstr
117from 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-
178import 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
2510import json
2611import requests
2712
2813
2914logger = logging .getLogger (__name__ )
3015
31- NONCE_KEY = "oidc_nonce"
32- STATE_KEY = "oidc_state"
3316
3417# https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
35- class AppleBackend (BackendModule ):
18+ class AppleBackend (OpenIDConnectBackend ):
3619 """Sign in with Apple backend"""
3720
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-
13421 def _get_tokens (self , authn_response , context ):
13522 """
13623 :param authn_response: authentication response from OP
@@ -169,25 +56,6 @@ def _get_tokens(self, authn_response, context):
16956
17057 return authn_response .get ("access_token" ), authn_response .get ("id_token" )
17158
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-
19159 def response_endpoint (self , context , * args ):
19260 """
19361 Handles the authentication response from the OP.
@@ -209,8 +77,8 @@ def response_endpoint(self, context, *args):
20977 # - https://developer.apple.com/documentation/sign_in_with_apple/namei
21078 try :
21179 userdata = context .request .get ("user" , "{}" )
212- userinfo = json .load (userdata )
213- except Exception :
80+ userinfo = json .loads (userdata )
81+ except json . JSONDecodeError :
21482 userinfo = {}
21583
21684 authn_resp = self .client .parse_response (
@@ -242,78 +110,19 @@ def response_endpoint(self, context, *args):
242110 raise SATOSAAuthenticationError (context .state , "No user info available." )
243111
244112 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+
245122 msg = "UserInfo: {}" .format (all_user_claims )
246123 logline = lu .LOG_FMT .format (id = lu .get_session_id (context .state ), message = msg )
247124 logger .debug (logline )
248125 internal_resp = self ._translate_response (
249126 all_user_claims , self .client .authorization_endpoint
250127 )
251128 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