diff options
30 files changed, 2008 insertions, 690 deletions
diff --git a/docs/authcode.rst b/docs/authcode.rst new file mode 100644 index 0000000..35f1dad --- /dev/null +++ b/docs/authcode.rst @@ -0,0 +1,7 @@ +Authorization Code Grant +------------------------ + +TODO: describe on a high level what the grant is and when it is useful + +.. autoclass:: oauthlib.oauth2.draft25.grant_types.AuthorizationCodeGrant + :members: diff --git a/docs/backendapplicationclient.rst b/docs/backendapplicationclient.rst new file mode 100644 index 0000000..d4c92c0 --- /dev/null +++ b/docs/backendapplicationclient.rst @@ -0,0 +1,5 @@ +Client Credentials Grant flow (BackendApplicationClient) +-------------------------------------------------------- + +.. autoclass:: oauthlib.oauth2.draft25.BackendApplicationClient + :members: diff --git a/docs/client.rst b/docs/client.rst index 90e607c..dafb6e3 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -1,121 +1,114 @@ ========================= -OAuth 1 Client (RFC 5849) +OAuth 1: Using the Client ========================= -Are you using requests? ------------------------ +**Are you using requests?** -If you then you should take a look at `requests-oauthlib`_ which has several examples of how to use OAuth1 with requests. + If you then you should take a look at `requests-oauthlib`_ which has several examples of how to use OAuth1 with requests. -.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib + .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib -Signing a request with an HMAC-SHA1 signature (most common) ------------------------------------------------------------ +**Signing a request with an HMAC-SHA1 signature (most common)** -See `requests-oauthlib`_ for more detailed examples of going through the OAuth workflow. In a nutshell you will be doing three types of requests, to obtain a request token, to obtain an access token and to access a protected resource. + See `requests-oauthlib`_ for more detailed examples of going through the OAuth workflow. In a nutshell you will be doing three types of requests, to obtain a request token, to obtain an access token and to access a protected resource. -Obtaining a request token will require client key and secret which are provided to you when registering a client with the OAuth provider:: + Obtaining a request token will require client key and secret which are provided to you when registering a client with the OAuth provider:: - client = oauthlib.oauth1.Client('client_key', client_secret='your_secret') - uri, headers, body = client.sign('http://example.com/request_token') + client = oauthlib.oauth1.Client('client_key', client_secret='your_secret') + uri, headers, body = client.sign('http://example.com/request_token') -You will then need to redirect to the authorization page of the OAuth provider, which will later redirect back with a verifier and a token secret parameter appended to your callback url. These will be used in addition to the credentials from before when obtaining an access token:: + You will then need to redirect to the authorization page of the OAuth provider, which will later redirect back with a verifier and a token secret parameter appended to your callback url. These will be used in addition to the credentials from before when obtaining an access token:: - client = oauthlib.oauth1.Client('client_key', client_secret='your_secret', - resource_owner_secret='the_new_secret', verifier='the_verifier') - uri, headers, body = client.sign('http://example.com/access_token') + client = oauthlib.oauth1.Client('client_key', client_secret='your_secret', + resource_owner_secret='the_new_secret', verifier='the_verifier') + uri, headers, body = client.sign('http://example.com/access_token') -The provider will now give you an access token and a new token secret which you will use to access protected resources:: + The provider will now give you an access token and a new token secret which you will use to access protected resources:: - client = oauthlib.oauth1.Client('client_key', client_secret='your_secret', - resource_owner_key='the_access_token', resource_owner_secret='the_token_secret') - uri, headers, body = client.sign('http://example.com/access_token') + client = oauthlib.oauth1.Client('client_key', client_secret='your_secret', + resource_owner_key='the_access_token', resource_owner_secret='the_token_secret') + uri, headers, body = client.sign('http://example.com/access_token') -.. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib + .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib -Unicode Everywhere ------------------- +**Unicode Everywhere** -Starting with 0.3.5 OAuthLib supports automatic conversion to unicode if you supply input in utf-8 encoding. If you are using another encoding you will have to make sure to convert all input to unicode before passing it to OAuthLib. Note that the automatic conversion is limited to the use of oauthlib.oauth1.Client. + Starting with 0.3.5 OAuthLib supports automatic conversion to unicode if you supply input in utf-8 encoding. If you are using another encoding you will have to make sure to convert all input to unicode before passing it to OAuthLib. Note that the automatic conversion is limited to the use of oauthlib.oauth1.Client. -Request body ------------- +**Request body** -The OAuth 1 spec only covers signing of x-www-url-formencoded information. If -you are sending some other kind of data in the body (say, multipart file uploads), -these don't count as a body for the purposes of signing. Don't provide the body -to Client.sign() if it isn't x-www-url-formencoded data. + The OAuth 1 spec only covers signing of x-www-url-formencoded information. If + you are sending some other kind of data in the body (say, multipart file uploads), + these don't count as a body for the purposes of signing. Don't provide the body + to Client.sign() if it isn't x-www-url-formencoded data. -For convenience, you can pass body data in one of three ways: + For convenience, you can pass body data in one of three ways: -* a dictionary -* an iterable of 2-tuples -* a properly-formated x-www-url-formencoded string + * a dictionary + * an iterable of 2-tuples + * a properly-formated x-www-url-formencoded string -RSA Signatures --------------- +**RSA Signatures** -OAuthLib supports the 'RSA-SHA1' signature but does not install the PyCrypto dependency by default. This is not done because PyCrypto is fairly cumbersome to install, especially on Windows. Linux and Mac OS X (?) users can install PyCrypto using pip:: + OAuthLib supports the 'RSA-SHA1' signature but does not install the PyCrypto dependency by default. This is not done because PyCrypto is fairly cumbersome to install, especially on Windows. Linux and Mac OS X (?) users can install PyCrypto using pip:: - pip install pycrypto + pip install pycrypto -Windows users will have to jump through a few hoops. The following links may be helpful: + Windows users will have to jump through a few hoops. The following links may be helpful: -* `Voidspace Python prebuilt binaries for PyCrypto <http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_ + * `Voidspace Python prebuilt binaries for PyCrypto <http://www.voidspace.org.uk/python/modules.shtml#pycrypto>`_ -* `Can I install Python Windows packages into virtualenvs <http://stackoverflow.com/questions/3271590/can-i-install-python-windows-packages-into-virtualenvs>`_ + * `Can I install Python Windows packages into virtualenvs <http://stackoverflow.com/questions/3271590/can-i-install-python-windows-packages-into-virtualenvs>`_ -* `Compiling pycrypto on Windows 7 (64bit) <http://yorickdowne.wordpress.com/2010/12/22/compiling-pycrypto-on-win7-64/>`_ + * `Compiling pycrypto on Windows 7 (64bit) <http://yorickdowne.wordpress.com/2010/12/22/compiling-pycrypto-on-win7-64/>`_ -When you have pycrypto installed using RSA signatures is similar to HMAC but differ in a few aspects. RSA signatures does not make use of client secrets nor resource owner secrets (token secrets) and requires you to specify the signature type when constructing a client:: + When you have pycrypto installed using RSA signatures is similar to HMAC but differ in a few aspects. RSA signatures does not make use of client secrets nor resource owner secrets (token secrets) and requires you to specify the signature type when constructing a client:: - client = oauthlib.oauth1.Client('your client key', - signature_method=oauthlib.oauth1.SIGNATURE_RSA, - resource_owner_key='a token you have obtained', - rsa_key=open('your_private_key.pem').read()) + client = oauthlib.oauth1.Client('your client key', + signature_method=oauthlib.oauth1.SIGNATURE_RSA, + resource_owner_key='a token you have obtained', + rsa_key=open('your_private_key.pem').read()) -Plaintext signatures --------------------- +**Plaintext signatures** -OAuthLib supports plaintext signatures and they are identical in use to HMAC-SHA1 signatures except that you will need to set the signature_method when constructing Clients:: + OAuthLib supports plaintext signatures and they are identical in use to HMAC-SHA1 signatures except that you will need to set the signature_method when constructing Clients:: - client = oauthlib.oauth1.Client('your client key', - client_secret='your secret', - resource_owner_key='a token you have obtained', - resource_owner_secret='a token secret', - signature_method=oauthlib.oauth1.SIGNATURE_PLAINTEXT) + client = oauthlib.oauth1.Client('your client key', + client_secret='your secret', + resource_owner_key='a token you have obtained', + resource_owner_secret='a token secret', + signature_method=oauthlib.oauth1.SIGNATURE_PLAINTEXT) -Where to put the signature? Signature types -------------------------------------------- +**Where to put the signature? Signature types** -OAuth 1 commonly use the Authorization header to pass the OAuth signature and other OAuth parameters. This is the default setting in Client and need not be specified. However you may also use the request url query or the request body to pass the parameters. You can specify this location using the signature_type constructor parameter, as shown below:: + OAuth 1 commonly use the Authorization header to pass the OAuth signature and other OAuth parameters. This is the default setting in Client and need not be specified. However you may also use the request url query or the request body to pass the parameters. You can specify this location using the signature_type constructor parameter, as shown below:: - # Embed in Authorization header (recommended) - client = oauthlib.oauth1.Client('client_key', - signature_type=SIGNATURE_TYPE_AUTH_HEADER, - ) + # Embed in Authorization header (recommended) + client = oauthlib.oauth1.Client('client_key', + signature_type=SIGNATURE_TYPE_AUTH_HEADER, + ) - >>> uri, headers, body = client.sign('http://example.com/path?query=hello') - >>> headers - {u'Authorization': u'OAuth oauth_nonce="107143098223781054691360095427", oauth_timestamp="1360095427", oauth_version="1.0", oauth_signature_method="HMAC-SHA1", oauth_consumer_key="client_key", oauth_signature="86gpxY1DUXSBRRyWnRNJekeWEzw%3D"'} + >>> uri, headers, body = client.sign('http://example.com/path?query=hello') + >>> headers + {u'Authorization': u'OAuth oauth_nonce="107143098223781054691360095427", oauth_timestamp="1360095427", oauth_version="1.0", oauth_signature_method="HMAC-SHA1", oauth_consumer_key="client_key", oauth_signature="86gpxY1DUXSBRRyWnRNJekeWEzw%3D"'} - # Embed in url query - client = oauthlib.oauth1.Client('client_key', - signature_type=SIGNATURE_TYPE_QUERY, - ) - >>> uri, headers, body = client.sign('http://example.com/path?query=hello') - >>> uri - http://example.com/path?query=hello&oauth_nonce=97599600646423262881360095509&oauth_timestamp=1360095509&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=client_key&oauth_signature=VQAib%2F4uRPwfVmCZkgSE3q2p7zU%3D - - # Embed in body - client = oauthlib.oauth1.Client('client_key', - signature_type=SIGNATURE_TYPE_BODY, - ) + # Embed in url query + client = oauthlib.oauth1.Client('client_key', + signature_type=SIGNATURE_TYPE_QUERY, + ) + >>> uri, headers, body = client.sign('http://example.com/path?query=hello') + >>> uri + http://example.com/path?query=hello&oauth_nonce=97599600646423262881360095509&oauth_timestamp=1360095509&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=client_key&oauth_signature=VQAib%2F4uRPwfVmCZkgSE3q2p7zU%3D + + # Embed in body + client = oauthlib.oauth1.Client('client_key', + signature_type=SIGNATURE_TYPE_BODY, + ) - # Please set content-type to application/x-www-form-urlencoded - >>> headers = {'Authorization':oauthlib.oauth1.CONTENT_TYPE_FORM_URLENCODED} - >>> uri, headers, body = client.sign('http://example.com/path?query=hello', - headers=headers) - >>> body - u'oauth_nonce=148092408248153282511360095722&oauth_timestamp=1360095722&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=client_key&oauth_signature=5IKjrRKU3%2FIduI9UumVI%2FbQ0Hv0%3D' + # Please set content-type to application/x-www-form-urlencoded + >>> headers = {'Authorization':oauthlib.oauth1.CONTENT_TYPE_FORM_URLENCODED} + >>> uri, headers, body = client.sign('http://example.com/path?query=hello', + headers=headers) + >>> body + u'oauth_nonce=148092408248153282511360095722&oauth_timestamp=1360095722&oauth_version=1.0&oauth_signature_method=HMAC-SHA1&oauth_consumer_key=client_key&oauth_signature=5IKjrRKU3%2FIduI9UumVI%2FbQ0Hv0%3D' diff --git a/docs/client2.rst b/docs/client2.rst index 15286ec..005a65c 100644 --- a/docs/client2.rst +++ b/docs/client2.rst @@ -1,5 +1,51 @@ -========================= -OAuth 2 Client (RFC 6749) -========================= +====================== +OAuth 2: Using Clients +====================== -Contributing oauth 2 client usage docs is very very welcome =) +OAuthLib supports all four core grant types defined in the OAuth 2 RFC and +will continue to add more as they are defined. For more information on how +to use them please browse the documentation for each client type below. + +.. toctree:: + :maxdepth: 2 + + webapplicationclient + mobileapplicationclient + legacyapplicationclient + backendapplicationclient + +**A few notes on security** + OAuth 2 is much simpler to implement for clients than OAuth 1 as + cryptographic signing is no longer necessary. Instead a strict + requirement on the use of TLS for all connections have been + introduced:: + + # OAuthLib will raise errors if you attempt to interact with a + # non HTTPS endpoint during authorization. + # However OAuthLib offers no such protection during token requests + # as the URI is not provided, only the request body. + + Note that while OAuth 2 is simpler it does subtly transfer a few important + responsibilities from the provider to the client. Most notably that the client + must ensure that all tokens are kept secret at all times. Access to protected + resources using Bearer tokens provides no authenticity of clients which means + that a malicious party able to obtain your tokens can use them without the + provider being able to know the difference. This is unlike OAuth 1 where a + lost token could not be utilized without the client secret and the token + bound secret, since they are required for the signing of each request:: + + # DO NOT REGISTER A NON-HTTPS REDIRECTION URI + # OAuthLib will raise errors if you attempt to parse a response + # redirect back to a insecure redirection endpoint. + +**Existing libraries** + If you are using the `requests`_ HTTP library you may be interested in using + `requests-oauthlib`_ which provides an OAuth 2 Client. This client removes much + of the boilerplate you might otherwise need to deal with when interacting + with OAuthLib directly. + + If you are interested in integrating OAuth 2 support into your favourite + HTTP library you might find the requests-oauthlib implementation interesting. + + .. _`requests`: https://github.com/kennethreitz/requests + .. _`requests-oauthlib`: https://github.com/requests/requests-oauthlib diff --git a/docs/credentials.rst b/docs/credentials.rst new file mode 100644 index 0000000..33bcb7b --- /dev/null +++ b/docs/credentials.rst @@ -0,0 +1,7 @@ +Client Credentials Grant +------------------------ + +TODO: describe on a high level what the grant is and when it is useful + +.. autoclass:: oauthlib.oauth2.draft25.grant_types.ClientCredentialsGrant + :members: diff --git a/docs/decorators.rst b/docs/decorators.rst new file mode 100644 index 0000000..a005c85 --- /dev/null +++ b/docs/decorators.rst @@ -0,0 +1,21 @@ +========== +Decorators +========== + +Hopefully, it should be quite straightforward to port the django decorator to other web frameworks as the decorator mainly provide a means for translating the framework specific request object into uri, http_method, headers and body. + +Django OAuth 2 Decorator +------------------------ + +TODO: hows and important notes about csrf, xss, etc + +Implementing a new OAuth 2 Provider Decorator +--------------------------------------------- + +TODO: what needs to be done, what to keep an eye out for + +Extensions per web framework +---------------------------- + +If you are currently developing an extension, please let us know and we will +list it here. Or better yet, send a PR! diff --git a/docs/endpoints.rst b/docs/endpoints.rst new file mode 100644 index 0000000..5a4332f --- /dev/null +++ b/docs/endpoints.rst @@ -0,0 +1,265 @@ +================= +OAuth 2 Endpoints +================= + +Endpoints in OAuth 2 are targets with a specific responsibility and often +associated with a particular URL. Because of this the word endpoint might +be used interchangably from the endpoint url. + +There main three responsibilities in an OAuth 2 flow is to authorize access +to a certain users resources to a client, to supply said client with a token +embodying this authorization and to verify that the token is valid when the +client attempts to access thee user resources on their behalf. + +**Much of the logic presented in code snippets below can be conveniently +extracted away into a decorator class.** See :doc:`decorators` for examples. + +------------- +Authorization +------------- + +Authorization can be either explicit or implicit. The former require the user +to actively authorize the client by being redirected to the authorization +endpoint. There he/she is usually presented by a form and asked to either +accept or deny access to certain scopes. These scopes can be +thought of as Access Control Lists that are tied to certain privileges and +categories of resources, such as write access to their status feed or read +access to their profile. It is vital that the implications of granting access +to a certain scope is very clear in the authorization form presented to the +user. It is up to the provider to allow the user agree to all, a few or none +of the scopes. Being flexible here is a great benefit to the user at the cost +of added complexity in both the provider and clients. + +Implicit authorization happens when the authorization happens before the +OAuth flow, such as the user giving the client his/her password and username, +or if there is a very high level of trust between the user, client and provider +and no explicit authorization is necessary. + +Examples of explicit authorization is the Authorization Code Grant and the +Implicit Grant. + +Examples of implicit authorization is the Resource Owner Password Credentials +Grant and the Client Credentials Grant. + +**Pre Authorization Request** + OAuth is known for it's authorization page where the user accepts or + denies access to a certain client and set of scopes. Before presenting + the user with such a form you need to ensure the credentials the client + supplied in the redirection to this page are valid:: + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Validate request + uri = 'https://example.com/authorize?client_id=foo&state=xyz + headers, body, http_method = {}, '', 'GET' + + from oauthlib.oauth2 import FatalClientError + from your_framework import redirect + try: + scopes, credentials = server.validate_authorization_request( + uri, http_method, body, headers) + # scopes will hold default scopes for client, i.e. + ['https://example.com/userProfile', 'https://example.com/pictures'] + + # credentials is a dictionary of + { + 'client_id': 'foo', + 'redirect_uri': 'https://foo.com/welcome_back', + 'response_type': 'code', + 'state': 'randomstring', + } + # these credentials will be needed in the post authorization view and + # should be persisted between. None of them are secret but take care + # to ensure their integrety if embedding them in the form or cookies. + from your_datastore import persist_credentials + persist_credentials(credentials) + + # Present user with a nice form where client (id foo) request access to + # his default scopes (omitted from request), after which you will + # redirect to his default redirect uri (omitted from request). + + except FatalClientError as e: + # this is your custom error page + from your_views import authorization_error_page_uri + # Use in_uri to embed error code and description in the redirect uri + redirect(e.in_uri(authorization_error_page_uri)) + + +**Post Authorization Request** + Generally, this is where you handle the submitted form. Rather than using + ``validate_authorization_request`` we use ``create_authorization_response`` + which in the case of Authorization Code Grant embed an authorization code + in the client provided redirect uri:: + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Validate request + uri = 'https://example.com/post_authorize?client_id=foo + headers, body, http_method = {}, '', 'GET' + + # Fetch the credentials saved in the pre authorization phase + from your_datastore import fetch_credentials + credentials = fetch_credentials() + + # Fetch authorized scopes from the request + from your_framework import request + scopes = request.POST.get('scopes') + + from oauthlib.oauth2 import FatalClientError, OAuth2Error + from your_framework import redirect + try: + uri, headers, body, status = server.create_authorization_response( + uri, http_method, body, headers, scopes, credentials) + # uri = https://foo.com/welcome_back?code=somerandomstring&state=xyz + # headers = {}, this might change to include suggested headers related + # to cache best practices etc. + # body = '', this might be set in future custom grant types + # status = 302, suggested HTTP status code + + redirect(uri, headers=headers, status=status, body=body) + + except FatalClientError as e: + # this is your custom error page + from your_views import authorization_error_page_uri + # Use in_uri to embed error code and description in the redirect uri + redirect(e.in_uri(authorization_error_page_uri)) + + except OAuth2Error as e: + # Less grave errors will be reported back to client + client_redirect_uri = credentials.get('redirect_uri') + redirect(e.in_uri(client_redirect_uri)) + +.. autoclass:: oauthlib.oauth2.draft25.AuthorizationEndpoint + :members: + +-------------- +Token creation +-------------- + +Token endpoints issue tokens to clients who have already been authorized +access, be it by explicit actions from the user or implicitely. The token +response is well defined and typically consist of an unguessable access token, +the token type, its expiration from now in seconds and depending on the +scenario, a refresh token to be used to fetch new access tokens without +authorization. + +One argument for OAuth 2 being more scalable than OAuth 1 is that tokens +may contain hidden information. A provider may embed information such as +client identifier, user identifier, expiration times, etc. in the token +by encrypting it. This trades a slight increase in work required to decrypt +the token but frees the necessary database lookups otherwise required, +thus improving latency substantially. OAuthlib currently does not provide +a method for creating crypto-tokens but may do in the future. + +The standard token type, Bearer, does not require that the provider bind a +specific client to the token. Not binding clients to tokens allow for +anonymized tokens which unless you are certain you need them, are a bad idea. + +**Token Request** + A POST request used in most grant types but with a varied setup of + credentials. If you wish to embed extra credentials in the request, i.e. + for later use in validation or when creating the token, you can + use the ``credentials`` argument in ``create_token_response``. + + All responses are in json format and the headers argument returned by + ``create_token_response`` will contain a few suggested headers related + to content type and caching:: + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Validate request + uri = 'https://example.com/token' + http_method = 'POST' + body = 'authorization_code=somerandomstring&' + 'grant_type=authorization_code&' + # Clients authenticate through a method of your choosing, for example + # using HTTP Basic Authentication + headers = { 'Authorization': 'Basic ksjdhf923sf' } + + # Extra credentials you wish to include + credentials = {'client_ip': '1.2.3.4'} + + uri, headers, body, status = server.create_token_response( + uri, http_method, body, headers, credentials) + + # uri is not used by most grant types + # headers will contain some suggested headers to add to your response + { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } + # body will contain the token in json format and expiration from now + # in seconds. + { + 'access_token': 'sldafh309sdf', + 'refresh_token': 'alsounguessablerandomstring', + 'expires_in': 3600, + 'scopes': [ + 'https://example.com/userProfile', + 'https://example.com/pictures' + ], + 'token_type': 'Bearer' + } + # body will contain an error code and possibly an error description if + # the request failed, also in json format. + { + 'error': 'invalid_grant_type', + 'description': 'athorizatoin_coed is not a valid grant type' + } + # status will be a suggested status code, 200 on ok, 400 on bad request + # and 401 if client is trying to use an invalid authorization code, + # fail to authenticate etc. + + from your_framework import http_response + http_response(body, status=status, headers=headers) + +.. autoclass:: oauthlib.oauth2.draft25.TokenEndpoint + :members: + +--------------------------- +Authorizing resource access +--------------------------- + +Resource endpoints verify that the token presented is valid and granted access +to the scopes associated with the resource in question. + +**Request Verfication** + Each view may set certain scopes under which it is bound. Only requests + that present an access token bound to the correct scopes may access the + view. Access tokens are commonly embedded in the authorization header but + may appear in the query or the body as well:: + + # Initial setup + from your_validator import your_validator + server = WebApplicationServer(your_validator) + + # Per view scopes + required_scopes = ['https://example.com/userProfile'] + + # Validate request + uri = 'https://example.com/userProfile?access_token=sldafh309sdf' + headers, body, http_method = {}, '', 'GET' + + valid, oauthlib_request = server.verify_request( + uri, http_method, body, headers, required_scopes) + + # oauthlib_request has a few convenient attributes set such as + # oauthlib_request.client = the client associated with the token + # oauthlib_request.user = the user associated with the token + # oauthlib_request.scopes = the scopes bound to this token + + if valid: + # return the protected resource / view + else: + # return an http forbidden 403 + +.. autoclass:: oauthlib.oauth2.draft25.ResourceEndpoint + :members: + diff --git a/docs/implicit.rst b/docs/implicit.rst new file mode 100644 index 0000000..7bd8cbf --- /dev/null +++ b/docs/implicit.rst @@ -0,0 +1,7 @@ +Implicit Grant +-------------- + +TODO: describe on a high level what the grant is and when it is useful + +.. autoclass:: oauthlib.oauth2.draft25.grant_types.ImplicitGrant + :members: diff --git a/docs/index.rst b/docs/index.rst index e4cf1b0..f97e2e7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,15 +6,30 @@ Welcome to OAuthLib's documentation! ==================================== -Contents: +If you can't find what you need or have suggestions for improvement, don't +hesitate to open a `new issue on GitHub`_! + +For news and discussions please check out our `G+ OAuthLib community`_. + +.. _`new issue on GitHub`: https://github.com/idan/oauthlib/issues/new +.. _`G+ OAuthLib community`: https://plus.google.com/communities/101889017375384052571 .. toctree:: :maxdepth: 2 contributing + +.. toctree:: + :maxdepth: 2 + client - client2 server + +.. toctree:: + :maxdepth: 2 + + oauth2_overview + client2 server2 Indices and tables diff --git a/docs/legacyapplicationclient.rst b/docs/legacyapplicationclient.rst new file mode 100644 index 0000000..8e20f87 --- /dev/null +++ b/docs/legacyapplicationclient.rst @@ -0,0 +1,5 @@ +Resource Owner Password Credentials Grant flow (LegacyApplicationClient) +------------------------------------------------------------------------ + +.. autoclass:: oauthlib.oauth2.draft25.LegacyApplicationClient + :members: diff --git a/docs/mobileapplicationclient.rst b/docs/mobileapplicationclient.rst new file mode 100644 index 0000000..0a81afe --- /dev/null +++ b/docs/mobileapplicationclient.rst @@ -0,0 +1,5 @@ +Implicit Grant flow (MobileApplicationClient) +--------------------------------------------- + +.. autoclass:: oauthlib.oauth2.draft25.MobileApplicationClient + :members: diff --git a/docs/oauth2_overview.rst b/docs/oauth2_overview.rst new file mode 100644 index 0000000..f398ddd --- /dev/null +++ b/docs/oauth2_overview.rst @@ -0,0 +1,48 @@ +============================== +OAuth 2: A high level overview +============================== + +OAuth 2 is a very generic set of documents that leave a lot up to the implementer. It is not even a protocol, it is a framework. OAuthLib approaches this by separating the logic into three categories, endpoints, grant types and tokens. + +Endpoints +--------- + +.. toctree:: + :maxdepth: 2 + + endpoints + +There are three different endpoints, the authorization endpoint which mainly handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This process is simplified for you using a decorator such as the django one described later. + +The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. + +Grant types +----------- + +.. toctree:: + :maxdepth: 2 + + authcode + implicit + password + credentials + +Grant types are what make OAuth 2 so flexible. The Authorization Code grant is very similar to OAuth 1 (with less crypto), the Implicit grant serves less secure applications such as mobile applications, the Resource Owner Password Credentials grant allows for legacy applications to incrementally transition to OAuth 2, the Client Credentials grant is excellent for embedded services and backend applications. + +The main purpose of the grant types is to authorize access to protected resources in various ways with different security credentials. + +Naturally, OAuth 2 allows for extension grant types to be defined and OAuthLib attempts to cater for easy inclusion of this as much as possible. + +Certain grant types allow the issuing of refresh tokens which will allow a client to request new tokens for as long as you as provider allow them too. In general, OAuth 2 tokens should expire quickly and rather than annoying the user by require them to go through the authorization redirect loop you may use the refresh token to get a new access token. Refresh tokens, contrary to what their name suggest, are components of a grant type rather than token types (like Bearer tokens), much like the authorization code in the authorization code grant. + +Tokens +------ + +.. toctree:: + :maxdepth: 2 + + tokens + +The main token type of OAuth 2 is Bearer tokens and that is what OAuthLib currently supports. Other tokens, such as JWT, SAML and possibly MAC (if the spec matures) can easily be added (and will be in due time). + +The purpose of a token is to authorize access to protected resources to a client (i.e. your G+ feed). diff --git a/docs/password.rst b/docs/password.rst new file mode 100644 index 0000000..32896cc --- /dev/null +++ b/docs/password.rst @@ -0,0 +1,7 @@ +Resource Owner Password Credentials Grant +----------------------------------------- + +TODO: describe on a high level what the grant is and when it is useful + +.. autoclass:: oauthlib.oauth2.draft25.grant_types.ResourceOwnerPasswordCredentialsGrant + :members: diff --git a/docs/preconfigured_servers.rst b/docs/preconfigured_servers.rst new file mode 100644 index 0000000..41948c1 --- /dev/null +++ b/docs/preconfigured_servers.rst @@ -0,0 +1,36 @@ +Preconfigured all-in-one servers +================================ + +A pre configured server is an all-in-one endpoint serving a specific +class of application clients. As the individual endpoints, they depend +on the use of a :doc:`validator`. + +Once constructed they can be plugged into any of the available +:doc:`decorators` or used on their own. For the latter case you +might be interested in looking at :doc:`endpoints`. + +Construction is simple, only import your validator and you are good to go:: + + from your_validator import your_validator + + from oauthlib.oauth2 import WebApplicationServer + server - WebApplicationServer(your_validator) + +If you prefer to construct tokens yourself you may pass a token generator:: + + def your_token_generator(request): + return 'a_custom_token' + request.client_id + + server - WebApplicationServer(your_validator, token_generator-your_token_generator) + +.. autoclass:: oauthlib.oauth2.draft25.WebApplicationServer + :members: + +.. autoclass:: oauthlib.oauth2.draft25.MobileApplicationServer + :members: + +.. autoclass:: oauthlib.oauth2.draft25.LegacyApplicationServer + :members: + +.. autoclass:: oauthlib.oauth2.draft25.BackendApplicationServer + :members: diff --git a/docs/security.rst b/docs/security.rst new file mode 100644 index 0000000..fee4fc9 --- /dev/null +++ b/docs/security.rst @@ -0,0 +1,5 @@ +======== +Security +======== + +TODO: the essentials to get right diff --git a/docs/server.rst b/docs/server.rst index 28994be..536d0f6 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -1,5 +1,5 @@ -Creating an OAuth provider -========================== +OAuth 1: Creating a Provider +============================ Note that the current OAuth1 provider interface will change into one resembling the work in progress OAuth 2 provider in a not too distant future. More information in `issue #95`_. @@ -7,225 +7,65 @@ Note that the current OAuth1 provider interface will change into one resembling Implementing an OAuth provider is simple with OAuthLib. It is done by inheriting from ``oauthlib.oauth1.rfc5849.Server`` and overloading a few key methods. The base class provide a secure by default implementation including a ``verify_request`` method as well as several input validation methods, all configurable using properties. While it is straightforward to use OAuthLib directly with your web framework of choice it is worth first exploring whether there is an OAuthLib based OAuth provider plugin available for your framework. -A few important facts regarding OAuth security ----------------------------------------------- +**A few important facts regarding OAuth security** -* **OAuth without SSL is a Bad Ideaâ„¢** and it's strongly recommended to use SSL for all interactions both with your API as well as for setting up tokens. An example of when it's especially bad is when sending POST requests with form data, this data is not accounted for in the OAuth signature and a successfull man-in-the-middle attacker could swap your form data (or files) to whatever he pleases without invalidating the signature. This is an even bigger issue if you fail to check nonce/timestamp pairs for each request, allowing an attacker who intercept your request to replay it later, overriding your initial request. **Server defaults to fail all requests which are not made over HTTPS**, you can explicitely disable this using the enforce_ssl property. + * **OAuth without SSL is a Bad Ideaâ„¢** and it's strongly recommended to use SSL for all interactions both with your API as well as for setting up tokens. An example of when it's especially bad is when sending POST requests with form data, this data is not accounted for in the OAuth signature and a successfull man-in-the-middle attacker could swap your form data (or files) to whatever he pleases without invalidating the signature. This is an even bigger issue if you fail to check nonce/timestamp pairs for each request, allowing an attacker who intercept your request to replay it later, overriding your initial request. **Server defaults to fail all requests which are not made over HTTPS**, you can explicitely disable this using the enforce_ssl property. -* **Tokens must be random**, OAuthLib provides a method for generating secure tokens and it's packed into ``oauthlib.common.generate_token``, use it. If you decide to roll your own, use ``random.SystemRandom`` which is based on ``os.urandom`` rather than the default ``random`` based on the effecient but not truly random Mersenne Twister. Predicatble tokens allow attackers to bypass virtually all defences OAuth provides. + * **Tokens must be random**, OAuthLib provides a method for generating secure tokens and it's packed into ``oauthlib.common.generate_token``, use it. If you decide to roll your own, use ``random.SystemRandom`` which is based on ``os.urandom`` rather than the default ``random`` based on the effecient but not truly random Mersenne Twister. Predicatble tokens allow attackers to bypass virtually all defences OAuth provides. -* **Timing attacks are real** and more than possible if you host your application inside a shared datacenter. Ensure all ``validate_`` methods execute in near constant time no matter which input is given. This will be covered in more detail later. Failing to account for timing attacks could **enable attackers to enumerate tokens and successfully guess HMAC secrets**. Note that RSA keys are protected through RSA blinding and are not at risk. + * **Timing attacks are real** and more than possible if you host your application inside a shared datacenter. Ensure all ``validate_`` methods execute in near constant time no matter which input is given. This will be covered in more detail later. Failing to account for timing attacks could **enable attackers to enumerate tokens and successfully guess HMAC secrets**. Note that RSA keys are protected through RSA blinding and are not at risk. -* **Nonce and timestamps must be checked**, do not ignore this as it's a simple and effective way to prevent replay attacks. Failing this allows online bruteforcing of secrets which is not something you want. + * **Nonce and timestamps must be checked**, do not ignore this as it's a simple and effective way to prevent replay attacks. Failing this allows online bruteforcing of secrets which is not something you want. -* **Whitelisting is your friend** and effectively eliminates SQL injection and other nasty attacks on your precious data. More details on this in the ``check_`` methods. + * **Whitelisting is your friend** and effectively eliminates SQL injection and other nasty attacks on your precious data. More details on this in the ``check_`` methods. -* **Require all callback URIs to be registered before use**. OAuth providers are in the unique position of being able to restrict which URIs may be submitted, making validation simple and safe. This registration should be done in your Application management interface. + * **Require all callback URIs to be registered before use**. OAuth providers are in the unique position of being able to restrict which URIs may be submitted, making validation simple and safe. This registration should be done in your Application management interface. -Methods that must be overloaded -------------------------------- +**Verifying requests** -Example implementations have been provided, note that the database used is a simple dictionary and serves only an illustrative purpose. Use whichever database suits your project and how to access it is entirely up to you. The methods are introduced in an order which should make understanding their use more straightforward and as such it could be worth reading what follows in chronological order. + Request verification is provided through the ``Server.verify_request`` method which has the following signature:: -#. ``validate_timestamp_and_nonce(self, client_key, timestamp, nonce, request_token=None, access_token=None)`` -#. ``validate_client_key(self, client_key)`` -#. ``validate_request_token(self, client_key, request_token)`` -#. ``validate_access_token(self, client_key, access_token)`` -#. ``dummy_client(self)`` -#. ``dummy_request_token(self)`` -#. ``dummy_access_token(self)`` -#. ``validate_redirect_uri(self, client_key, redirect_uri)`` -#. ``validate_requested_realm(self, client_key, realm)`` -#. ``validate_realm(self, client_key, access_token, uri, required_realm=None)`` -#. ``validate_verifier(self, client_key, request_token, verifier)`` -#. ``get_client_secret(self, client_key)`` -#. ``get_request_token_secret(self, client_key, request_token)`` -#. ``get_access_token(self, client_key, access_token)`` -#. ``get_rsa_key(self, client_key)`` + verify_request(self, uri, http_method=u'GET', body=None, headers=None, + require_resource_owner=True, + require_verifier=False, + require_realm=False, + required_realm=None) -``validate_timestamp_and_nonce(self, client_key, timestamp, nonce, request_token=None, access_token=None)`` + There are three types of verifications you will want to perform, all which could be altered through the use of a realm parameter if you choose to allow/require this. Note that if verify_request returns false a HTTP 401Unauthorized should be returned. If a ValueError is raised a HTTP 400 Bad Request response should be returned. All request verifications will look similar to the following:: -The first thing you want to do is check nonce and timestamp, which are associated with a client key and possibly a token, and immediately fail the request if the nonce/timestamp pair has been used before. This prevents replay attacks and is an essential part of OAuth security. Note that this is done before checking the validity of the client and token.:: + try: + authorized = server.verify_request(uri, http_method, body, headers) + if not authorized: + # return a HTTP 401 Unauthorized response + else: + # Create, save and return request token/access token/protected resource + # or whatever you had in mind that required OAuth + except ValueError: + # return a HTTP 400 Bad Request response - nonces_and_timestamps_database = [ - (u'foo', 1234567890, u'rannoMstrInghere', u'bar') - ] + The only change will be parameters to the verify_request method. - def validate_timestamp_and_nonce(self, client_key, timestamp, nonce, - request_token=None, access_token=None): + #. Requests to obtain request tokens, these may include an optional redirection URI parameter:: - return ((client_key, timestamp, nonce, request_token or access_token) - in self.nonces_and_timestamps_database) + authorized = server.verify_request(uri, http_method, body, headers, + require_resource_owner=False) -``validate_client_key(self, client_key)`` and -``validate_request_token(self, client_key, request_token)`` -``validate_access_token(self, client_key, access_token)`` + #. Requests to obtain access tokens, these should always include a verifier and a resource owner key:: -Validation of client keys simply ensure that the provided key is associated with a registered client. Same goes for the tokens:: + authorized = server.verify_request(uri, http_method, body, headers, + require_verifier=True) - clients_database = [u'foo'] + #. Requests to protected resources:: - def validate_client_key(self, client_key): - return client_key in self.clients_database + authorized = server.verify_request(uri, http_method, body, headers) - request_token_database = [(u'foo', u'bar')] - access_token_database = [] - def validate_request_token(self, client_key, request_token): - return (client_key, request_token) in self.request_token_database +**Configuring check methods and their respective properties** -Note that your dummy client and dummy tokens must validate to false and do so without affecting the execution time of the client validation. **Avoid doing this**:: + There are a number of input validation checks that perform white listing of input parameters. I hope to document them soon but for now please refer to the Server source code found in oauthlib.oauth1.rfc5849.__init__.py. - def validate_client_key(self, client_key): - if client_key == dummy_client: - return False - return client_key in self.clients_database +**Methods that must be overloaded** + +.. autoclass:: oauthlib.oauth1.rfc5849.Server + :members: - -``dummy_client(self)``, ``dummy_request_token(self)`` and ``dummy_access_token(self)`` - -Dummy values are used to enable the verification to execute in near constant time even if the client key or token is invalid. No early exits are taken during the verification and even a signature is calculated for the dummy client and/or token. The use of these dummy values effectively eliminate the chance of an attacker guessing tokens and secrets by measuring the response time of request verification:: - - @property - def dummy_client(self): - return u'dummy_client' - - @property - def dummy_resource_owner(self): - return u'dummy_resource_owner' - -``validate_redirect_uri(self, client_key, redirect_uri)`` - -All redirection URIs (provided when obtaining request tokens) must be validated. If you require clients to register these URIs this is a trivial operation. It is worth considering a hash comparison of values since URIs could be hard to sanitize and thus not optimal to throw into a database query. The example below illustrates this using pythons builtin membership comparison:: - - def validate_redirect_uri(self, client_key, redirect_uri): - redirect_uris = db.get_all_redirect_uris_for_client(client_key) - return redirect_uri in redirect_uris - -As opposed to:: - - def validate_redirect_uri(self, client_key, redirect_uri): - return len(db.query_client_redirect_uris(uri=redirect_uri).result) == 1 - -Using our familiar example dict database:: - - redirect_uris = { - u'foo' : [u'https://some.fance.io/callback'] - } - - def validate_redirect_uri(self, client_key, redirect_uri): - return (client_key in self.redirect_uris and - redirect_uri in self.redirect_uris.get(client_key)) - -``validate_realm(self, client_key, resource_owner_key, realm, uri)`` - -Realms are useful when restricting scope. Scope could be a variety of things but commonly relates to privileges (read/write) or content categories (photos/private/code). Since realms are commonly associated not only with client keys and tokens but also a resource URI the requested URI is an included argument as well:: - - assigned_realms = { - u'foo' : [u'photos'] - } - - realms = { - (u'foo', u'bar') : u'photos' - } - - def validate_requested_realm(self, client_key, realm): - return realm in self.assigned_realms.get(client_key) - - def validate_realm(self, client_key, access_token, uri=None, required_realm=None): - if required_realm: - return self.realms.get((client_key, access_token)) in required_realm - else: - # Use the URI to figure out if the associated realm is valid - -``validate_verifier(self, client_key, resource_owner_key, verifier)`` - -Verifiers are assigned to a client after the resource owner (user) has authorized access. They will thus only be present (and valid) in access token request. Naturally they must be validated and it should be done in near constant time (to avoid verifier enumeration). To achieve this we need a constant time string comparison which is provided by OAuthLib in ``oauthlib.common.safe_string_equals``:: - - verifiers = { - (u'foo', u'request_token') : u'randomVerifierString' - } - - def validate_verifier(self, client_key, request_token, verifier): - return safe_string_equals(verifier, self.verifiers.get((client_key, request_token)) - -``get_client_secret(self, client_key)`` - -Fetches the client secret associated with client key from your database. Note that your database should include a dummy key associated with your dummy user mentioned previously:: - - client_secrets_database = { - u'foo' : u'fooshizzle', - u'user1' : u'password1', - u'dummy_client' : u'dummy-secret' - } - - def get_client_secret(self, client_key): - return self.client_secrets_database.get(client_key) - -``get_request_token_secret(self, client_key, request_token)`` -``get_access_token_secret(self, client_key, access_token)`` - -Fetches the resource owner secret associated with client key and token. Similar to ``get_client_secret`` the database should include a dummy resource owner secret:: - - request_token_secrets_database = { - (u'foo', u'someResourceOwner') : u'seeeecret', - (u'dummy_client', 'dummy_resource_owner') : u'dummy-owner-secret' - } - - def get_request_token_secret(client_key, request_token): - return self.request_token_secrets.get((client_key, request_token)) - -``get_rsa_key(self, client_key)`` - - If RSA signatures are used the Server must fetch the **public key** associated with the client. There should be a dummy RSA public key associated with dummy clients. Keys have been cut in length for obvious reasons:: - - rsa_public_keys = { - u'foo' : u'-----BEGIN PUBLIC KEY-----MIGfMA0GCSqG....', - u'dummy_client' : u'-----BEGIN PUBLIC KEY-----e1Sb3fKQIDAQA....' - } - - def get_rsa_key(self, client_key): - return self.rsa_public_keys.get(client_key) - -Verifying requests ------------------- - -Request verification is provided through the ``Server.verify_request`` method which has the following signature:: - - verify_request(self, uri, http_method=u'GET', body=None, headers=None, - require_resource_owner=True, - require_verifier=False, - require_realm=False, - required_realm=None) - -There are three types of verifications you will want to perform, all which could be altered through the use of a realm parameter if you choose to allow/require this. Note that if verify_request returns false a HTTP 401Unauthorized should be returned. If a ValueError is raised a HTTP 400 Bad Request response should be returned. All request verifications will look similar to the following:: - - try: - authorized = server.verify_request(uri, http_method, body, headers) - if not authorized: - # return a HTTP 401 Unauthorized response - else: - # Create, save and return request token/access token/protected resource - # or whatever you had in mind that required OAuth - except ValueError: - # return a HTTP 400 Bad Request response - -The only change will be parameters to the verify_request method. - -#. Requests to obtain request tokens, these may include an optional redirection URI parameter:: - - authorized = server.verify_request(uri, http_method, body, headers, require_resource_owner=False) - -#. Requests to obtain access tokens, these should always include a verifier and a resource owner key:: - - authorized = server.verify_request(uri, http_method, body, headers, require_verifier=True) - -#. Requests to protected resources:: - - authorized = server.verify_request(uri, http_method, body, headers) - - -Configuring check methods and their respective properties ---------------------------------------------------------- - -There are a number of input validation checks that perform white listing of input parameters. I hope to document them soon but for now please refer to the Server source code found in oauthlib.oauth1.rfc5849.__init__.py. diff --git a/docs/server2.rst b/docs/server2.rst index 0a015f8..4be1719 100644 --- a/docs/server2.rst +++ b/docs/server2.rst @@ -1,147 +1,317 @@ ============================ -Creating an OAuth 2 provider +OAuth 2: Creating a Provider ============================ -Note that OAuth 2 provider is still very much a work in progress, consider it a preview of a near future =) Feedback in any form much welcome! +Note that OAuth 2 provider is still very much a work in progress, consider it a preview of a near future =) -A high level overview ---------------------- +**1. Which framework are you using?** -OAuth 2 is a very generic set of documents that leave a lot up to the implementer. It is not even a protocol, it is a framework. OAuthLib approaches this by separating the logic into three categories, endpoints, grant types and tokens. + OAuthLib is a dependency free library that may be used with any web framework. + That said, there are framework specific helper decorator classes to make + your life easier. The one we will be using in this example is for Django. + For others, and information on how to create one, check out :doc:`decorators`. -Endpoints -~~~~~~~~~ + The main purpose of these decoraters is to help marshall between the framework + specific request object and framework agnostic url, headers, body and + http method parameters. They may also be useful for making sure common + best security practices are followed. -There are three different endpoints, the authorization endpoint which mainly handles user authorization, the token endpoint which provides tokens and the resource endpoint which provides access to protected resources. It is to the endpoints you will feed requests and get back an almost complete response. This process is simplified for you using a decorator such as the django one described later. + Their purpose is not to be a full solution to all your needs as a provider, for + that you will want to seek out framework specific extensions building upon + OAuthLib. See the section on :doc:`decorators` for a list of extensions. -The main purpose of the endpoint in OAuthLib is to figure out which grant type or token to dispatch the request to. + Relevant sections include: -Grant types -~~~~~~~~~~~ + .. toctree:: + :maxdepth: 1 -Grant types are what make OAuth 2 so flexible. The Authorization Code grant is very similar to OAuth 1 (with less crypto), the Implicit grant serves less secure applications such as mobile applications, the Resource Owner Password Credentials grant allows for legacy applications to incrementally transition to OAuth 2, the Client Credentials grant is excellent for embedded services and backend applications. + decorators -The main purpose of the grant types is to authorize access to protected resources in various ways with different security credentials. +**2. Create your datastore models** -Naturally, OAuth 2 allows for extension grant types to be defined and OAuthLib attempts to cater for easy inclusion of this as much as possible. + These models will represent various OAuth specific concepts. There are a few + important links between them that the security of OAuth is based on. Below is + a suggestion for models and why you need certain properties. There is also + example Django model fields which should be straightforward to translate + to other ORMs such as SQLAlchemy and the Appengine Datastore. -Certain grant types allow the issuing of refresh tokens which will allow a client to request new tokens for as long as you as provider allow them too. In general, OAuth 2 tokens should expire quickly and rather than annoying the user by require them to go through the authorization redirect loop you may use the refresh token to get a new access token. Refresh tokens, contrary to what their name suggest, are components of a grant type rather than token types (like Bearer tokens), much like the authorization code in the authorization code grant. + **User (or Resource Owner)** + The user of your site which resources might be access by clients upon + authorization from the user. In our example we will re-use the User + model provided in django.contrib.auth.models. How the user authenticates + is orthogonal from OAuth and may be any way you prefer:: -Tokens -~~~~~~ + from django.contrib.auth.models import User -The main token type of OAuth 2 is Bearer tokens and that is what OAuthLib currently supports. Other tokens, such as JWT, SAML and possibly MAC (if the spec matures) can easily be added (and will be in due time). + **Client (or Consumer)** + The client interested in accessing protected resources. -The purpose of a token is to authorize access to protected resources to a client (i.e. your G+ feed). + **Client Identifier**: + Required. The identifier the client will use during the OAuth + workflow. Structure is up to you and may be a simple UUID:: + client_id = django.db.models.CharField(max_length=100, unique=True) -How do I develop an OAuth 2 provider? -------------------------------------- + **User**: + Recommended. It is common practice to link each client with one of your + existing users. Whether you do associate clients and users or not, + ensure you are able to protect yourself against malicious clients:: -The majority of the work involves mapping various validation and persistence methods to a storage backend. The not very accurately named interface you will need to implement is called a RequestValidator (name suggestions welcome). + user = django.db.models.ForeignKey(User) -The request validator can be found in oauthlib.oauth2.draft25.grant_types, which will be the main source of documentation on which methods you need to implement. As an example, a very basic validate_client_id method might be implemented in Django as follows:: + **Grant Type**: + Required. The grant type the client may utilize. This should only be + one per client as each grant type has different security properties + and it is best to keep them separate to avoid mistakes:: - from oauthlib.oauth2 import RequestValidator + # max_length and choices depend on which grants you support + grant_type = django.db.models.CharField(max_length=18, + choices=[('Authorization code', 'authorization_code')]) - from my_models import Client + **Response Type**: + Required, if using a grant type with an associated response type or + using a grant which only utilizes response types. An example of the + former is Authorization Code Grant and the latter Implicit Grant:: - class MyRequestValidator(RequestValidator): + # max_length and choices depend on which response types you support + grant_type = django.db.models.CharField(max_length=4, + choices=[('Authorization code', 'code')]) - def validate_client_id(self, client_id, request): - try: - Client.objects.get(client_id=client_id) - return True - except Client.DoesNotExist: - return False + **Scopes**: + Required. The list of scopes the client may request access to. If you + allow multiple types of grants this will vary related to their + different security properties. For example, the Implicit Grant might + only allow read-only scopes but the Authorization Grant also allow + writes:: + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + scopes = django.db.models.TextField() -Pre configured endpoints ------------------------- + # You might also want to mark a certain set of scopes as default + # scopes in case the client does not specify any in the authorization + default_scopes = django.db.models.TextField() -OAuthLib provide a number of configured all-in-one endpoints (auth + token + resource) with different grant types, all utilize Bearer tokens. The available configurations are + **Redirect URIs**: + These are the absolute URIs that a client may use to redirect to after + authorization. You should never allow a client to redirect to a URI + that has not previously been registered:: -* WebApplicationServer featuring Authorization Code Grant and Refresh Tokens -* MobileApplicationServer featuring Implicit Grant -* LegacyApplicationServer featuring Resource Owner Password Credentials Grant and Refresh Tokens -* BackendApplicationServer featuring Client Credentials Grant -* Server featuring all above bundled into one + # You could represent the URIs either as a list of keys or by + # serializing them into a string. + redirect_uris = django.db.models.TextField() + # You might also want to mark a certain URI as defaul in case the + # client does not specify any in the authorization + default_redirect_uri = django.db.models.TextField() -Using the django decorator --------------------------- + **Bearer Token (OAuth 2 Standard Token)** + The most common type of OAuth 2 token. Through the documentation this will + be considered an object with several properties, such as token type and + expiration date, and distinct from the access token it contains. Think of + OAuth 2 tokens as containers and access tokens and refresh tokens as text. -Assuming you have the validator from above implemented already, creating an OAuth 2 provider can be as simple as:: + **Client**: + Association with the client to whom the token was given:: - # your_views.py - from my_validator import MyRequestValidator - - from oauthlib.oauth2 import WebApplicationServer # BearerTokens + Authorization Code grant - from oauthlib.oauth2.ext.django import OAuth2ProviderDecorator - - validator = MyRequestValidator() - server = WebApplicationServer(validator) - provider = OAuth2ProviderDecorator('/error', server) # See view error below - - @login_required - @provider.pre_authorization_view - def authorize(request, scopes=None): - # This is the traditional authorization page - # Scopes will be the list of scopes client requested access too - # You will want to present them in a nice form where the user can - # select which scopes they allow the client to access. - return render(request, 'authorize.html', {'scopes': scopes}) + client = django.db.models.ForeignKey(Client) + **User**: + Association with the user to which protected resources this token + grants access:: - @login_required - @provider.post_authorization_view - def authorization_response(request): - # This is where the form submitted from authorize should end up - # Which scopes user authorized access to + extra credentials you want - # appended to the request object passed into the validator methods - return request.POST['scopes'], {} + user = django.db.models.ForeignKey(User) + **Scopes**: + Scopes to which the token is bound. Attempt to access protected + resources outside these scopes will be denied:: - @provider.access_token_view - def token_response(request): - # Not much too do here for you, return a dict with extra credentials - # you want appended to request.credentials passed to the save_bearer_token - # method of the validator. - return {'extra': 'creds'} + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + scopes = django.db.models.TextField() + **Access Token**: + An unguessable unique string of characters:: - @provider.protected_resource_view(scopes=['images']) - def i_am_protected(request, client, resource_owner, **kwargs): - # One of your many OAuth 2 protected resource views, returns whatever you fancy - # May be bound to various scopes of your choosing - return HttpResponse('pictures of cats') + access_token = django.db.models.CharField(max_length=100, unique=True) + **Refresh Token**: + An unguessable unique string of characters. This token is only supplied + to confidential clients. For example the Authorization Code Grant or + the Resource Owner Password Credentials Grant:: - def error(request): - # The /error page users will be redirected to if there was something - # wrong with the credentials the client included when redirecting the - # user to the authorization form. Mainly if the client was invalid or - # included a malformed / invalid redirect url. - # Error and description can be found in GET['error'] and GET['error_description'] - return HttpResponse('Bad client! Warn user!') + refresh_token = django.db.models.CharField(max_length=100, unique=True) + **Expiration time**: + Exact time of expiration. Commonly this is one hour after creation:: -Can you please add X, Y and Z? ------------------------------- + expires_at = django.db.models.DateTimeField() -If these include dashboards, database migrations, registration APIs and similar the answer is no. While these would be excellent to have, oauthlib is not the place for them. I would much rather see a django middleware plugin with these features but I currently lack the time to develop it myself. + **Authorization Code** + This is specific to the Authorization Code grant and represent the + temporary credential granted to the client upon successful authorization. + It will later be exchanged for an access token, when that is done it should + cease to exist. It should have a limited life time, less than ten minutes. + This model is similar to the Bearer Token as it mainly acts a temporary + storage of properties to later be transferred to the token. + + **Client**: + Association with the client to whom the token was given:: -Creating decorators for other frameworks ----------------------------------------- + client = django.db.models.ForeignKey(Client) -Hopefully, it should be quite straightforward to port the django decorator to other web frameworks as the decorator mainly provide a means for translating the framework specific request object into uri, http_method, headers and body. + **User**: + Association with the user to which protected resources this token + grants access:: + user = django.db.models.ForeignKey(User) -How do I enable logging? ------------------------- -OAuthLib can provide valuable debug logs that help you get your provider up and running much quicker. You can log to stdout for example using:: + **Scopes**: + Scopes to which the token is bound. Attempt to access protected + resources outside these scopes will be denied:: - import logging - log = logging.getLogger('oauthlib') - log.setLevel(logging.DEBUG) + # You could represent it either as a list of keys or by serializing + # the scopes into a string. + scopes = django.db.models.TextField() + **Authorization Code**: + An unguessable unique string of characters:: + + code = django.db.models.CharField(max_length=100, unique=True) + + **Expiration time**: + Exact time of expiration. Commonly this is under ten minutes after creation:: + + expires_at = django.db.models.DateTimeField() + +**3. Implement a validator** + + The majority of the work involved in implementing an OAuth 2 provider relates + to mapping various validation and persistence methods to a storage backend. + The not very accurately named interface you will need to implement is called + a :doc:`RequestValidator <validator>` (name suggestions welcome). + + An example of a very basic implementation of the validate_client_id method + can be seen below:: + + from oauthlib.oauth2 import RequestValidator + + # From the previous section on models + from my_models import Client + + class MyRequestValidator(RequestValidator): + + def validate_client_id(self, client_id, request): + try: + Client.objects.get(client_id=client_id) + return True + except Client.DoesNotExist: + return False + + The full API you will need to implement is available in the + :doc:`RequestValidator <validator>` section. You might not need to implement + all methods depending on which grant types you wish to support. + + Relevant sections include: + + .. toctree:: + :maxdepth: 1 + + validator + security + + +**4. Create your composite endpoint** + + Each of the endpoints can function independently from eachother, however for + this example it is easier to consider them as one unit. An example of a + pre-configured all-in-one Authorization Code Grant endpoint is given below:: + + # From the previous section on validators + from my_validator import MyRequestValidator + + from oauthlib.oauth2 import WebApplicationServer + from oauthlib.oauth2.ext.django import OAuth2ProviderDecorator + + validator = MyRequestValidator() + server = WebApplicationServer(validator) + provider = OAuth2ProviderDecorator('/error', server) # See next section + + Relevant sections include: + + .. toctree:: + :maxdepth: 1 + + preconfigured_servers + + +**5. Decorate your endpoint views** + + We are implementing support for the Authorization Code Grant and will therefore + need two views for the authorization, pre- and post-authorization together + with the token view. We also include an error page to redirect users to if + the client supplied invalid credentials in their redirection, for example + an invalid redirect URI:: + + @login_required + @provider.pre_authorization_view + def authorize(request, scopes=None): + # This is the traditional authorization page + # Scopes will be the list of scopes client requested access too + # You will want to present them in a nice form where the user can + # select which scopes they allow the client to access. + return render(request, 'authorize.html', {'scopes': scopes}) + + + @login_required + @provider.post_authorization_view + def authorization_response(request): + # This is where the form submitted from authorize should end up + # Which scopes user authorized access to + extra credentials you want + # appended to the request object passed into the validator methods + return request.POST['scopes'], {} + + + @provider.access_token_view + def token_response(request): + # Not much too do here for you, return a dict with extra credentials + # you want appended to request.credentials passed to the save_bearer_token + # method of the validator. + return {'extra': 'creds'} + + def error(request): + # The /error page users will be redirected to if there was something + # wrong with the credentials the client included when redirecting the + # user to the authorization form. Mainly if the client was invalid or + # included a malformed / invalid redirect url. + # Error and description can be found in + # GET['error'] and GET['error_description'] + return HttpResponse('Bad client! Warn user!') + + +**6. Protect your APIs using scopes** + + At this point you are ready to protect your API views with OAuth. Take some + time to come up with a good set of scopes as they can be very powerful in + controlling access:: + + @provider.protected_resource_view(scopes=['images']) + def i_am_protected(request, client, resource_owner, **kwargs): + # One of your many OAuth 2 protected resource views + # Returns whatever you fancy + # May be bound to various scopes of your choosing + return HttpResponse('pictures of cats') + +**7. Let us know how it went!** + + Drop a line in our `G+ community`_ or open a `GitHub issue`_ =) + + .. _`G+ community`: https://plus.google.com/communities/101889017375384052571 + .. _`GitHub issue`: https://github.com/idan/oauthlib/issues/new + + If you run into issues it can be helpful to enable debug logging:: + + import logging + log = logging.getLogger('oauthlib') + log.addHandler(logging.StreamHandler(sys.stdout)) + log.setLevel(logging.DEBUG) diff --git a/docs/tokens.rst b/docs/tokens.rst new file mode 100644 index 0000000..659e1fa --- /dev/null +++ b/docs/tokens.rst @@ -0,0 +1,46 @@ +============== +OAuth 2 Tokens +============== + +TODO(ib-lundgren): Outline the various use cases for each token type. + +------------------------ +Bearer Tokens (standard) +------------------------ + +The most common OAuth 2 token type. It provides very little in terms of +security and relies heavily upon the ability of the client to keep the +token secret. + +Bearer tokens are the default setting with all configured endpoints. +Generally you will not need to ever construct a token yourself as +the provided servers will do so for you. + +.. autoclass:: oauthlib.oauth2.draft25.tokens.BearerToken + :members: + +----------- +SAML Tokens +----------- + +Not yet implemented. Track progress in `GitHub issue 49`_. + +.. _`GitHub issue 49`: https://github.com/idan/oauthlib/issues/49 + +---------- +JWT Tokens +---------- + +Not yet implemented. Track progress in `GitHub issue 50`_. + +.. _`GitHub issue 50`: https://github.com/idan/oauthlib/issues/50 + +---------- +MAC tokens +---------- + +Not yet implemented. Track progress in `GitHub issue 29`_. Might never +be supported depending on whether the work on the specification is +resumed or not. + +.. _`GitHub issue 29`: https://github.com/idan/oauthlib/issues/29 diff --git a/docs/validator.rst b/docs/validator.rst new file mode 100644 index 0000000..c6a399e --- /dev/null +++ b/docs/validator.rst @@ -0,0 +1,8 @@ +================= +Request Validator +================= + +TODO: high level overview of what it is and what to do + +.. autoclass:: oauthlib.oauth2.draft25.grant_types.RequestValidator + :members: diff --git a/docs/webapplicationclient.rst b/docs/webapplicationclient.rst new file mode 100644 index 0000000..5cfcef1 --- /dev/null +++ b/docs/webapplicationclient.rst @@ -0,0 +1,5 @@ +Authorization Code Grant flow (WebApplicationClient) +---------------------------------------------------- + +.. autoclass:: oauthlib.oauth2.draft25.WebApplicationClient + :members: diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index d35b884..f2f26d9 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -16,5 +16,6 @@ from .draft25 import ClientCredentialsClient as BackendApplicationClient from .draft25 import AuthorizationEndpoint, TokenEndpoint, ResourceEndpoint from .draft25 import WebApplicationServer, MobileApplicationServer from .draft25 import LegacyApplicationServer, BackendApplicationServer -from .draft25.grant_types import RequestValidator +from .draft25.grant_types import * +from .draft25.tokens import BearerToken from .draft25.errors import * diff --git a/oauthlib/oauth2/draft25/__init__.py b/oauthlib/oauth2/draft25/__init__.py index c7f87b5..ede8e80 100644 --- a/oauthlib/oauth2/draft25/__init__.py +++ b/oauthlib/oauth2/draft25/__init__.py @@ -13,7 +13,7 @@ import logging from oauthlib.common import Request from oauthlib.oauth2.draft25 import tokens, grant_types -from .errors import TokenExpiredError +from .errors import TokenExpiredError, InsecureTransportError from .parameters import prepare_grant_uri, prepare_token_request from .parameters import parse_authorization_code_response from .parameters import parse_implicit_response, parse_token_response @@ -113,6 +113,9 @@ class Client(object): .. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#ref-I-D.ietf-oauth-v2-bearer .. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#ref-I-D.ietf-oauth-v2-http-mac """ + if not uri.lower().startswith('https://'): + raise InsecureTransportError() + token_placement = token_placement or self.default_token_placement if not self.token_type in self.token_types: @@ -246,52 +249,85 @@ class WebApplicationClient(Client): The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI - using the "application/x-www-form-urlencoded" format as defined by - [`W3C.REC-html401-19991224`_]: - - response_type - REQUIRED. Value MUST be set to "code". - client_id - REQUIRED. The client identifier as described in `Section 2.2`_. - redirect_uri - OPTIONAL. As described in `Section 3.1.2`_. - scope - OPTIONAL. The scope of the access request as described by - `Section 3.3`_. - state - RECOMMENDED. An opaque value used by the client to maintain - state between the request and callback. The authorization - server includes this value when redirecting the user-agent back - to the client. The parameter SHOULD be used for preventing - cross-site request forgery as described in `Section 10.12`_. - - .. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#ref-W3C.REC-html401-19991224 - .. _`Section 2.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3 - .. _`Section 10.12`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-10.12 + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI + and it should have been registerd with the OAuth + provider prior to use. As described in `Section 3.1.2`_. + + :param scope: OPTIONAL. The scope of the access request as described by + Section 3.3`_. These may be any string but are commonly + URIs or various categories such as ``videos`` or ``documents``. + + :param state: RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + :param kwargs: Extra arguments to include in the request URI. + + In addition to supplied parameters, OAuthLib will append the ``client_id`` + that was provided in the constructor as well as the mandatory ``response_type`` + argument, set to ``code``:: + + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> client.prepare_request_uri('https://example.com') + 'https://example.com?client_id=your_id&response_type=code' + >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback') + 'https://example.com?client_id=your_id&response_type=code&redirect_uri=https%3A%2F%2Fa.b%2Fcallback' + >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures']) + 'https://example.com?client_id=your_id&response_type=code&scope=profile+pictures' + >>> client.prepare_request_uri('https://example.com', foo='bar') + 'https://example.com?client_id=your_id&response_type=code&foo=bar' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 """ return prepare_grant_uri(uri, self.client_id, 'code', redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) - def prepare_request_body(self, code=None, body='', redirect_uri=None, **kwargs): + def prepare_request_body(self, client_id=None, code=None, body='', + redirect_uri=None, **kwargs): """Prepare the access token request body. The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format in the HTTP request entity-body: - grant_type - REQUIRED. Value MUST be set to "authorization_code". - code - REQUIRED. The authorization code received from the - authorization server. - redirect_uri - REQUIRED, if the "redirect_uri" parameter was included in the - authorization request as described in Section 4.1.1, and their - values MUST be identical. - - .. _`Section 4.1.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-4.1.1 + :param client_id: REQUIRED, if the client is not authenticating with the + authorization server as described in `Section 3.2.1`_. + + :param code: REQUIRED. The authorization code received from the + authorization server. + + :param redirect_uri: REQUIRED, if the "redirect_uri" parameter was included in the + authorization request as described in `Section 4.1.1`_, and their + values MUST be identical. + + :param kwargs: Extra parameters to include in the token request. + + In addition OAuthLib will add the ``grant_type`` parameter set to + ``authorization_code``. + + If the client type is confidential or the client was issued client + credentials (or assigned other authentication requirements), the + client MUST authenticate with the authorization server as described + in `Section 3.2.1`_:: + + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> client.prepare_request_body(code='sh35ksdf09sf') + 'grant_type=authorization_code&code=sh35ksdf09sf' + >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar') + 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar' + + .. _`Section 4.1.1`: http://tools.ietf.org/html/rfc6749#section-4.1.1 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 """ code = code or self.code return prepare_token_request('authorization_code', code=code, body=body, @@ -305,21 +341,41 @@ class WebApplicationClient(Client): adding the following parameters to the query component of the redirection URI using the "application/x-www-form-urlencoded" format: - code - REQUIRED. The authorization code generated by the - authorization server. The authorization code MUST expire - shortly after it is issued to mitigate the risk of leaks. A - maximum authorization code lifetime of 10 minutes is - RECOMMENDED. The client MUST NOT use the authorization code - more than once. If an authorization code is used more than - once, the authorization server MUST deny the request and SHOULD - revoke (when possible) all tokens previously issued based on - that authorization code. The authorization code is bound to - the client identifier and redirection URI. - state - REQUIRED if the "state" parameter was present in the client - authorization request. The exact value received from the - client. + :param uri: The callback URI that resulted from the user being redirected + back from the provider to you, the client. + :param state: The state provided in the authorization request. + + **code** + The authorization code generated by the authorization server. + The authorization code MUST expire shortly after it is issued + to mitigate the risk of leaks. A maximum authorization code + lifetime of 10 minutes is RECOMMENDED. The client MUST NOT + use the authorization code more than once. If an authorization + code is used more than once, the authorization server MUST deny + the request and SHOULD revoke (when possible) all tokens + previously issued based on that authorization code. + The authorization code is bound to the client identifier and + redirection URI. + + **state** + If the "state" parameter was present in the authorization request. + + This method is mainly intended to enforce strict state checking with + the added benefit of easily extracting parameters from the URI:: + + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> uri = 'https://example.com/callback?code=sdfkjh345&state=sfetw45' + >>> client.parse_request_uri_response(uri, state='sfetw45') + {'state': 'sfetw45', 'code': 'sdfkjh345'} + >>> client.parse_request_uri_response(uri, state='other') + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 357, in parse_request_uri_response + back from the provider to you, the client. + File "oauthlib/oauth2/draft25/parameters.py", line 153, in parse_authorization_code_response + raise MismatchingStateError() + oauthlib.oauth2.draft25.errors.MismatchingStateError """ response = parse_authorization_code_response(uri, state=state) self._populate_attributes(response) @@ -334,15 +390,97 @@ class WebApplicationClient(Client): authentication failed or is invalid, the authorization server returns an error response as described in `Section 5.2`_. - .. `Section 5.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.1 - .. `Section 5.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.2 + :param body: The response body from the token request. + :param scope: Scopes originally requested. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + These response are json encoded and could easily be parsed without + the assistance of OAuthLib. However, there are a few subtle issues + to be aware of regarding the response which are helpfully addressed + through the raising of various errors. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A normal response might look like:: + + >>> json.loads(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': 'hello world', + } + >>> from oauthlib.oauth2 import WebApplicationClient + >>> client = WebApplicationClient('your_id') + >>> client.parse_request_body_response(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': ['hello', 'world'], # note the list + } + + If there was a scope change you will be notified with a warning:: + + >>> client.parse_request_body_response(response_body, scope=['images']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + File "oauthlib/oauth2/draft25/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/draft25/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + If there was an error on the providers side you will be notified with + an error. For example, if there was no ``token_type`` provided:: + + >>> client.parse_request_body_response(response_body) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/draft25/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/draft25/parameters.py", line 276, in validate_token_parameters + raise MissingTokenTypeError() + oauthlib.oauth2.draft25.errors.MissingTokenTypeError + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) self._populate_attributes(self.token) return self.token -class UserAgentClient(Client): +class MobileApplicationClient(Client): """A public client utilizing the implicit code grant workflow. A user-agent-based application is a public client in which the @@ -381,23 +519,44 @@ class UserAgentClient(Client): The client constructs the request URI by adding the following parameters to the query component of the authorization endpoint URI - using the "application/x-www-form-urlencoded" format: - - response_type - REQUIRED. Value MUST be set to "token". - client_id - REQUIRED. The client identifier as described in Section 2.2. - redirect_uri - OPTIONAL. As described in Section 3.1.2. - scope - OPTIONAL. The scope of the access request as described by - Section 3.3. - state - RECOMMENDED. An opaque value used by the client to maintain - state between the request and callback. The authorization - server includes this value when redirecting the user-agent back - to the client. The parameter SHOULD be used for preventing - cross-site request forgery as described in Section 10.12. + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + :param redirect_uri: OPTIONAL. The redirect URI must be an absolute URI + and it should have been registerd with the OAuth + provider prior to use. As described in `Section 3.1.2`_. + + :param scope: OPTIONAL. The scope of the access request as described by + Section 3.3`_. These may be any string but are commonly + URIs or various categories such as ``videos`` or ``documents``. + + :param state: RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + :param kwargs: Extra arguments to include in the request URI. + + In addition to supplied parameters, OAuthLib will append the ``client_id`` + that was provided in the constructor as well as the mandatory ``response_type`` + argument, set to ``token``:: + + >>> from oauthlib.oauth2 import MobileApplicationClient + >>> client = MobileApplicationClient('your_id') + >>> client.prepare_request_uri('https://example.com') + 'https://example.com?client_id=your_id&response_type=token' + >>> client.prepare_request_uri('https://example.com', redirect_uri='https://a.b/callback') + 'https://example.com?client_id=your_id&response_type=token&redirect_uri=https%3A%2F%2Fa.b%2Fcallback' + >>> client.prepare_request_uri('https://example.com', scope=['profile', 'pictures']) + 'https://example.com?client_id=your_id&response_type=token&scope=profile+pictures' + >>> client.prepare_request_uri('https://example.com', foo='bar') + 'https://example.com?client_id=your_id&response_type=token&foo=bar' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 """ return prepare_grant_uri(uri, self.client_id, 'token', redirect_uri=redirect_uri, state=state, scope=scope, **kwargs) @@ -410,35 +569,81 @@ class UserAgentClient(Client): the following parameters to the fragment component of the redirection URI using the "application/x-www-form-urlencoded" format: - access_token - REQUIRED. The access token issued by the authorization server. - token_type - REQUIRED. The type of the token issued as described in - `Section 7.1`_. Value is case insensitive. - expires_in - RECOMMENDED. The lifetime in seconds of the access token. For - example, the value "3600" denotes that the access token will - expire in one hour from the time the response was generated. - If omitted, the authorization server SHOULD provide the - expiration time via other means or document the default value. - scope - OPTIONAL, if identical to the scope requested by the client, - otherwise REQUIRED. The scope of the access token as described - by `Section 3.3`_. - state - REQUIRED if the "state" parameter was present in the client - authorization request. The exact value received from the - client. - - .. _`Section 7.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-7.1 - .. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3 + :param uri: The callback URI that resulted from the user being redirected + back from the provider to you, the client. + :param state: The state provided in the authorization request. + :param scope: The scopes provided in the authorization request. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + **state** + If you provided the state parameter in the authorization phase, then + the provider is required to include that exact state value in the + response. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A few example responses can be seen below:: + + >>> response_uri = 'https://example.com/callback#access_token=sdlfkj452&state=ss345asyht&token_type=Bearer&scope=hello+world' + >>> from oauthlib.oauth2 import MobileApplicationClient + >>> client = MobileApplicationClient('your_id') + >>> client.parse_request_uri_response(response_uri) + { + 'access_token': 'sdlfkj452', + 'token_type': 'Bearer', + 'state': 'ss345asyht', + 'scope': [u'hello', u'world'] + } + >>> client.parse_request_uri_response(response_uri, state='other') + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 598, in parse_request_uri_response + **scope** + File "oauthlib/oauth2/draft25/parameters.py", line 197, in parse_implicit_response + raise ValueError("Mismatching or missing state in params.") + ValueError: Mismatching or missing state in params. + >>> client.parse_request_uri_response(response_uri, scope=['other']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 598, in parse_request_uri_response + **scope** + File "oauthlib/oauth2/draft25/parameters.py", line 199, in parse_implicit_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/draft25/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 """ self.token = parse_implicit_response(uri, state=state, scope=scope) self._populate_attributes(self.token) return self.token -class ClientCredentialsClient(Client): +class BackendApplicationClient(Client): """A public client utilizing the client credentials grant workflow. The client can request an access token using only its client @@ -460,15 +665,26 @@ class ClientCredentialsClient(Client): The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" - format in the HTTP request entity-body: + format per `Appendix B`_ in the HTTP request entity-body: - grant_type - REQUIRED. Value MUST be set to "client_credentials". - scope - OPTIONAL. The scope of the access request as described by - `Section 3.3`_. + :param scope: The scope of the access request as described by + `Section 3.3`_. + :param kwargs: Extra credentials to include in the token request. + + The client MUST authenticate with the authorization server as + described in `Section 3.2.1`_. + + The prepared body will include all provided credentials as well as + the ``grant_type`` parameter set to ``client_credentials``:: - .. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3 + >>> from oauthlib.oauth2 import BackendApplicationClient + >>> client = BackendApplicationClient('your_id') + >>> client.prepare_request_body(scope=['hello', 'world']) + 'grant_type=client_credentials&scope=hello+world' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 """ return prepare_token_request('client_credentials', body=body, scope=scope, **kwargs) @@ -482,15 +698,97 @@ class ClientCredentialsClient(Client): failed client authentication or is invalid, the authorization server returns an error response as described in `Section 5.2`_. - .. `Section 5.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.1 - .. `Section 5.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.2 + :param body: The response body from the token request. + :param scope: Scopes originally requested. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + These response are json encoded and could easily be parsed without + the assistance of OAuthLib. However, there are a few subtle issues + to be aware of regarding the response which are helpfully addressed + through the raising of various errors. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A normal response might look like:: + + >>> json.loads(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': 'hello world', + } + >>> from oauthlib.oauth2 import BackendApplicationClient + >>> client = BackendApplicationClient('your_id') + >>> client.parse_request_body_response(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': ['hello', 'world'], # note the list + } + + If there was a scope change you will be notified with a warning:: + + >>> client.parse_request_body_response(response_body, scope=['images']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + File "oauthlib/oauth2/draft25/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/draft25/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + If there was an error on the providers side you will be notified with + an error. For example, if there was no ``token_type`` provided:: + + >>> client.parse_request_body_response(response_body) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/draft25/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/draft25/parameters.py", line 276, in validate_token_parameters + raise MissingTokenTypeError() + oauthlib.oauth2.draft25.errors.MissingTokenTypeError + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) self._populate_attributes(self.token) return self.token -class PasswordCredentialsClient(Client): +class LegacyApplicationClient(Client): """A public client using the resource owner password and username directly. The resource owner password credentials grant type is suitable in @@ -512,32 +810,41 @@ class PasswordCredentialsClient(Client): MUST discard the credentials once an access token has been obtained. """ - def __init__(self, client_id, username, password, **kwargs): - super(PasswordCredentialsClient, self).__init__(client_id, **kwargs) - self.username = username - self.password = password + def __init__(self, client_id, **kwargs): + super(LegacyApplicationClient, self).__init__(client_id, **kwargs) - def prepare_request_body(self, body='', scope=None, **kwargs): + def prepare_request_body(self, username, password, body='', scope=None, **kwargs): """Add the resource owner password and username to the request body. The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" - format in the HTTP request entity-body: - - grant_type - REQUIRED. Value MUST be set to "password". - username - REQUIRED. The resource owner username. - password - REQUIRED. The resource owner password. - scope - OPTIONAL. The scope of the access request as described by - `Section 3.3`_. - - .. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3 + format per `Appendix B`_ in the HTTP request entity-body: + + :param username: The resource owner username. + :param password: The resource owner password. + :param scope: The scope of the access request as described by + `Section 3.3`_. + :param kwargs: Extra credentials to include in the token request. + + If the client type is confidential or the client was issued client + credentials (or assigned other authentication requirements), the + client MUST authenticate with the authorization server as described + in `Section 3.2.1`_. + + The prepared body will include all provided credentials as well as + the ``grant_type`` parameter set to ``password``:: + + >>> from oauthlib.oauth2 import LegacyApplicationClient + >>> client = LegacyApplicationClient('your_id') + >>> client.prepare_request_body(username='foo', password='bar', scope=['hello', 'world']) + 'grant_type=password&username=foo&scope=hello+world&password=bar' + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 """ - return prepare_token_request('password', body=body, username=self.username, - password=self.password, scope=scope, **kwargs) + return prepare_token_request('password', body=body, username=username, + password=password, scope=scope, **kwargs) def parse_request_body_response(self, body, scope=None): """Parse the JSON response body. @@ -548,14 +855,109 @@ class PasswordCredentialsClient(Client): authentication or is invalid, the authorization server returns an error response as described in `Section 5.2`_. - .. `Section 5.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.1 - .. `Section 5.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.2 + :param body: The response body from the token request. + :param scope: Scopes originally requested. + :return: Dictionary of token parameters. + :raises: Warning if scope has changed. OAuth2Error if response is invalid. + + These response are json encoded and could easily be parsed without + the assistance of OAuthLib. However, there are a few subtle issues + to be aware of regarding the response which are helpfully addressed + through the raising of various errors. + + A successful response should always contain + + **access_token** + The access token issued by the authorization server. Often + a random string. + + **token_type** + The type of the token issued as described in `Section 7.1`_. + Commonly ``Bearer``. + + While it is not mandated it is recommended that the provider include + + **expires_in** + The lifetime in seconds of the access token. For + example, the value "3600" denotes that the access token will + expire in one hour from the time the response was generated. + If omitted, the authorization server SHOULD provide the + expiration time via other means or document the default value. + + **scope** + Providers may supply this in all responses but are required to only + if it has changed since the authorization request. + + A normal response might look like:: + + >>> json.loads(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': 'hello world', + } + >>> from oauthlib.oauth2 import LegacyApplicationClient + >>> client = LegacyApplicationClient('your_id') + >>> client.parse_request_body_response(response_body) + { + 'access_token': 'sdfkjh345', + 'token_type': 'Bearer', + 'expires_in': '3600', + 'refresh_token': 'x345dgasd', + 'scope': ['hello', 'world'], # note the list + } + + If there was a scope change you will be notified with a warning:: + + >>> client.parse_request_body_response(response_body, scope=['images']) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + File "oauthlib/oauth2/draft25/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/draft25/parameters.py", line 285, in validate_token_parameters + raise Warning("Scope has changed to %s." % new_scope) + Warning: Scope has changed to [u'hello', u'world']. + + If there was an error on the providers side you will be notified with + an error. For example, if there was no ``token_type`` provided:: + + >>> client.parse_request_body_response(response_body) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/draft25/__init__.py", line 421, in parse_request_body_response + File "oauthlib/oauth2/draft25/parameters.py", line 263, in parse_token_response + validate_token_parameters(params, scope) + File "oauthlib/oauth2/draft25/parameters.py", line 276, in validate_token_parameters + raise MissingTokenTypeError() + oauthlib.oauth2.draft25.errors.MissingTokenTypeError + + .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 + .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 """ self.token = parse_token_response(body, scope=scope) self._populate_attributes(self.token) return self.token +# TODO(ib-lundgren): Deprecate these names +class UserAgentClient(MobileApplicationClient): + pass + + +class PasswordCredentialsClient(LegacyApplicationClient): + pass + + +class ClientCredentialsClient(BackendApplicationClient): + pass + + class AuthorizationEndpoint(object): """Authorization endpoint - used by the client to obtain authorization from the resource owner via user-agent redirection. @@ -568,24 +970,35 @@ class AuthorizationEndpoint(object): this specification. The endpoint URI MAY include an "application/x-www-form-urlencoded" - formatted (per Appendix B) query component ([RFC3986] section 3.4), + formatted (per `Appendix B`_) query component, which MUST be retained when adding additional query parameters. The - endpoint URI MUST NOT include a fragment component. + endpoint URI MUST NOT include a fragment component:: + + https://example.com/path?query=component # OK + https://example.com/path?query=component#fragment # Not OK Since requests to the authorization endpoint result in user authentication and the transmission of clear-text credentials (in the HTTP response), the authorization server MUST require the use of TLS as described in Section 1.6 when sending requests to the - authorization endpoint. + authorization endpoint:: + + # We will deny any request which URI schema is not with https The authorization server MUST support the use of the HTTP "GET" method [RFC2616] for the authorization endpoint, and MAY support the - use of the "POST" method as well. + use of the "POST" method as well:: + + # HTTP method is currently not enforced Parameters sent without a value MUST be treated as if they were omitted from the request. The authorization server MUST ignore unrecognized request parameters. Request and response parameters - MUST NOT be included more than once. + MUST NOT be included more than once:: + + # Enforced through the design of oauthlib.common.Request + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B """ def __init__(self, default_response_type, default_token_type, @@ -614,7 +1027,7 @@ class AuthorizationEndpoint(object): headers=None, scopes=None, credentials=None): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) - request.authorized_scopes = scopes # TODO: implement/test/doc this + request.scopes = scopes # TODO: decide whether this should be a required argument request.user = None # TODO: explain this in docs for k, v in (credentials or {}).items(): @@ -630,12 +1043,54 @@ class AuthorizationEndpoint(object): headers=None): """Extract response_type and route to the designated handler.""" request = Request(uri, http_method=http_method, body=body, headers=headers) + request.scopes = None response_type_handler = self.response_types.get( request.response_type, self.default_response_type_handler) return response_type_handler.validate_authorization_request(request) class TokenEndpoint(object): + """Token issuing endpoint. + + The token endpoint is used by the client to obtain an access token by + presenting its authorization grant or refresh token. The token + endpoint is used with every authorization grant except for the + implicit grant type (since an access token is issued directly). + + The means through which the client obtains the location of the token + endpoint are beyond the scope of this specification, but the location + is typically provided in the service documentation. + + The endpoint URI MAY include an "application/x-www-form-urlencoded" + formatted (per `Appendix B`_) query component, + which MUST be retained when adding additional query parameters. The + endpoint URI MUST NOT include a fragment component:: + + https://example.com/path?query=component # OK + https://example.com/path?query=component#fragment # Not OK + + Since requests to the authorization endpoint result in user + Since requests to the token endpoint result in the transmission of + clear-text credentials (in the HTTP request and response), the + authorization server MUST require the use of TLS as described in + Section 1.6 when sending requests to the token endpoint:: + + # We will deny any request which URI schema is not with https + + The client MUST use the HTTP "POST" method when making access token + requests:: + + # HTTP method is currently not enforced + + Parameters sent without a value MUST be treated as if they were + omitted from the request. The authorization server MUST ignore + unrecognized request parameters. Request and response parameters + MUST NOT be included more than once:: + + # Delegated to each grant type. + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + """ def __init__(self, default_grant_type, default_token_type, grant_types): self._grant_types = grant_types @@ -672,7 +1127,30 @@ class TokenEndpoint(object): class ResourceEndpoint(object): - + """Authorizes access to protected resources. + + The client accesses protected resources by presenting the access + token to the resource server. The resource server MUST validate the + access token and ensure that it has not expired and that its scope + covers the requested resource. The methods used by the resource + server to validate the access token (as well as any error responses) + are beyond the scope of this specification but generally involve an + interaction or coordination between the resource server and the + authorization server:: + + # For most cases, returning a 403 should suffice. + + The method in which the client utilizes the access token to + authenticate with the resource server depends on the type of access + token issued by the authorization server. Typically, it involves + using the HTTP "Authorization" request header field [RFC2617] with an + authentication scheme defined by the specification of the access + token type used, such as [RFC6750]:: + + # Access tokens may also be provided in query and body + https://example.com/protected?access_token=kjfch2345sdf # Query + access_token=sdf23409df # Body + """ def __init__(self, default_token, token_types): self._tokens = token_types self._default_token = default_token @@ -745,6 +1223,13 @@ class WebApplicationServer(AuthorizationEndpoint, TokenEndpoint, ResourceEndpoin """An all-in-one endpoint featuring Authorization code grant and Bearer tokens.""" def __init__(self, request_validator, token_generator=None, **kwargs): + """Construct a new web application server. + + :param request_validator: An implementation of oauthlib.oauth2.RequestValidator. + :param token_generator: A function to generate a token from a request. + :param kwargs: Extra parameters to pass to authorization endpoint, + token endpoint and resource endpoint constructors. + """ auth_grant = grant_types.AuthorizationCodeGrant(request_validator) refresh_grant = grant_types.RefreshTokenGrant(request_validator) bearer = tokens.BearerToken(request_validator, token_generator) diff --git a/oauthlib/oauth2/draft25/grant_types.py b/oauthlib/oauth2/draft25/grant_types.py index 349556b..5d4787b 100644 --- a/oauthlib/oauth2/draft25/grant_types.py +++ b/oauthlib/oauth2/draft25/grant_types.py @@ -136,8 +136,9 @@ class RequestValidator(object): - a resource owner / user (request.user) - authorized scopes (request.scopes) - The authorization code grant dict (code) holds at least the key 'code', - {'code': 'sdf345jsdf0934f'}. + The authorization code grant dict (code) holds at least the key 'code':: + + {'code': 'sdf345jsdf0934f'} :param client_id: Unicode client identifier :param code: A dict of the authorization code grant. @@ -159,15 +160,16 @@ class RequestValidator(object): - an expiration time - a refresh token, if issued - The Bearer token dict may hold a number of items, - { - 'token_type': 'Bearer', - 'token': 'askfjh234as9sd8', - 'expires_in': 3600, - 'scope': ['list', 'of', 'authorized', 'scopes'], - 'refresh_token': '23sdf876234', # if issued - 'state': 'given_by_client', # if supplied by client - } + The Bearer token dict may hold a number of items:: + + { + 'token_type': 'Bearer', + 'token': 'askfjh234as9sd8', + 'expires_in': 3600, + 'scope': ['list', 'of', 'authorized', 'scopes'], + 'refresh_token': '23sdf876234', # if issued + 'state': 'given_by_client', # if supplied by client + } :param client_id: Unicode client identifier :param token: A Bearer token dict @@ -361,8 +363,9 @@ class GrantTypeBase(object): raise errors.UnauthorizedClientError() def validate_scopes(self, request): - request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list( - self.request_validator.get_default_scopes(request.client_id, request)) + if not request.scopes: + request.scopes = utils.scope_to_list(request.scope) or utils.scope_to_list( + self.request_validator.get_default_scopes(request.client_id, request)) log.debug('Validating access to scopes %r for client %r (%r).', request.scopes, request.client_id, request.client) if not self.request_validator.validate_scopes(request.client_id, @@ -371,7 +374,80 @@ class GrantTypeBase(object): class AuthorizationCodeGrant(GrantTypeBase): - + """`Authorization Code Grant`_ + + The authorization code grant type is used to obtain both access + tokens and refresh tokens and is optimized for confidential clients. + Since this is a redirection-based flow, the client must be capable of + interacting with the resource owner's user-agent (typically a web + browser) and capable of receiving incoming requests (via redirection) + from the authorization server:: + + +----------+ + | Resource | + | Owner | + | | + +----------+ + ^ + | + (B) + +----|-----+ Client Identifier +---------------+ + | -+----(A)-- & Redirection URI ---->| | + | User- | | Authorization | + | Agent -+----(B)-- User authenticates --->| Server | + | | | | + | -+----(C)-- Authorization Code ---<| | + +-|----|---+ +---------------+ + | | ^ v + (A) (C) | | + | | | | + ^ v | | + +---------+ | | + | |>---(D)-- Authorization Code ---------' | + | Client | & Redirection URI | + | | | + | |<---(E)----- Access Token -------------------' + +---------+ (w/ Optional Refresh Token) + + Note: The lines illustrating steps (A), (B), and (C) are broken into + two parts as they pass through the user-agent. + + Figure 3: Authorization Code Flow + + The flow illustrated in Figure 3 includes the following steps: + + (A) The client initiates the flow by directing the resource owner's + user-agent to the authorization endpoint. The client includes + its client identifier, requested scope, local state, and a + redirection URI to which the authorization server will send the + user-agent back once access is granted (or denied). + + (B) The authorization server authenticates the resource owner (via + the user-agent) and establishes whether the resource owner + grants or denies the client's access request. + + (C) Assuming the resource owner grants access, the authorization + server redirects the user-agent back to the client using the + redirection URI provided earlier (in the request or during + client registration). The redirection URI includes an + authorization code and any local state provided by the client + earlier. + + (D) The client requests an access token from the authorization + server's token endpoint by including the authorization code + received in the previous step. When making the request, the + client authenticates with the authorization server. The client + includes the redirection URI used to obtain the authorization + code for verification. + + (E) The authorization server authenticates the client, validates the + authorization code, and ensures that the redirection URI + received matches the URI used to redirect the client in + step (C). If valid, the authorization server responds back with + an access token and, optionally, a refresh token. + + .. _`Authorization Code Grant`: http://tools.ietf.org/html/rfc6749#section-4.1 + """ def __init__(self, request_validator=None): self.request_validator = request_validator or RequestValidator() @@ -385,7 +461,83 @@ class AuthorizationCodeGrant(GrantTypeBase): return grant def create_authorization_response(self, request, token_handler): + """ + The client constructs the request URI by adding the following + parameters to the query component of the authorization endpoint URI + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + response_type + REQUIRED. Value MUST be set to "code". + client_id + REQUIRED. The client identifier as described in `Section 2.2`_. + redirect_uri + OPTIONAL. As described in `Section 3.1.2`_. + scope + OPTIONAL. The scope of the access request as described by + `Section 3.3`_. + state + RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + The client directs the resource owner to the constructed URI using an + HTTP redirection response, or by other means available to it via the + user-agent. + + :param request: oauthlib.commong.Request + :param token_handler: A token handler instace, for example of type + oauthlib.oauth2.BearerToken. + :returns: uri, headers, body, status + :raises: FatalClientError on invalid redirect URI or client id. + ValueError if scopes are not set on the request object. + + A few examples:: + + >>> from your_validator import your_validator + >>> request = Request('https://example.com/authorize?client_id=valid' + ... '&redirect_uri=http%3A%2F%2Fclient.com%2F') + >>> from oauthlib.common import Request + >>> from oauthlib.oauth2 import AuthorizationCodeGrant, BearerToken + >>> token = BearerToken(your_validator) + >>> grant = AuthorizationCodeGrant(your_validator) + >>> grant.create_authorization_response(request, token) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/grant_types.py", line 513, in create_authorization_response + raise ValueError('Scopes must be set on post auth.') + ValueError: Scopes must be set on post auth. + >>> request.scopes = ['authorized', 'in', 'some', 'form'] + >>> grant.create_authorization_response(request, token) + (u'http://client.com/?error=invalid_request&error_description=Missing+response_type+parameter.', None, None, 400) + >>> request = Request('https://example.com/authorize?client_id=valid' + ... '&redirect_uri=http%3A%2F%2Fclient.com%2F' + ... '&response_type=code') + >>> request.scopes = ['authorized', 'in', 'some', 'form'] + >>> grant.create_authorization_response(request, token) + (u'http://client.com/?code=u3F05aEObJuP2k7DordviIgW5wl52N', None, None, 200) + >>> # If the client id or redirect uri fails validation + >>> grant.create_authorization_response(request, token) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "oauthlib/oauth2/draft25/grant_types.py", line 515, in create_authorization_response + >>> grant.create_authorization_response(request, token) + File "oauthlib/oauth2/draft25/grant_types.py", line 591, in validate_authorization_request + oauthlib.oauth2.draft25.errors.InvalidClientIdError + + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + """ try: + # request.scopes is only mandated in post auth and both pre and + # post auth use validate_authorization_request + if not request.scopes: + raise ValueError('Scopes must be set on post auth.') + self.validate_authorization_request(request) log.debug('Pre resource owner authorization validation ok for %r.', request) @@ -414,7 +566,7 @@ class AuthorizationCodeGrant(GrantTypeBase): grant = self.create_authorization_code(request) logging.debug('Saving grant %r for %r.', grant, request) self.request_validator.save_authorization_code(request, grant) - return common.add_params_to_uri(request.redirect_uri, grant.items()), None, None, 200 + return common.add_params_to_uri(request.redirect_uri, grant.items()), None, None, 302 def create_token_response(self, request, token_handler): """Validate the authorization code. @@ -425,14 +577,19 @@ class AuthorizationCodeGrant(GrantTypeBase): previously issued based on that authorization code. The authorization code is bound to the client identifier and redirection URI. """ + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } try: self.validate_token_request(request) log.debug('Token request validation ok for %r.', request) except errors.OAuth2Error as e: log.debug('Client error during validation of %r. %r.', request, e) - return None, {}, e.json, e.status_code + return None, headers, e.json, e.status_code - return None, {}, json.dumps(token_handler.create_token(request, refresh_token=True)), 200 + return None, headers, json.dumps(token_handler.create_token(request, refresh_token=True)), 200 def validate_authorization_request(self, request): """Check the authorization request for normal and fatal errors. @@ -518,6 +675,7 @@ class AuthorizationCodeGrant(GrantTypeBase): 'client_id': request.client_id, 'redirect_uri': request.redirect_uri, 'response_type': request.response_type, + 'state': request.state, } def validate_token_request(self, request): @@ -587,54 +745,128 @@ class ImplicitGrant(GrantTypeBase): relies on the presence of the resource owner and the registration of the redirection URI. Because the access token is encoded into the redirection URI, it may be exposed to the resource owner and other - applications residing on the same device. - - See `Sections 10.3`_ and `10.16`_ for important security considerations + applications residing on the same device:: + + +----------+ + | Resource | + | Owner | + | | + +----------+ + ^ + | + (B) + +----|-----+ Client Identifier +---------------+ + | -+----(A)-- & Redirection URI --->| | + | User- | | Authorization | + | Agent -|----(B)-- User authenticates -->| Server | + | | | | + | |<---(C)--- Redirection URI ----<| | + | | with Access Token +---------------+ + | | in Fragment + | | +---------------+ + | |----(D)--- Redirection URI ---->| Web-Hosted | + | | without Fragment | Client | + | | | Resource | + | (F) |<---(E)------- Script ---------<| | + | | +---------------+ + +-|--------+ + | | + (A) (G) Access Token + | | + ^ v + +---------+ + | | + | Client | + | | + +---------+ + + Note: The lines illustrating steps (A) and (B) are broken into two + parts as they pass through the user-agent. + + Figure 4: Implicit Grant Flow + + The flow illustrated in Figure 4 includes the following steps: + + (A) The client initiates the flow by directing the resource owner's + user-agent to the authorization endpoint. The client includes + its client identifier, requested scope, local state, and a + redirection URI to which the authorization server will send the + user-agent back once access is granted (or denied). + + (B) The authorization server authenticates the resource owner (via + the user-agent) and establishes whether the resource owner + grants or denies the client's access request. + + (C) Assuming the resource owner grants access, the authorization + server redirects the user-agent back to the client using the + redirection URI provided earlier. The redirection URI includes + the access token in the URI fragment. + + (D) The user-agent follows the redirection instructions by making a + request to the web-hosted client resource (which does not + include the fragment per [RFC2616]). The user-agent retains the + fragment information locally. + + (E) The web-hosted client resource returns a web page (typically an + HTML document with an embedded script) capable of accessing the + full redirection URI including the fragment retained by the + user-agent, and extracting the access token (and other + parameters) contained in the fragment. + + (F) The user-agent executes the script provided by the web-hosted + client resource locally, which extracts the access token. + + (G) The user-agent passes the access token to the client. + + See `Section 10.3`_ and `Section 10.16`_ for important security considerations when using the implicit grant. - The client constructs the request URI by adding the following - parameters to the query component of the authorization endpoint URI - using the "application/x-www-form-urlencoded" format, per `Appendix B`_: - - response_type - REQUIRED. Value MUST be set to "token". - - client_id - REQUIRED. The client identifier as described in `Section 2.2`_. - - redirect_uri - OPTIONAL. As described in `Section 3.1.2`_. - - scope - OPTIONAL. The scope of the access request as described by - `Section 3.3`_. - - state - RECOMMENDED. An opaque value used by the client to maintain - state between the request and callback. The authorization - server includes this value when redirecting the user-agent back - to the client. The parameter SHOULD be used for preventing - cross-site request forgery as described in `Section 10.12`_. - - The authorization server validates the request to ensure that all - required parameters are present and valid. The authorization server - MUST verify that the redirection URI to which it will redirect the - access token matches a redirection URI registered by the client as - described in `Section 3.1.2`_. - .. _`Implicit Grant`: http://tools.ietf.org/html/rfc6749#section-4.2 - .. _`10.16`: http://tools.ietf.org/html/rfc6749#section-10.16 - .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 - .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 .. _`Section 10.3`: http://tools.ietf.org/html/rfc6749#section-10.3 - .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + .. _`Section 10.16`: http://tools.ietf.org/html/rfc6749#section-10.16 """ def __init__(self, request_validator=None): self.request_validator = request_validator or RequestValidator() def create_authorization_response(self, request, token_handler): + """Create an authorization response. + The client constructs the request URI by adding the following + parameters to the query component of the authorization endpoint URI + using the "application/x-www-form-urlencoded" format, per `Appendix B`_: + + response_type + REQUIRED. Value MUST be set to "token". + + client_id + REQUIRED. The client identifier as described in `Section 2.2`_. + + redirect_uri + OPTIONAL. As described in `Section 3.1.2`_. + + scope + OPTIONAL. The scope of the access request as described by + `Section 3.3`_. + + state + RECOMMENDED. An opaque value used by the client to maintain + state between the request and callback. The authorization + server includes this value when redirecting the user-agent back + to the client. The parameter SHOULD be used for preventing + cross-site request forgery as described in `Section 10.12`_. + + The authorization server validates the request to ensure that all + required parameters are present and valid. The authorization server + MUST verify that the redirection URI to which it will redirect the + access token matches a redirection URI registered by the client as + described in `Section 3.1.2`_. + + .. _`Section 2.2`: http://tools.ietf.org/html/rfc6749#section-2.2 + .. _`Section 3.1.2`: http://tools.ietf.org/html/rfc6749#section-3.1.2 + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 10.12`: http://tools.ietf.org/html/rfc6749#section-10.12 + .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B + """ return self.create_token_response(request, token_handler) def create_token_response(self, request, token_handler): @@ -674,9 +906,14 @@ class ImplicitGrant(GrantTypeBase): .. _`Appendix B`: http://tools.ietf.org/html/rfc6749#appendix-B .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 7.2`: http://tools.ietf.org/html/rfc6749#section-7.2 + .. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1 """ try: + # request.scopes is only mandated in post auth and both pre and + # post auth use validate_authorization_request + if not request.scopes: + raise ValueError('Scopes must be set on post auth.') + self.validate_token_request(request) # If the request fails due to a missing, invalid, or mismatching @@ -702,7 +939,7 @@ class ImplicitGrant(GrantTypeBase): token = token_handler.create_token(request, refresh_token=False) return common.add_params_to_uri(request.redirect_uri, token.items(), - fragment=True), {}, None, 200 + fragment=True), {}, None, 302 def validate_authorization_request(self, request): return self.validate_token_request(request) @@ -801,65 +1038,62 @@ class ImplicitGrant(GrantTypeBase): 'client_id': request.client_id, 'redirect_uri': request.redirect_uri, 'response_type': request.response_type, + 'state': request.state, } class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): """`Resource Owner Password Credentials Grant`_ - The client makes a request to the token endpoint by adding the - following parameters using the "application/x-www-form-urlencoded" - format per Appendix B with a character encoding of UTF-8 in the HTTP - request entity-body: - - grant_type - REQUIRED. Value MUST be set to "password". - - username - REQUIRED. The resource owner username. - - password - REQUIRED. The resource owner password. - - scope - OPTIONAL. The scope of the access request as described by - `Section 3.3`_. - - If the client type is confidential or the client was issued client - credentials (or assigned other authentication requirements), the - client MUST authenticate with the authorization server as described - in `Section 3.2.1`_. - - For example, the client makes the following HTTP request using - transport-layer security (with extra line breaks for display purposes - only): - - POST /token HTTP/1.1 - Host: server.example.com - Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW - Content-Type: application/x-www-form-urlencoded - - grant_type=password&username=johndoe&password=A3ddj3w - - The authorization server MUST: - - o require client authentication for confidential clients or for any - client that was issued client credentials (or with other - authentication requirements), - - o authenticate the client if client authentication is included, and - - o validate the resource owner password credentials using its - existing password validation algorithm. - - Since this access token request utilizes the resource owner's - password, the authorization server MUST protect the endpoint against - brute force attacks (e.g., using rate-limitation or generating - alerts). + The resource owner password credentials grant type is suitable in + cases where the resource owner has a trust relationship with the + client, such as the device operating system or a highly privileged + application. The authorization server should take special care when + enabling this grant type and only allow it when other flows are not + viable. + + This grant type is suitable for clients capable of obtaining the + resource owner's credentials (username and password, typically using + an interactive form). It is also used to migrate existing clients + using direct authentication schemes such as HTTP Basic or Digest + authentication to OAuth by converting the stored credentials to an + access token:: + + +----------+ + | Resource | + | Owner | + | | + +----------+ + v + | Resource Owner + (A) Password Credentials + | + v + +---------+ +---------------+ + | |>--(B)---- Resource Owner ------->| | + | | Password Credentials | Authorization | + | Client | | Server | + | |<--(C)---- Access Token ---------<| | + | | (w/ Optional Refresh Token) | | + +---------+ +---------------+ + + Figure 5: Resource Owner Password Credentials Flow + + The flow illustrated in Figure 5 includes the following steps: + + (A) The resource owner provides the client with its username and + password. + + (B) The client requests an access token from the authorization + server's token endpoint by including the credentials received + from the resource owner. When making the request, the client + authenticates with the authorization server. + + (C) The authorization server authenticates the client and validates + the resource owner credentials, and if valid, issues an access + token. .. _`Resource Owner Password Credentials Grant`: http://tools.ietf.org/html/rfc6749#section-4.3 - .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 - .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 """ def __init__(self, request_validator=None): @@ -878,6 +1112,11 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 """ + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } try: if require_authentication: log.debug('Authenticating client, %r.', request) @@ -896,14 +1135,57 @@ class ResourceOwnerPasswordCredentialsGrant(GrantTypeBase): self.validate_token_request(request) except errors.OAuth2Error as e: log.debug('Client error in token request, %s.', e) - return None, {}, e.json, e.status_code + return None, headers, e.json, e.status_code token = token_handler.create_token(request, refresh_token=True) log.debug('Issuing token %r to client id %r (%r) and username %s.', token, request.client_id, request.client, request.username) - return None, {}, json.dumps(token), 200 + return None, headers, json.dumps(token), 200 def validate_token_request(self, request): + """ + The client makes a request to the token endpoint by adding the + following parameters using the "application/x-www-form-urlencoded" + format per Appendix B with a character encoding of UTF-8 in the HTTP + request entity-body: + + grant_type + REQUIRED. Value MUST be set to "password". + + username + REQUIRED. The resource owner username. + + password + REQUIRED. The resource owner password. + + scope + OPTIONAL. The scope of the access request as described by + `Section 3.3`_. + + If the client type is confidential or the client was issued client + credentials (or assigned other authentication requirements), the + client MUST authenticate with the authorization server as described + in `Section 3.2.1`_. + + The authorization server MUST: + + o require client authentication for confidential clients or for any + client that was issued client credentials (or with other + authentication requirements), + + o authenticate the client if client authentication is included, and + + o validate the resource owner password credentials using its + existing password validation algorithm. + + Since this access token request utilizes the resource owner's + password, the authorization server MUST protect the endpoint against + brute force attacks (e.g., using rate-limitation or generating + alerts). + + .. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3 + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + """ for param in ('grant_type', 'username', 'password'): if not getattr(request, param): raise errors.InvalidRequestError( @@ -946,17 +1228,17 @@ class ClientCredentialsGrant(GrantTypeBase): the scope of this specification). The client credentials grant type MUST only be used by confidential - clients. + clients:: +---------+ +---------------+ - | | | | - | |>--(A)- Client Authentication --->| Authorization | - | Client | | Server | - | |<--(B)---- Access Token ---------<| | - | | | | + : : : : + : :>-- A - Client Authentication --->: Authorization : + : Client : : Server : + : :<-- B ---- Access Token ---------<: : + : : : : +---------+ +---------------+ - Figure 6: Client Credentials Flow + Figure 6: Client Credentials Flow The flow illustrated in Figure 6 includes the following steps: @@ -1052,17 +1334,22 @@ class RefreshTokenGrant(GrantTypeBase): .. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1 .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2 """ + headers = { + 'Content-Type': 'application/json;charset=UTF-8', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + } try: log.debug('Validating refresh token request, %r.', request) self.validate_token_request(request) except errors.OAuth2Error as e: - return None, {}, e.json, 400 + return None, headers, e.json, 400 token = token_handler.create_token(request, refresh_token=self.issue_new_refresh_tokens) log.debug('Issuing new token to client id %r (%r), %r.', request.client_id, request.client, token) - return None, {}, json.dumps(token), 200 + return None, headers, json.dumps(token), 200 def validate_token_request(self, request): # REQUIRED. Value MUST be set to "refresh_token". diff --git a/oauthlib/oauth2/draft25/parameters.py b/oauthlib/oauth2/draft25/parameters.py index 2b3c02a..d171305 100644 --- a/oauthlib/oauth2/draft25/parameters.py +++ b/oauthlib/oauth2/draft25/parameters.py @@ -143,6 +143,9 @@ def parse_authorization_code_response(uri, state=None): &state=xyz """ + if not uri.lower().startswith('https://'): + raise InsecureTransportError() + query = urlparse.urlparse(uri).query params = dict(urlparse.parse_qsl(query)) @@ -187,6 +190,9 @@ def parse_implicit_response(uri, state=None, scope=None): Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600 """ + if not uri.lower().startswith('https://'): + raise InsecureTransportError() + fragment = urlparse.urlparse(uri).fragment params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True)) diff --git a/oauthlib/oauth2/draft25/tokens.py b/oauthlib/oauth2/draft25/tokens.py index 2c034d6..1cfc29e 100644 --- a/oauthlib/oauth2/draft25/tokens.py +++ b/oauthlib/oauth2/draft25/tokens.py @@ -126,7 +126,7 @@ def prepare_bearer_uri(token, uri): http://www.example.com/path?access_token=h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18 + .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 """ return add_params_to_uri(uri, [(('access_token', token))]) @@ -137,7 +137,7 @@ def prepare_bearer_headers(token, headers=None): Authorization: Bearer h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18 + .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 """ headers = headers or {} headers['Authorization'] = 'Bearer %s' % token @@ -149,7 +149,7 @@ def prepare_bearer_body(token, body=''): access_token=h480djs93hd8 - .. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18 + .. _`Bearer Token`: http://tools.ietf.org/html/rfc6750 """ return add_params_to_qs(body, [(('access_token', token))]) diff --git a/oauthlib/oauth2/ext/django.py b/oauthlib/oauth2/ext/django.py index ce14bc0..9cc915d 100644 --- a/oauthlib/oauth2/ext/django.py +++ b/oauthlib/oauth2/ext/django.py @@ -49,9 +49,6 @@ class OAuth2ProviderDecorator(object): except errors.FatalClientError as e: log.debug('Fatal client error, redirecting to error page.') return HttpResponseRedirect(e.in_uri(self._error_uri)) - except errors.OAuth2Error as e: - log.debug('Client error, redirecting back to client.') - return HttpResponseRedirect(e.in_uri(redirect_uri)) return wrapper def post_authorization_view(self, f): @@ -90,9 +87,6 @@ class OAuth2ProviderDecorator(object): response = HttpResponse(content=body, status=status) for k, v in headers: response[k] = v - response['Content-Type'] = 'application/json;charset=UTF-8' - response['Cache-Control'] = 'no-store' - response['Pragma'] = 'no-cache' return response return wrapper diff --git a/tests/oauth2/draft25/test_client.py b/tests/oauth2/draft25/test_client.py index 2d3248d..47ed538 100644 --- a/tests/oauth2/draft25/test_client.py +++ b/tests/oauth2/draft25/test_client.py @@ -371,21 +371,21 @@ class PasswordCredentialsClientTest(TestCase): } def test_request_body(self): - client = PasswordCredentialsClient(self.client_id, self.username, - self.password) + client = PasswordCredentialsClient(self.client_id) # Basic, no extra arguments - body = client.prepare_request_body(body=self.body) + body = client.prepare_request_body(self.username, self.password, + body=self.body) self.assertFormBodyEqual(body, self.body_up) # With extra parameters, checked using length since order of # dict items is undefined - body = client.prepare_request_body(body=self.body, **self.kwargs) + body = client.prepare_request_body(self.username, self.password, + body=self.body, **self.kwargs) self.assertEqual(len(body), len(self.body_kwargs)) def test_parse_token_response(self): - client = PasswordCredentialsClient(self.client_id, self.username, - self.password) + client = PasswordCredentialsClient(self.client_id) # Parse code and state response = client.parse_request_body_response(self.token_json, scope=self.scope) diff --git a/tests/oauth2/draft25/test_grant_types.py b/tests/oauth2/draft25/test_grant_types.py index 549644d..4040db9 100644 --- a/tests/oauth2/draft25/test_grant_types.py +++ b/tests/oauth2/draft25/test_grant_types.py @@ -123,7 +123,7 @@ class ImplicitGrantTest(TestCase): common.generate_token = lambda *args, **kwargs: '1234' uri, headers, body, status_code = self.auth.create_token_response( self.request, bearer) - correct_uri = 'https://b.c/p#access_token=1234&token_type=Bearer&expires_in=3600&state=xyz' + correct_uri = 'https://b.c/p#access_token=1234&token_type=Bearer&expires_in=3600&state=xyz&scope=hello+world' self.assertURLEqual(uri, correct_uri, parse_fragment=True) def test_error_response(self): diff --git a/tests/oauth2/draft25/test_server.py b/tests/oauth2/draft25/test_server.py index 329e57c..46298c2 100644 --- a/tests/oauth2/draft25/test_server.py +++ b/tests/oauth2/draft25/test_server.py @@ -34,14 +34,16 @@ class AuthorizationEndpointTest(TestCase): def test_authorization_grant(self): uri = 'http://i.b/l?response_type=code&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' - uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + uri, headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) self.assertURLEqual(uri, 'http://back.to/me?code=abc&state=xyz') @mock.patch('oauthlib.common.generate_token', new=lambda: 'abc') def test_implicit_grant(self): uri = 'http://i.b/l?response_type=token&client_id=me&scope=all+of+them&state=xyz' uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' - uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + uri, headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) self.assertURLEqual(uri, 'http://back.to/me#access_token=abc&expires_in=3600&token_type=Bearer&state=xyz&scope=all+of+them', parse_fragment=True) def test_missing_type(self): @@ -49,7 +51,8 @@ class AuthorizationEndpointTest(TestCase): uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( side_effect=errors.InvalidRequestError()) - uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + uri, headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) self.assertURLEqual(uri, 'http://back.to/me?error=invalid_request&error_description=Missing+response_type+parameter.') def test_invalid_type(self): @@ -57,7 +60,8 @@ class AuthorizationEndpointTest(TestCase): uri += '&redirect_uri=http%3A%2F%2Fback.to%2Fme' self.mock_validator.validate_request = mock.MagicMock( side_effect=errors.UnsupportedResponseTypeError()) - uri, headers, body, status_code = self.endpoint.create_authorization_response(uri) + uri, headers, body, status_code = self.endpoint.create_authorization_response( + uri, scopes=['all', 'of', 'them']) self.assertURLEqual(uri, 'http://back.to/me?error=unsupported_response_type') diff --git a/tests/oauth2/draft25/test_servers.py b/tests/oauth2/draft25/test_servers.py index 57d8361..3686949 100644 --- a/tests/oauth2/draft25/test_servers.py +++ b/tests/oauth2/draft25/test_servers.py @@ -20,11 +20,13 @@ from oauthlib.oauth2.draft25 import errors def get_query_credentials(uri): - return urlparse.parse_qs(urlparse.urlparse(uri).query) + return urlparse.parse_qs(urlparse.urlparse(uri).query, + keep_blank_values=True) def get_fragment_credentials(uri): - return urlparse.parse_qs(urlparse.urlparse(uri).fragment) + return urlparse.parse_qs(urlparse.urlparse(uri).fragment, + keep_blank_values=True) class TestScopeHandling(TestCase): @@ -79,24 +81,22 @@ class TestScopeHandling(TestCase): def test_scope_preservation(self): scope = 'pics+http%3A%2f%2fa.b%2fvideos' - correct_scope = 'pics http%3A%2f%2fa.b%2fvideos' decoded_scope = 'pics http://a.b/videos' - scopes = ['pics', 'http%3A%2f%2fa.b%2fvideos'] - auth_uri = 'http://example.com/path?client_id=abc&scope=%s&%s' + auth_uri = 'http://example.com/path?client_id=abc&response_type=' token_uri = 'http://example.com/path' # authorization grant uri, _, _, _ = self.web.create_authorization_response( - auth_uri % (scope, 'response_type=code')) - self.validator.validate_code.side_effect = self.set_scopes(scopes) + auth_uri + 'code', scopes=decoded_scope.split(' ')) + self.validator.validate_code.side_effect = self.set_scopes(decoded_scope.split(' ')) code = get_query_credentials(uri)['code'][0] _, _, body, _ = self.web.create_token_response(token_uri, body='grant_type=authorization_code&code=%s' % code) - self.assertEqual(json.loads(body)['scope'], correct_scope) + self.assertEqual(json.loads(body)['scope'], decoded_scope) # implicit grant uri, _, _, _ = self.mobile.create_authorization_response( - auth_uri % (scope, 'response_type=token')) + auth_uri + 'token', scopes=decoded_scope.split(' ')) self.assertEqual(get_fragment_credentials(uri)['scope'][0], decoded_scope) # resource owner password credentials grant @@ -116,12 +116,12 @@ class TestScopeHandling(TestCase): scope = 'pics+http%3A%2f%2fa.b%2fvideos' scopes = ['images', 'http://a.b/videos'] decoded_scope = 'images http://a.b/videos' - auth_uri = 'http://example.com/path?client_id=abc&scope=%s&%s' + auth_uri = 'http://example.com/path?client_id=abc&response_type=' token_uri = 'http://example.com/path' # authorization grant uri, _, _, _ = self.web.create_authorization_response( - auth_uri % (scope, 'response_type=code')) + auth_uri + 'code', scopes=scopes) code = get_query_credentials(uri)['code'][0] self.validator.validate_code.side_effect = self.set_scopes(scopes) _, _, body, _ = self.web.create_token_response(token_uri, @@ -131,7 +131,7 @@ class TestScopeHandling(TestCase): # implicit grant self.validator.validate_scopes.side_effect = self.set_scopes(scopes) uri, _, _, _ = self.mobile.create_authorization_response( - auth_uri % (scope, 'response_type=token')) + auth_uri + 'token', scopes=scopes) self.assertEqual(get_fragment_credentials(uri)['scope'][0], decoded_scope) # resource owner password credentials grant @@ -151,20 +151,20 @@ class TestScopeHandling(TestCase): def test_invalid_scope(self): scope = 'pics+http%3A%2f%2fa.b%2fvideos' - auth_uri = 'http://example.com/path?client_id=abc&scope=%s&%s' + auth_uri = 'http://example.com/path?client_id=abc&response_type=' token_uri = 'http://example.com/path' self.validator.validate_scopes.return_value = False # authorization grant uri, _, _, _ = self.web.create_authorization_response( - auth_uri % (scope, 'response_type=code')) + auth_uri + 'code', scopes=['invalid']) error = get_query_credentials(uri)['error'][0] self.assertEqual(error, 'invalid_scope') # implicit grant uri, _, _, _ = self.mobile.create_authorization_response( - auth_uri % (scope, 'response_type=token')) + auth_uri + 'token', scopes=['invalid']) error = get_fragment_credentials(uri)['error'][0] self.assertEqual(error, 'invalid_scope') @@ -203,13 +203,12 @@ class PreservationTest(TestCase): return True def test_state_preservation(self): - scope = 'pics+http%3A%2f%2fa.b%2fvideos' - auth_uri = 'http://example.com/path?state=xyz&client_id=abc&scope=%s&%s' + auth_uri = 'http://example.com/path?state=xyz&client_id=abc&response_type=' token_uri = 'http://example.com/path' # authorization grant uri, _, _, _ = self.web.create_authorization_response( - auth_uri % (scope, 'response_type=code')) + auth_uri + 'code', scopes=['random']) code = get_query_credentials(uri)['code'][0] self.validator.validate_code.side_effect = self.set_state('xyz') _, _, body, _ = self.web.create_token_response(token_uri, @@ -218,7 +217,7 @@ class PreservationTest(TestCase): # implicit grant uri, _, _, _ = self.mobile.create_authorization_response( - auth_uri % (scope, 'response_type=token')) + auth_uri + 'token', scopes=['random']) self.assertEqual(get_fragment_credentials(uri)['state'][0], 'xyz') def test_redirect_uri_preservation(self): @@ -228,7 +227,7 @@ class PreservationTest(TestCase): # authorization grant uri, _, _, _ = self.web.create_authorization_response( - auth_uri + '&response_type=code') + auth_uri + '&response_type=code', scopes=['random']) self.assertTrue(uri.startswith(redirect_uri)) # confirm_redirect_uri should return false if the redirect uri @@ -241,7 +240,7 @@ class PreservationTest(TestCase): # implicit grant uri, _, _, _ = self.mobile.create_authorization_response( - auth_uri + '&response_type=token') + auth_uri + '&response_type=token', scopes=['random']) self.assertTrue(uri.startswith(redirect_uri)) def test_invalid_redirect_uri(self): @@ -251,12 +250,12 @@ class PreservationTest(TestCase): # authorization grant self.assertRaises(errors.MismatchingRedirectURIError, self.web.create_authorization_response, - auth_uri + '&response_type=code') + auth_uri + '&response_type=code', scopes=['random']) # implicit grant self.assertRaises(errors.MismatchingRedirectURIError, self.mobile.create_authorization_response, - auth_uri + '&response_type=token') + auth_uri + '&response_type=token', scopes=['random']) def test_default_uri(self): auth_uri = 'http://example.com/path?state=xyz&client_id=abc' @@ -266,12 +265,12 @@ class PreservationTest(TestCase): # authorization grant self.assertRaises(errors.MissingRedirectURIError, self.web.create_authorization_response, - auth_uri + '&response_type=code') + auth_uri + '&response_type=code', scopes=['random']) # implicit grant self.assertRaises(errors.MissingRedirectURIError, self.mobile.create_authorization_response, - auth_uri + '&response_type=token') + auth_uri + '&response_type=token', scopes=['random']) class ClientAuthenticationTest(TestCase): @@ -326,10 +325,11 @@ class ClientAuthenticationTest(TestCase): # implicit grant auth_uri = 'http://example.com/path?client_id=abc&response_type=token' - self.assertRaises(ValueError, self.mobile.create_authorization_response, auth_uri) + self.assertRaises(ValueError, self.mobile.create_authorization_response, + auth_uri, scopes=['random']) self.validator.validate_client_id.side_effect = self.set_client_id - uri, _, _, _ = self.mobile.create_authorization_response(auth_uri) + uri, _, _, _ = self.mobile.create_authorization_response(auth_uri, scopes=['random']) self.assertIn('access_token', get_fragment_credentials(uri)) def test_custom_authentication(self): @@ -399,7 +399,7 @@ class ResourceOwnerAssociationTest(TestCase): # TODO: code generator + intercept test uri, _, _, _ = self.web.create_authorization_response( self.auth_uri + '&response_type=code', - credentials={'user': 'test'}) + credentials={'user': 'test'}, scopes=['random']) code = get_query_credentials(uri)['code'][0] self.assertRaises(ValueError, self.web.create_token_response, self.token_uri, @@ -417,7 +417,7 @@ class ResourceOwnerAssociationTest(TestCase): uri, _, _, _ = self.mobile.create_authorization_response( self.auth_uri + '&response_type=token', - credentials={'user': 'test'}) + credentials={'user': 'test'}, scopes=['random']) self.assertEqual(get_fragment_credentials(uri)['access_token'][0], 'abc') def test_legacy_application(self): |