Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/oauthlib/oauth2/rfc6749/grant_types/authorization_code.py: 13%
157 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:22 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:22 +0000
1"""
2oauthlib.oauth2.rfc6749.grant_types
3~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4"""
5import base64
6import hashlib
7import json
8import logging
10from oauthlib import common
12from .. import errors
13from .base import GrantTypeBase
15log = logging.getLogger(__name__)
18def code_challenge_method_s256(verifier, challenge):
19 """
20 If the "code_challenge_method" from `Section 4.3`_ was "S256", the
21 received "code_verifier" is hashed by SHA-256, base64url-encoded, and
22 then compared to the "code_challenge", i.e.:
24 BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
26 How to implement a base64url-encoding
27 function without padding, based upon the standard base64-encoding
28 function that uses padding.
30 To be concrete, example C# code implementing these functions is shown
31 below. Similar code could be used in other languages.
33 static string base64urlencode(byte [] arg)
34 {
35 string s = Convert.ToBase64String(arg); // Regular base64 encoder
36 s = s.Split('=')[0]; // Remove any trailing '='s
37 s = s.Replace('+', '-'); // 62nd char of encoding
38 s = s.Replace('/', '_'); // 63rd char of encoding
39 return s;
40 }
42 In python urlsafe_b64encode is already replacing '+' and '/', but preserve
43 the trailing '='. So we have to remove it.
45 .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
46 """
47 return base64.urlsafe_b64encode(
48 hashlib.sha256(verifier.encode()).digest()
49 ).decode().rstrip('=') == challenge
52def code_challenge_method_plain(verifier, challenge):
53 """
54 If the "code_challenge_method" from `Section 4.3`_ was "plain", they are
55 compared directly, i.e.:
57 code_verifier == code_challenge.
59 .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
60 """
61 return verifier == challenge
64class AuthorizationCodeGrant(GrantTypeBase):
66 """`Authorization Code Grant`_
68 The authorization code grant type is used to obtain both access
69 tokens and refresh tokens and is optimized for confidential clients.
70 Since this is a redirection-based flow, the client must be capable of
71 interacting with the resource owner's user-agent (typically a web
72 browser) and capable of receiving incoming requests (via redirection)
73 from the authorization server::
75 +----------+
76 | Resource |
77 | Owner |
78 | |
79 +----------+
80 ^
81 |
82 (B)
83 +----|-----+ Client Identifier +---------------+
84 | -+----(A)-- & Redirection URI ---->| |
85 | User- | | Authorization |
86 | Agent -+----(B)-- User authenticates --->| Server |
87 | | | |
88 | -+----(C)-- Authorization Code ---<| |
89 +-|----|---+ +---------------+
90 | | ^ v
91 (A) (C) | |
92 | | | |
93 ^ v | |
94 +---------+ | |
95 | |>---(D)-- Authorization Code ---------' |
96 | Client | & Redirection URI |
97 | | |
98 | |<---(E)----- Access Token -------------------'
99 +---------+ (w/ Optional Refresh Token)
101 Note: The lines illustrating steps (A), (B), and (C) are broken into
102 two parts as they pass through the user-agent.
104 Figure 3: Authorization Code Flow
106 The flow illustrated in Figure 3 includes the following steps:
108 (A) The client initiates the flow by directing the resource owner's
109 user-agent to the authorization endpoint. The client includes
110 its client identifier, requested scope, local state, and a
111 redirection URI to which the authorization server will send the
112 user-agent back once access is granted (or denied).
114 (B) The authorization server authenticates the resource owner (via
115 the user-agent) and establishes whether the resource owner
116 grants or denies the client's access request.
118 (C) Assuming the resource owner grants access, the authorization
119 server redirects the user-agent back to the client using the
120 redirection URI provided earlier (in the request or during
121 client registration). The redirection URI includes an
122 authorization code and any local state provided by the client
123 earlier.
125 (D) The client requests an access token from the authorization
126 server's token endpoint by including the authorization code
127 received in the previous step. When making the request, the
128 client authenticates with the authorization server. The client
129 includes the redirection URI used to obtain the authorization
130 code for verification.
132 (E) The authorization server authenticates the client, validates the
133 authorization code, and ensures that the redirection URI
134 received matches the URI used to redirect the client in
135 step (C). If valid, the authorization server responds back with
136 an access token and, optionally, a refresh token.
138 OAuth 2.0 public clients utilizing the Authorization Code Grant are
139 susceptible to the authorization code interception attack.
141 A technique to mitigate against the threat through the use of Proof Key for Code
142 Exchange (PKCE, pronounced "pixy") is implemented in the current oauthlib
143 implementation.
145 .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1
146 .. _`PKCE`: https://tools.ietf.org/html/rfc7636
147 """
149 default_response_mode = 'query'
150 response_types = ['code']
152 # This dict below is private because as RFC mention it:
153 # "S256" is Mandatory To Implement (MTI) on the server.
154 #
155 _code_challenge_methods = {
156 'plain': code_challenge_method_plain,
157 'S256': code_challenge_method_s256
158 }
160 def create_authorization_code(self, request):
161 """
162 Generates an authorization grant represented as a dictionary.
164 :param request: OAuthlib request.
165 :type request: oauthlib.common.Request
166 """
167 grant = {'code': common.generate_token()}
168 if hasattr(request, 'state') and request.state:
169 grant['state'] = request.state
170 log.debug('Created authorization code grant %r for request %r.',
171 grant, request)
172 return grant
174 def create_authorization_response(self, request, token_handler):
175 """
176 The client constructs the request URI by adding the following
177 parameters to the query component of the authorization endpoint URI
178 using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
180 response_type
181 REQUIRED. Value MUST be set to "code" for standard OAuth2
182 authorization flow. For OpenID Connect it must be one of
183 "code token", "code id_token", or "code token id_token" - we
184 essentially test that "code" appears in the response_type.
185 client_id
186 REQUIRED. The client identifier as described in `Section 2.2`_.
187 redirect_uri
188 OPTIONAL. As described in `Section 3.1.2`_.
189 scope
190 OPTIONAL. The scope of the access request as described by
191 `Section 3.3`_.
192 state
193 RECOMMENDED. An opaque value used by the client to maintain
194 state between the request and callback. The authorization
195 server includes this value when redirecting the user-agent back
196 to the client. The parameter SHOULD be used for preventing
197 cross-site request forgery as described in `Section 10.12`_.
199 The client directs the resource owner to the constructed URI using an
200 HTTP redirection response, or by other means available to it via the
201 user-agent.
203 :param request: OAuthlib request.
204 :type request: oauthlib.common.Request
205 :param token_handler: A token handler instance, for example of type
206 oauthlib.oauth2.BearerToken.
207 :returns: headers, body, status
208 :raises: FatalClientError on invalid redirect URI or client id.
210 A few examples::
212 >>> from your_validator import your_validator
213 >>> request = Request('https://example.com/authorize?client_id=valid'
214 ... '&redirect_uri=http%3A%2F%2Fclient.com%2F')
215 >>> from oauthlib.common import Request
216 >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
217 >>> token = BearerToken(your_validator)
218 >>> grant = AuthorizationCodeGrant(your_validator)
219 >>> request.scopes = ['authorized', 'in', 'some', 'form']
220 >>> grant.create_authorization_response(request, token)
221 (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
222 >>> request = Request('https://example.com/authorize?client_id=valid'
223 ... '&redirect_uri=http%3A%2F%2Fclient.com%2F'
224 ... '&response_type=code')
225 >>> request.scopes = ['authorized', 'in', 'some', 'form']
226 >>> grant.create_authorization_response(request, token)
227 (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200)
228 >>> # If the client id or redirect uri fails validation
229 >>> grant.create_authorization_response(request, token)
230 Traceback (most recent call last):
231 File "<stdin>", line 1, in <module>
232 File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response
233 >>> grant.create_authorization_response(request, token)
234 File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
235 oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
237 .. _`Appendix B`: https://tools.ietf.org/html/rfc6749#appendix-B
238 .. _`Section 2.2`: https://tools.ietf.org/html/rfc6749#section-2.2
239 .. _`Section 3.1.2`: https://tools.ietf.org/html/rfc6749#section-3.1.2
240 .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3
241 .. _`Section 10.12`: https://tools.ietf.org/html/rfc6749#section-10.12
242 """
243 try:
244 self.validate_authorization_request(request)
245 log.debug('Pre resource owner authorization validation ok for %r.',
246 request)
248 # If the request fails due to a missing, invalid, or mismatching
249 # redirection URI, or if the client identifier is missing or invalid,
250 # the authorization server SHOULD inform the resource owner of the
251 # error and MUST NOT automatically redirect the user-agent to the
252 # invalid redirection URI.
253 except errors.FatalClientError as e:
254 log.debug('Fatal client error during validation of %r. %r.',
255 request, e)
256 raise
258 # If the resource owner denies the access request or if the request
259 # fails for reasons other than a missing or invalid redirection URI,
260 # the authorization server informs the client by adding the following
261 # parameters to the query component of the redirection URI using the
262 # "application/x-www-form-urlencoded" format, per Appendix B:
263 # https://tools.ietf.org/html/rfc6749#appendix-B
264 except errors.OAuth2Error as e:
265 log.debug('Client error during validation of %r. %r.', request, e)
266 request.redirect_uri = request.redirect_uri or self.error_uri
267 redirect_uri = common.add_params_to_uri(
268 request.redirect_uri, e.twotuples,
269 fragment=request.response_mode == "fragment")
270 return {'Location': redirect_uri}, None, 302
272 grant = self.create_authorization_code(request)
273 for modifier in self._code_modifiers:
274 grant = modifier(grant, token_handler, request)
275 if 'access_token' in grant:
276 self.request_validator.save_token(grant, request)
277 log.debug('Saving grant %r for %r.', grant, request)
278 self.request_validator.save_authorization_code(
279 request.client_id, grant, request)
280 return self.prepare_authorization_response(
281 request, grant, {}, None, 302)
283 def create_token_response(self, request, token_handler):
284 """Validate the authorization code.
286 The client MUST NOT use the authorization code more than once. If an
287 authorization code is used more than once, the authorization server
288 MUST deny the request and SHOULD revoke (when possible) all tokens
289 previously issued based on that authorization code. The authorization
290 code is bound to the client identifier and redirection URI.
292 :param request: OAuthlib request.
293 :type request: oauthlib.common.Request
294 :param token_handler: A token handler instance, for example of type
295 oauthlib.oauth2.BearerToken.
297 """
298 headers = self._get_default_headers()
299 try:
300 self.validate_token_request(request)
301 log.debug('Token request validation ok for %r.', request)
302 except errors.OAuth2Error as e:
303 log.debug('Client error during validation of %r. %r.', request, e)
304 headers.update(e.headers)
305 return headers, e.json, e.status_code
307 token = token_handler.create_token(request, refresh_token=self.refresh_token)
309 for modifier in self._token_modifiers:
310 token = modifier(token, token_handler, request)
312 self.request_validator.save_token(token, request)
313 self.request_validator.invalidate_authorization_code(
314 request.client_id, request.code, request)
315 headers.update(self._create_cors_headers(request))
316 return headers, json.dumps(token), 200
318 def validate_authorization_request(self, request):
319 """Check the authorization request for normal and fatal errors.
321 A normal error could be a missing response_type parameter or the client
322 attempting to access scope it is not allowed to ask authorization for.
323 Normal errors can safely be included in the redirection URI and
324 sent back to the client.
326 Fatal errors occur when the client_id or redirect_uri is invalid or
327 missing. These must be caught by the provider and handled, how this
328 is done is outside of the scope of OAuthLib but showing an error
329 page describing the issue is a good idea.
331 :param request: OAuthlib request.
332 :type request: oauthlib.common.Request
333 """
335 # First check for fatal errors
337 # If the request fails due to a missing, invalid, or mismatching
338 # redirection URI, or if the client identifier is missing or invalid,
339 # the authorization server SHOULD inform the resource owner of the
340 # error and MUST NOT automatically redirect the user-agent to the
341 # invalid redirection URI.
343 # First check duplicate parameters
344 for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
345 try:
346 duplicate_params = request.duplicate_params
347 except ValueError:
348 raise errors.InvalidRequestFatalError(description='Unable to parse query string', request=request)
349 if param in duplicate_params:
350 raise errors.InvalidRequestFatalError(description='Duplicate %s parameter.' % param, request=request)
352 # REQUIRED. The client identifier as described in Section 2.2.
353 # https://tools.ietf.org/html/rfc6749#section-2.2
354 if not request.client_id:
355 raise errors.MissingClientIdError(request=request)
357 if not self.request_validator.validate_client_id(request.client_id, request):
358 raise errors.InvalidClientIdError(request=request)
360 # OPTIONAL. As described in Section 3.1.2.
361 # https://tools.ietf.org/html/rfc6749#section-3.1.2
362 log.debug('Validating redirection uri %s for client %s.',
363 request.redirect_uri, request.client_id)
365 # OPTIONAL. As described in Section 3.1.2.
366 # https://tools.ietf.org/html/rfc6749#section-3.1.2
367 self._handle_redirects(request)
369 # Then check for normal errors.
371 # If the resource owner denies the access request or if the request
372 # fails for reasons other than a missing or invalid redirection URI,
373 # the authorization server informs the client by adding the following
374 # parameters to the query component of the redirection URI using the
375 # "application/x-www-form-urlencoded" format, per Appendix B.
376 # https://tools.ietf.org/html/rfc6749#appendix-B
378 # Note that the correct parameters to be added are automatically
379 # populated through the use of specific exceptions.
381 request_info = {}
382 for validator in self.custom_validators.pre_auth:
383 request_info.update(validator(request))
385 # REQUIRED.
386 if request.response_type is None:
387 raise errors.MissingResponseTypeError(request=request)
388 # Value MUST be set to "code" or one of the OpenID authorization code including
389 # response_types "code token", "code id_token", "code token id_token"
390 elif not 'code' in request.response_type and request.response_type != 'none':
391 raise errors.UnsupportedResponseTypeError(request=request)
393 if not self.request_validator.validate_response_type(request.client_id,
394 request.response_type,
395 request.client, request):
397 log.debug('Client %s is not authorized to use response_type %s.',
398 request.client_id, request.response_type)
399 raise errors.UnauthorizedClientError(request=request)
401 # OPTIONAL. Validate PKCE request or reply with "error"/"invalid_request"
402 # https://tools.ietf.org/html/rfc6749#section-4.4.1
403 if self.request_validator.is_pkce_required(request.client_id, request) is True:
404 if request.code_challenge is None:
405 raise errors.MissingCodeChallengeError(request=request)
407 if request.code_challenge is not None:
408 request_info["code_challenge"] = request.code_challenge
410 # OPTIONAL, defaults to "plain" if not present in the request.
411 if request.code_challenge_method is None:
412 request.code_challenge_method = "plain"
414 if request.code_challenge_method not in self._code_challenge_methods:
415 raise errors.UnsupportedCodeChallengeMethodError(request=request)
416 request_info["code_challenge_method"] = request.code_challenge_method
418 # OPTIONAL. The scope of the access request as described by Section 3.3
419 # https://tools.ietf.org/html/rfc6749#section-3.3
420 self.validate_scopes(request)
422 request_info.update({
423 'client_id': request.client_id,
424 'redirect_uri': request.redirect_uri,
425 'response_type': request.response_type,
426 'state': request.state,
427 'request': request
428 })
430 for validator in self.custom_validators.post_auth:
431 request_info.update(validator(request))
433 return request.scopes, request_info
435 def validate_token_request(self, request):
436 """
437 :param request: OAuthlib request.
438 :type request: oauthlib.common.Request
439 """
440 # REQUIRED. Value MUST be set to "authorization_code".
441 if request.grant_type not in ('authorization_code', 'openid'):
442 raise errors.UnsupportedGrantTypeError(request=request)
444 for validator in self.custom_validators.pre_token:
445 validator(request)
447 if request.code is None:
448 raise errors.InvalidRequestError(
449 description='Missing code parameter.', request=request)
451 for param in ('client_id', 'grant_type', 'redirect_uri'):
452 if param in request.duplicate_params:
453 raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
454 request=request)
456 if self.request_validator.client_authentication_required(request):
457 # If the client type is confidential or the client was issued client
458 # credentials (or assigned other authentication requirements), the
459 # client MUST authenticate with the authorization server as described
460 # in Section 3.2.1.
461 # https://tools.ietf.org/html/rfc6749#section-3.2.1
462 if not self.request_validator.authenticate_client(request):
463 log.debug('Client authentication failed, %r.', request)
464 raise errors.InvalidClientError(request=request)
465 elif not self.request_validator.authenticate_client_id(request.client_id, request):
466 # REQUIRED, if the client is not authenticating with the
467 # authorization server as described in Section 3.2.1.
468 # https://tools.ietf.org/html/rfc6749#section-3.2.1
469 log.debug('Client authentication failed, %r.', request)
470 raise errors.InvalidClientError(request=request)
472 if not hasattr(request.client, 'client_id'):
473 raise NotImplementedError('Authenticate client must set the '
474 'request.client.client_id attribute '
475 'in authenticate_client.')
477 request.client_id = request.client_id or request.client.client_id
479 # Ensure client is authorized use of this grant type
480 self.validate_grant_type(request)
482 # REQUIRED. The authorization code received from the
483 # authorization server.
484 if not self.request_validator.validate_code(request.client_id,
485 request.code, request.client, request):
486 log.debug('Client, %r (%r), is not allowed access to scopes %r.',
487 request.client_id, request.client, request.scopes)
488 raise errors.InvalidGrantError(request=request)
490 # OPTIONAL. Validate PKCE code_verifier
491 challenge = self.request_validator.get_code_challenge(request.code, request)
493 if challenge is not None:
494 if request.code_verifier is None:
495 raise errors.MissingCodeVerifierError(request=request)
497 challenge_method = self.request_validator.get_code_challenge_method(request.code, request)
498 if challenge_method is None:
499 raise errors.InvalidGrantError(request=request, description="Challenge method not found")
501 if challenge_method not in self._code_challenge_methods:
502 raise errors.ServerError(
503 description="code_challenge_method {} is not supported.".format(challenge_method),
504 request=request
505 )
507 if not self.validate_code_challenge(challenge,
508 challenge_method,
509 request.code_verifier):
510 log.debug('request provided a invalid code_verifier.')
511 raise errors.InvalidGrantError(request=request)
512 elif self.request_validator.is_pkce_required(request.client_id, request) is True:
513 if request.code_verifier is None:
514 raise errors.MissingCodeVerifierError(request=request)
515 raise errors.InvalidGrantError(request=request, description="Challenge not found")
517 for attr in ('user', 'scopes'):
518 if getattr(request, attr, None) is None:
519 log.debug('request.%s was not set on code validation.', attr)
521 # REQUIRED, if the "redirect_uri" parameter was included in the
522 # authorization request as described in Section 4.1.1, and their
523 # values MUST be identical.
524 if request.redirect_uri is None:
525 request.using_default_redirect_uri = True
526 request.redirect_uri = self.request_validator.get_default_redirect_uri(
527 request.client_id, request)
528 log.debug('Using default redirect_uri %s.', request.redirect_uri)
529 if not request.redirect_uri:
530 raise errors.MissingRedirectURIError(request=request)
531 else:
532 request.using_default_redirect_uri = False
533 log.debug('Using provided redirect_uri %s', request.redirect_uri)
535 if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
536 request.redirect_uri, request.client,
537 request):
538 log.debug('Redirect_uri (%r) invalid for client %r (%r).',
539 request.redirect_uri, request.client_id, request.client)
540 raise errors.MismatchingRedirectURIError(request=request)
542 for validator in self.custom_validators.post_token:
543 validator(request)
545 def validate_code_challenge(self, challenge, challenge_method, verifier):
546 if challenge_method in self._code_challenge_methods:
547 return self._code_challenge_methods[challenge_method](verifier, challenge)
548 raise NotImplementedError('Unknown challenge_method %s' % challenge_method)