Merge pull request #3 from guyrt/master
[twitter-api-cdsw] / oauthlib / oauth2 / rfc6749 / grant_types / authorization_code.py
1 # -*- coding: utf-8 -*-
2 """
3 oauthlib.oauth2.rfc6749.grant_types
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 """
6 from __future__ import unicode_literals, absolute_import
7
8 import json
9 import logging
10
11 from oauthlib import common
12 from oauthlib.uri_validate import is_absolute_uri
13
14 from .base import GrantTypeBase
15 from .. import errors
16 from ..request_validator import RequestValidator
17
18 log = logging.getLogger(__name__)
19
20
21 class AuthorizationCodeGrant(GrantTypeBase):
22
23     """`Authorization Code Grant`_
24
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::
31
32         +----------+
33         | Resource |
34         |   Owner  |
35         |          |
36         +----------+
37              ^
38              |
39             (B)
40         +----|-----+          Client Identifier      +---------------+
41         |         -+----(A)-- & Redirection URI ---->|               |
42         |  User-   |                                 | Authorization |
43         |  Agent  -+----(B)-- User authenticates --->|     Server    |
44         |          |                                 |               |
45         |         -+----(C)-- Authorization Code ---<|               |
46         +-|----|---+                                 +---------------+
47           |    |                                         ^      v
48          (A)  (C)                                        |      |
49           |    |                                         |      |
50           ^    v                                         |      |
51         +---------+                                      |      |
52         |         |>---(D)-- Authorization Code ---------'      |
53         |  Client |          & Redirection URI                  |
54         |         |                                             |
55         |         |<---(E)----- Access Token -------------------'
56         +---------+       (w/ Optional Refresh Token)
57
58     Note: The lines illustrating steps (A), (B), and (C) are broken into
59     two parts as they pass through the user-agent.
60
61     Figure 3: Authorization Code Flow
62
63     The flow illustrated in Figure 3 includes the following steps:
64
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).
70
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.
74
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
80          earlier.
81
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.
88
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.
94
95     .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1
96     """
97
98     def __init__(self, request_validator=None):
99         self.request_validator = request_validator or RequestValidator()
100
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.',
107                   grant, request)
108         return grant
109
110     def create_authorization_response(self, request, token_handler):
111         """
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`_:
115
116         response_type
117                 REQUIRED.  Value MUST be set to "code".
118         client_id
119                 REQUIRED.  The client identifier as described in `Section 2.2`_.
120         redirect_uri
121                 OPTIONAL.  As described in `Section 3.1.2`_.
122         scope
123                 OPTIONAL.  The scope of the access request as described by
124                 `Section 3.3`_.
125         state
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`_.
131
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
134         user-agent.
135
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.
142
143         A few examples::
144
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
175
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
181         """
182         try:
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.')
187
188             self.validate_authorization_request(request)
189             log.debug('Pre resource owner authorization validation ok for %r.',
190                       request)
191
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.',
199                       request, e)
200             raise
201
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
212
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
218
219     def create_token_response(self, request, token_handler):
220         """Validate the authorization code.
221
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.
227         """
228         headers = {
229             'Content-Type': 'application/json',
230             'Cache-Control': 'no-store',
231             'Pragma': 'no-cache',
232         }
233         try:
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
239
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
244
245     def validate_authorization_request(self, request):
246         """Check the authorization request for normal and fatal errors.
247
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.
252
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.
257         """
258
259         # First check for fatal errors
260
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.
266
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)
271
272         if not self.request_validator.validate_client_id(request.client_id, request):
273             raise errors.InvalidClientIdError(request=request)
274
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)
284
285             if not self.request_validator.validate_redirect_uri(
286                     request.client_id, request.redirect_uri, request):
287                 raise errors.MismatchingRedirectURIError(request=request)
288         else:
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)
295
296         # Then check for normal errors.
297
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
304
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)
309
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)
313
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)
319
320         # REQUIRED. Value MUST be set to "code".
321         if request.response_type != 'code':
322             raise errors.UnsupportedResponseTypeError(request=request)
323
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)
327
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,
333             'request': request,
334         }
335
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)
340
341         if request.code is None:
342             raise errors.InvalidRequestError(
343                 description='Missing code parameter.', request=request)
344
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,
348                                                  request=request)
349
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
354             # in Section 3.2.1.
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)
365
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.')
370
371         # Ensure client is authorized use of this grant type
372         self.validate_grant_type(request)
373
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)
381
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)
385
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)

Benjamin Mako Hill || Want to submit a patch?