|
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 | | - |
34 | 16 | # https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple |
35 | | -class AppleBackend(BackendModule): |
| 17 | +class AppleBackend(OpenIDConnectBackend): |
36 | 18 | """Sign in with Apple backend""" |
37 | 19 |
|
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 | 20 | def _get_tokens(self, authn_response, context): |
135 | 21 | """ |
136 | 22 | :param authn_response: authentication response from OP |
@@ -169,25 +55,6 @@ def _get_tokens(self, authn_response, context): |
169 | 55 |
|
170 | 56 | return authn_response.get("access_token"), authn_response.get("id_token") |
171 | 57 |
|
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 | 58 | def response_endpoint(self, context, *args): |
192 | 59 | """ |
193 | 60 | Handles the authentication response from the OP. |
@@ -249,71 +116,3 @@ def response_endpoint(self, context, *args): |
249 | 116 | all_user_claims, self.client.authorization_endpoint |
250 | 117 | ) |
251 | 118 | 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