1 # -*- coding: utf-8 -*-
3 oauthlib.oauth2.rfc6749.grant_types
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
6 from __future__ import unicode_literals, absolute_import
11 from oauthlib import common
12 from oauthlib.uri_validate import is_absolute_uri
14 from .base import GrantTypeBase
16 from ..request_validator import RequestValidator
18 log = logging.getLogger(__name__)
21 class AuthorizationCodeGrant(GrantTypeBase):
23 """`Authorization Code Grant`_
25 The authorization code grant type is used to obtain both access
26 tokens and refresh tokens and is optimized for confidential clients.
27 Since this is a redirection-based flow, the client must be capable of
28 interacting with the resource owner's user-agent (typically a web
29 browser) and capable of receiving incoming requests (via redirection)
30 from the authorization server::
40 +----|-----+ Client Identifier +---------------+
41 | -+----(A)-- & Redirection URI ---->| |
42 | User- | | Authorization |
43 | Agent -+----(B)-- User authenticates --->| Server |
45 | -+----(C)-- Authorization Code ---<| |
46 +-|----|---+ +---------------+
52 | |>---(D)-- Authorization Code ---------' |
53 | Client | & Redirection URI |
55 | |<---(E)----- Access Token -------------------'
56 +---------+ (w/ Optional Refresh Token)
58 Note: The lines illustrating steps (A), (B), and (C) are broken into
59 two parts as they pass through the user-agent.
61 Figure 3: Authorization Code Flow
63 The flow illustrated in Figure 3 includes the following steps:
65 (A) The client initiates the flow by directing the resource owner's
66 user-agent to the authorization endpoint. The client includes
67 its client identifier, requested scope, local state, and a
68 redirection URI to which the authorization server will send the
69 user-agent back once access is granted (or denied).
71 (B) The authorization server authenticates the resource owner (via
72 the user-agent) and establishes whether the resource owner
73 grants or denies the client's access request.
75 (C) Assuming the resource owner grants access, the authorization
76 server redirects the user-agent back to the client using the
77 redirection URI provided earlier (in the request or during
78 client registration). The redirection URI includes an
79 authorization code and any local state provided by the client
82 (D) The client requests an access token from the authorization
83 server's token endpoint by including the authorization code
84 received in the previous step. When making the request, the
85 client authenticates with the authorization server. The client
86 includes the redirection URI used to obtain the authorization
87 code for verification.
89 (E) The authorization server authenticates the client, validates the
90 authorization code, and ensures that the redirection URI
91 received matches the URI used to redirect the client in
92 step (C). If valid, the authorization server responds back with
93 an access token and, optionally, a refresh token.
95 .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1
98 def __init__(self, request_validator=None):
99 self.request_validator = request_validator or RequestValidator()
101 def create_authorization_code(self, request):
102 """Generates an authorization grant represented as a dictionary."""
103 grant = {'code': common.generate_token()}
104 if hasattr(request, 'state') and request.state:
105 grant['state'] = request.state
106 log.debug('Created authorization code grant %r for request %r.',
110 def create_authorization_response(self, request, token_handler):
112 The client constructs the request URI by adding the following
113 parameters to the query component of the authorization endpoint URI
114 using the "application/x-www-form-urlencoded" format, per `Appendix B`_:
117 REQUIRED. Value MUST be set to "code".
119 REQUIRED. The client identifier as described in `Section 2.2`_.
121 OPTIONAL. As described in `Section 3.1.2`_.
123 OPTIONAL. The scope of the access request as described by
126 RECOMMENDED. An opaque value used by the client to maintain
127 state between the request and callback. The authorization
128 server includes this value when redirecting the user-agent back
129 to the client. The parameter SHOULD be used for preventing
130 cross-site request forgery as described in `Section 10.12`_.
132 The client directs the resource owner to the constructed URI using an
133 HTTP redirection response, or by other means available to it via the
136 :param request: oauthlib.commong.Request
137 :param token_handler: A token handler instace, for example of type
138 oauthlib.oauth2.BearerToken.
139 :returns: headers, body, status
140 :raises: FatalClientError on invalid redirect URI or client id.
141 ValueError if scopes are not set on the request object.
145 >>> from your_validator import your_validator
146 >>> request = Request('https://example.com/authorize?client_id=valid'
147 ... '&redirect_uri=http%3A%2F%2Fclient.com%2F')
148 >>> from oauthlib.common import Request
149 >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken
150 >>> token = BearerToken(your_validator)
151 >>> grant = AuthorizationCodeGrant(your_validator)
152 >>> grant.create_authorization_response(request, token)
153 Traceback (most recent call last):
154 File "<stdin>", line 1, in <module>
155 File "oauthlib/oauth2/rfc6749/grant_types.py", line 513, in create_authorization_response
156 raise ValueError('Scopes must be set on post auth.')
157 ValueError: Scopes must be set on post auth.
158 >>> request.scopes = ['authorized', 'in', 'some', 'form']
159 >>> grant.create_authorization_response(request, token)
160 (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400)
161 >>> request = Request('https://example.com/authorize?client_id=valid'
162 ... '&redirect_uri=http%3A%2F%2Fclient.com%2F'
163 ... '&response_type=code')
164 >>> request.scopes = ['authorized', 'in', 'some', 'form']
165 >>> grant.create_authorization_response(request, token)
166 (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200)
167 >>> # If the client id or redirect uri fails validation
168 >>> grant.create_authorization_response(request, token)
169 Traceback (most recent call last):
170 File "<stdin>", line 1, in <module>
171 File "oauthlib/oauth2/rfc6749/grant_types.py", line 515, in create_authorization_response
172 >>> grant.create_authorization_response(request, token)
173 File "oauthlib/oauth2/rfc6749/grant_types.py", line 591, in validate_authorization_request
174 oauthlib.oauth2.rfc6749.errors.InvalidClientIdError
176 .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B
177 .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2
178 .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2
179 .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
180 .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12
183 # request.scopes is only mandated in post auth and both pre and
184 # post auth use validate_authorization_request
185 if not request.scopes:
186 raise ValueError('Scopes must be set on post auth.')
188 self.validate_authorization_request(request)
189 log.debug('Pre resource owner authorization validation ok for %r.',
192 # If the request fails due to a missing, invalid, or mismatching
193 # redirection URI, or if the client identifier is missing or invalid,
194 # the authorization server SHOULD inform the resource owner of the
195 # error and MUST NOT automatically redirect the user-agent to the
196 # invalid redirection URI.
197 except errors.FatalClientError as e:
198 log.debug('Fatal client error during validation of %r. %r.',
202 # If the resource owner denies the access request or if the request
203 # fails for reasons other than a missing or invalid redirection URI,
204 # the authorization server informs the client by adding the following
205 # parameters to the query component of the redirection URI using the
206 # "application/x-www-form-urlencoded" format, per Appendix B:
207 # http://tools.ietf.org/html/rfc6749#appendix-B
208 except errors.OAuth2Error as e:
209 log.debug('Client error during validation of %r. %r.', request, e)
210 request.redirect_uri = request.redirect_uri or self.error_uri
211 return {'Location': common.add_params_to_uri(request.redirect_uri, e.twotuples)}, None, 302
213 grant = self.create_authorization_code(request)
214 log.debug('Saving grant %r for %r.', grant, request)
215 self.request_validator.save_authorization_code(
216 request.client_id, grant, request)
217 return {'Location': common.add_params_to_uri(request.redirect_uri, grant.items())}, None, 302
219 def create_token_response(self, request, token_handler):
220 """Validate the authorization code.
222 The client MUST NOT use the authorization code more than once. If an
223 authorization code is used more than once, the authorization server
224 MUST deny the request and SHOULD revoke (when possible) all tokens
225 previously issued based on that authorization code. The authorization
226 code is bound to the client identifier and redirection URI.
229 'Content-Type': 'application/json',
230 'Cache-Control': 'no-store',
231 'Pragma': 'no-cache',
234 self.validate_token_request(request)
235 log.debug('Token request validation ok for %r.', request)
236 except errors.OAuth2Error as e:
237 log.debug('Client error during validation of %r. %r.', request, e)
238 return headers, e.json, e.status_code
240 token = token_handler.create_token(request, refresh_token=True)
241 self.request_validator.invalidate_authorization_code(
242 request.client_id, request.code, request)
243 return headers, json.dumps(token), 200
245 def validate_authorization_request(self, request):
246 """Check the authorization request for normal and fatal errors.
248 A normal error could be a missing response_type parameter or the client
249 attempting to access scope it is not allowed to ask authorization for.
250 Normal errors can safely be included in the redirection URI and
251 sent back to the client.
253 Fatal errors occur when the client_id or redirect_uri is invalid or
254 missing. These must be caught by the provider and handled, how this
255 is done is outside of the scope of OAuthLib but showing an error
256 page describing the issue is a good idea.
259 # First check for fatal errors
261 # If the request fails due to a missing, invalid, or mismatching
262 # redirection URI, or if the client identifier is missing or invalid,
263 # the authorization server SHOULD inform the resource owner of the
264 # error and MUST NOT automatically redirect the user-agent to the
265 # invalid redirection URI.
267 # REQUIRED. The client identifier as described in Section 2.2.
268 # http://tools.ietf.org/html/rfc6749#section-2.2
269 if not request.client_id:
270 raise errors.MissingClientIdError(request=request)
272 if not self.request_validator.validate_client_id(request.client_id, request):
273 raise errors.InvalidClientIdError(request=request)
275 # OPTIONAL. As described in Section 3.1.2.
276 # http://tools.ietf.org/html/rfc6749#section-3.1.2
277 log.debug('Validating redirection uri %s for client %s.',
278 request.redirect_uri, request.client_id)
279 if request.redirect_uri is not None:
280 request.using_default_redirect_uri = False
281 log.debug('Using provided redirect_uri %s', request.redirect_uri)
282 if not is_absolute_uri(request.redirect_uri):
283 raise errors.InvalidRedirectURIError(request=request)
285 if not self.request_validator.validate_redirect_uri(
286 request.client_id, request.redirect_uri, request):
287 raise errors.MismatchingRedirectURIError(request=request)
289 request.redirect_uri = self.request_validator.get_default_redirect_uri(
290 request.client_id, request)
291 request.using_default_redirect_uri = True
292 log.debug('Using default redirect_uri %s.', request.redirect_uri)
293 if not request.redirect_uri:
294 raise errors.MissingRedirectURIError(request=request)
296 # Then check for normal errors.
298 # If the resource owner denies the access request or if the request
299 # fails for reasons other than a missing or invalid redirection URI,
300 # the authorization server informs the client by adding the following
301 # parameters to the query component of the redirection URI using the
302 # "application/x-www-form-urlencoded" format, per Appendix B.
303 # http://tools.ietf.org/html/rfc6749#appendix-B
305 # Note that the correct parameters to be added are automatically
306 # populated through the use of specific exceptions.
307 if request.response_type is None:
308 raise errors.InvalidRequestError(description='Missing response_type parameter.', request=request)
310 for param in ('client_id', 'response_type', 'redirect_uri', 'scope', 'state'):
311 if param in request.duplicate_params:
312 raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param, request=request)
314 if not self.request_validator.validate_response_type(request.client_id,
315 request.response_type, request.client, request):
316 log.debug('Client %s is not authorized to use response_type %s.',
317 request.client_id, request.response_type)
318 raise errors.UnauthorizedClientError(request=request)
320 # REQUIRED. Value MUST be set to "code".
321 if request.response_type != 'code':
322 raise errors.UnsupportedResponseTypeError(request=request)
324 # OPTIONAL. The scope of the access request as described by Section 3.3
325 # http://tools.ietf.org/html/rfc6749#section-3.3
326 self.validate_scopes(request)
328 return request.scopes, {
329 'client_id': request.client_id,
330 'redirect_uri': request.redirect_uri,
331 'response_type': request.response_type,
332 'state': request.state,
336 def validate_token_request(self, request):
337 # REQUIRED. Value MUST be set to "authorization_code".
338 if request.grant_type != 'authorization_code':
339 raise errors.UnsupportedGrantTypeError(request=request)
341 if request.code is None:
342 raise errors.InvalidRequestError(
343 description='Missing code parameter.', request=request)
345 for param in ('client_id', 'grant_type', 'redirect_uri'):
346 if param in request.duplicate_params:
347 raise errors.InvalidRequestError(description='Duplicate %s parameter.' % param,
350 if self.request_validator.client_authentication_required(request):
351 # If the client type is confidential or the client was issued client
352 # credentials (or assigned other authentication requirements), the
353 # client MUST authenticate with the authorization server as described
355 # http://tools.ietf.org/html/rfc6749#section-3.2.1
356 if not self.request_validator.authenticate_client(request):
357 log.debug('Client authentication failed, %r.', request)
358 raise errors.InvalidClientError(request=request)
359 elif not self.request_validator.authenticate_client_id(request.client_id, request):
360 # REQUIRED, if the client is not authenticating with the
361 # authorization server as described in Section 3.2.1.
362 # http://tools.ietf.org/html/rfc6749#section-3.2.1
363 log.debug('Client authentication failed, %r.', request)
364 raise errors.InvalidClientError(request=request)
366 if not hasattr(request.client, 'client_id'):
367 raise NotImplementedError('Authenticate client must set the '
368 'request.client.client_id attribute '
369 'in authenticate_client.')
371 # Ensure client is authorized use of this grant type
372 self.validate_grant_type(request)
374 # REQUIRED. The authorization code received from the
375 # authorization server.
376 if not self.request_validator.validate_code(request.client_id,
377 request.code, request.client, request):
378 log.debug('Client, %r (%r), is not allowed access to scopes %r.',
379 request.client_id, request.client, request.scopes)
380 raise errors.InvalidGrantError(request=request)
382 for attr in ('user', 'state', 'scopes'):
383 if getattr(request, attr) is None:
384 log.debug('request.%s was not set on code validation.', attr)
386 # REQUIRED, if the "redirect_uri" parameter was included in the
387 # authorization request as described in Section 4.1.1, and their
388 # values MUST be identical.
389 if not self.request_validator.confirm_redirect_uri(request.client_id, request.code,
390 request.redirect_uri, request.client):
391 log.debug('Redirect_uri (%r) invalid for client %r (%r).',
392 request.redirect_uri, request.client_id, request.client)
393 raise errors.AccessDeniedError(request=request)