1"""
2oauthlib.oauth2.rfc6749.grant_types
3~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4"""
5import base64
6import hashlib
7import json
8import logging
9
10from oauthlib import common
11
12from .. import errors
13from .base import GrantTypeBase
14
15log = logging.getLogger(__name__)
16
17
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.:
23
24 BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
25
26 How to implement a base64url-encoding
27 function without padding, based upon the standard base64-encoding
28 function that uses padding.
29
30 To be concrete, example C# code implementing these functions is shown
31 below. Similar code could be used in other languages.
32
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 }
41
42 In python urlsafe_b64encode is already replacing '+' and '/', but preserve
43 the trailing '='. So we have to remove it.
44
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
50
51
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.:
56
57 code_verifier == code_challenge.
58
59 .. _`Section 4.3`: https://tools.ietf.org/html/rfc7636#section-4.3
60 """
61 return verifier == challenge
62
63
64class AuthorizationCodeGrant(GrantTypeBase):
65
66 """`Authorization Code Grant`_
67
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::
74
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)
100
101 Note: The lines illustrating steps (A), (B), and (C) are broken into
102 two parts as they pass through the user-agent.
103
104 Figure 3: Authorization Code Flow
105
106 The flow illustrated in Figure 3 includes the following steps:
107
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).
113
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.
117
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.
124
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.
131
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.
137
138 OAuth 2.0 public clients utilizing the Authorization Code Grant are
139 susceptible to the authorization code interception attack.
140
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.
144
145 .. _`Authorization Code Grant`: https://tools.ietf.org/html/rfc6749#section-4.1
146 .. _`PKCE`: https://tools.ietf.org/html/rfc7636
147 """
148
149 default_response_mode = 'query'
150 response_types = ['code']
151
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 }
159
160 def create_authorization_code(self, request):
161 """
162 Generates an authorization grant represented as a dictionary.
163
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
173
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`_:
179
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`_.
198
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.
202
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.
209
210 A few examples::
211
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
236
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)
247
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
257
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
271
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)
282
283 def create_token_response(self, request, token_handler):
284 """Validate the authorization code.
285
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.
291
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.
296
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
306
307 token = token_handler.create_token(request, refresh_token=self.refresh_token)
308
309 for modifier in self._token_modifiers:
310 token = modifier(token, token_handler, request)
311
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
317
318 def validate_authorization_request(self, request):
319 """Check the authorization request for normal and fatal errors.
320
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.
325
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.
330
331 :param request: OAuthlib request.
332 :type request: oauthlib.common.Request
333 """
334
335 # First check for fatal errors
336
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.
342
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)
351
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)
356
357 if not self.request_validator.validate_client_id(request.client_id, request):
358 raise errors.InvalidClientIdError(request=request)
359
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)
364
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)
368
369 # Then check for normal errors.
370
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
377
378 # Note that the correct parameters to be added are automatically
379 # populated through the use of specific exceptions.
380
381 request_info = {}
382 for validator in self.custom_validators.pre_auth:
383 request_info.update(validator(request))
384
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 'code' not in request.response_type and request.response_type != 'none':
391 raise errors.UnsupportedResponseTypeError(request=request)
392
393 if not self.request_validator.validate_response_type(request.client_id,
394 request.response_type,
395 request.client, request):
396
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)
400
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 and request.code_challenge is None:
404 raise errors.MissingCodeChallengeError(request=request)
405
406 if request.code_challenge is not None:
407 request_info["code_challenge"] = request.code_challenge
408
409 # OPTIONAL, defaults to "plain" if not present in the request.
410 if request.code_challenge_method is None:
411 request.code_challenge_method = "plain"
412
413 if request.code_challenge_method not in self._code_challenge_methods:
414 raise errors.UnsupportedCodeChallengeMethodError(request=request)
415 request_info["code_challenge_method"] = request.code_challenge_method
416
417 # OPTIONAL. The scope of the access request as described by Section 3.3
418 # https://tools.ietf.org/html/rfc6749#section-3.3
419 self.validate_scopes(request)
420
421 request_info.update({
422 'client_id': request.client_id,
423 'redirect_uri': request.redirect_uri,
424 'response_type': request.response_type,
425 'state': request.state,
426 'request': request
427 })
428
429 for validator in self.custom_validators.post_auth:
430 request_info.update(validator(request))
431
432 return request.scopes, request_info
433
434 def validate_token_request(self, request):
435 """
436 :param request: OAuthlib request.
437 :type request: oauthlib.common.Request
438 """
439 # REQUIRED. Value MUST be set to "authorization_code".
440 if request.grant_type not in ('authorization_code', 'openid'):
441 raise errors.UnsupportedGrantTypeError(request=request)
442
443 for validator in self.custom_validators.pre_token:
444 validator(request)
445
446 if request.code is None:
447 raise errors.InvalidRequestError(
448 description='Missing code parameter.', request=request)
449
450 for param in ('client_id', 'grant_type', 'redirect_uri'):
451 if param in request.duplicate_params:
452 raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
453 request=request)
454
455 if self.request_validator.client_authentication_required(request):
456 # If the client type is confidential or the client was issued client
457 # credentials (or assigned other authentication requirements), the
458 # client MUST authenticate with the authorization server as described
459 # in Section 3.2.1.
460 # https://tools.ietf.org/html/rfc6749#section-3.2.1
461 if not self.request_validator.authenticate_client(request):
462 log.debug('Client authentication failed, %r.', request)
463 raise errors.InvalidClientError(request=request)
464 elif not self.request_validator.authenticate_client_id(request.client_id, request):
465 # REQUIRED, if the client is not authenticating with the
466 # authorization server as described in Section 3.2.1.
467 # https://tools.ietf.org/html/rfc6749#section-3.2.1
468 log.debug('Client authentication failed, %r.', request)
469 raise errors.InvalidClientError(request=request)
470
471 if not hasattr(request.client, 'client_id'):
472 raise NotImplementedError('Authenticate client must set the '
473 'request.client.client_id attribute '
474 'in authenticate_client.')
475
476 request.client_id = request.client_id or request.client.client_id
477
478 # Ensure client is authorized use of this grant type
479 self.validate_grant_type(request)
480
481 # REQUIRED. The authorization code received from the
482 # authorization server.
483 if not self.request_validator.validate_code(request.client_id,
484 request.code, request.client, request):
485 log.debug('Client, %r (%r), is not allowed access to scopes %r.',
486 request.client_id, request.client, request.scopes)
487 raise errors.InvalidGrantError(request=request)
488
489 # OPTIONAL. Validate PKCE code_verifier
490 challenge = self.request_validator.get_code_challenge(request.code, request)
491
492 if challenge is not None:
493 if request.code_verifier is None:
494 raise errors.MissingCodeVerifierError(request=request)
495
496 challenge_method = self.request_validator.get_code_challenge_method(request.code, request)
497 if challenge_method is None:
498 raise errors.InvalidGrantError(request=request, description="Challenge method not found")
499
500 if challenge_method not in self._code_challenge_methods:
501 raise errors.ServerError(
502 description="code_challenge_method {} is not supported.".format(challenge_method),
503 request=request
504 )
505
506 if not self.validate_code_challenge(challenge,
507 challenge_method,
508 request.code_verifier):
509 log.debug('request provided a invalid code_verifier.')
510 raise errors.InvalidGrantError(request=request)
511 elif self.request_validator.is_pkce_required(request.client_id, request) is True:
512 if request.code_verifier is None:
513 raise errors.MissingCodeVerifierError(request=request)
514 raise errors.InvalidGrantError(request=request, description="Challenge not found")
515
516 for attr in ('user', 'scopes'):
517 if getattr(request, attr, None) is None:
518 log.debug('request.%s was not set on code validation.', attr)
519
520 # REQUIRED, if the "redirect_uri" parameter was included in the
521 # authorization request as described in Section 4.1.1, and their
522 # values MUST be identical.
523 if request.redirect_uri is None:
524 request.using_default_redirect_uri = True
525 request.redirect_uri = self.request_validator.get_default_redirect_uri(
526 request.client_id, request)
527 log.debug('Using default redirect_uri %s.', request.redirect_uri)
528 if not request.redirect_uri:
529 raise errors.MissingRedirectURIError(request=request)
530 else:
531 request.using_default_redirect_uri = False
532 log.debug('Using provided redirect_uri %s', request.redirect_uri)
533
534 if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
535 request.redirect_uri, request.client,
536 request):
537 log.debug('Redirect_uri (%r) invalid for client %r (%r).',
538 request.redirect_uri, request.client_id, request.client)
539 raise errors.MismatchingRedirectURIError(request=request)
540
541 for validator in self.custom_validators.post_token:
542 validator(request)
543
544 def validate_code_challenge(self, challenge, challenge_method, verifier):
545 if challenge_method in self._code_challenge_methods:
546 return self._code_challenge_methods[challenge_method](verifier, challenge)
547 raise NotImplementedError('Unknown challenge_method %s' % challenge_method)