summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIb Lundgren <ib.lundgren@gmail.com>2012-06-28 14:32:32 +0200
committerIb Lundgren <ib.lundgren@gmail.com>2012-06-28 14:32:32 +0200
commitdb895be0dc532e410c302409a0a8c091f12a2aaa (patch)
treeee0b19c714a3dd4bf8c3eaf75af9f5cbb32fac4e
parent56b0cec481c7457a658b54f979f0fea3c1189c12 (diff)
downloadoauthlib-db895be0dc532e410c302409a0a8c091f12a2aaa.tar.gz
OAuth 2 parameter handlers
-rw-r--r--oauthlib/oauth2/draft25/parameters.py256
-rw-r--r--tests/oauth2/draft25/test_parameters.py160
2 files changed, 416 insertions, 0 deletions
diff --git a/oauthlib/oauth2/draft25/parameters.py b/oauthlib/oauth2/draft25/parameters.py
new file mode 100644
index 0000000..ecc9f63
--- /dev/null
+++ b/oauthlib/oauth2/draft25/parameters.py
@@ -0,0 +1,256 @@
+"""
+oauthlib.oauth2_draft28.parameters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This module contains methods related to `Section 4`_ of the OAuth 2 draft.
+
+.. _`Section 4`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-4
+"""
+
+import json
+import urlparse
+from oauthlib.common import add_params_to_uri, add_params_to_qs
+
+
+def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
+ scope=None, state=None, **kwargs):
+ """Prepare the authorization grant request URI.
+
+ 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`_.
+
+ GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
+ &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
+ Host: server.example.com
+
+ .. _`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
+ """
+ params = [((u'response_type', response_type)),
+ ((u'client_id', client_id))]
+
+ if redirect_uri:
+ params.append((u'redirect_uri', redirect_uri))
+ if scope:
+ params.append((u'scope', scope))
+ if state:
+ params.append((u'state', state))
+
+ for k in kwargs:
+ params.append((unicode(k), kwargs[k]))
+
+ return add_params_to_uri(uri, params)
+
+
+def prepare_token_request(grant_type, body=u'', **kwargs):
+ """Prepare the access token request.
+
+ 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.
+
+ grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
+ &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
+
+ .. _`Section 4.1.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-4.1.1
+ """
+ params = [(u'grant_type', grant_type)]
+ for k in kwargs:
+ params.append((unicode(k), kwargs[k]))
+
+ return add_params_to_qs(body, params)
+
+
+def parse_authorization_code_response(uri, state=None):
+ """Parse authorization grant response URI into a dict.
+
+ If the resource owner grants the access request, the authorization
+ server issues an authorization code and delivers it to the client by
+ 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.
+
+ For example, the authorization server redirects the user-agent by
+ sending the following HTTP response:
+
+ HTTP/1.1 302 Found
+ Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
+ &state=xyz
+
+ """
+ query = urlparse.urlparse(uri).query
+ params = dict(urlparse.parse_qsl(query))
+
+ if not u'code' in params:
+ raise KeyError("Missing code parameter in response.")
+
+ if state and params.get(u'state', None) != state:
+ raise ValueError("Mismatching or missing state in response.")
+
+ return params
+
+
+def parse_implicit_response(uri, state=None, scope=None):
+ """Parse the implicit token response URI into a dict.
+
+ If the resource owner grants the access request, the authorization
+ server issues an access token and delivers it to the client by adding
+ 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.
+
+ HTTP/1.1 302 Found
+ Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
+ &state=xyz&token_type=example&expires_in=3600
+ """
+ fragment = urlparse.urlparse(uri).fragment
+ params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
+ validate_token_parameters(params, scope)
+
+ if state and params.get(u'state', None) != state:
+ raise ValueError("Mismatching or missing state in params.")
+
+ return params
+
+
+def parse_token_response(body, scope=None):
+ """Parse the JSON token response body into a dict.
+
+ The authorization server issues an access token and optional refresh
+ token, and constructs the response by adding the following parameters
+ to the entity body of the HTTP response with a 200 (OK) status code:
+
+ 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.
+ refresh_token
+ OPTIONAL. The refresh token which can be used to obtain new
+ access tokens using the same authorization grant as described
+ in `Section 6`_.
+ 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`_.
+
+ The parameters are included in the entity body of the HTTP response
+ using the "application/json" media type as defined by [`RFC4627`_]. The
+ parameters are serialized into a JSON structure by adding each
+ parameter at the highest structure level. Parameter names and string
+ values are included as JSON strings. Numerical values are included
+ as JSON numbers. The order of parameters does not matter and can
+ vary.
+
+ For example:
+
+ HTTP/1.1 200 OK
+ Content-Type: application/json;charset=UTF-8
+ Cache-Control: no-store
+ Pragma: no-cache
+
+ {
+ "access_token":"2YotnFZFEjr1zCsicMWpAA",
+ "token_type":"example",
+ "expires_in":3600,
+ "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
+ "example_parameter":"example_value"
+ }
+
+ .. _`Section 7.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-7.1
+ .. _`Section 6`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-6
+ .. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3
+ .. _`RFC4627`: http://tools.ietf.org/html/rfc4627
+ """
+ params = json.loads(body)
+ validate_token_parameters(params, scope)
+ return params
+
+
+def validate_token_parameters(params, scope=None):
+ """Ensures token precence, token type, expiration and scope in params."""
+
+ if not u'access_token' in params:
+ raise KeyError("Missing access token parameter.")
+
+ if not u'token_type' in params:
+ raise KeyError("Missing token type parameter.")
+
+ # If the issued access token scope is different from the one requested by
+ # the client, the authorization server MUST include the "scope" response
+ # parameter to inform the client of the actual scope granted.
+ # http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3
+ new_scope = params.get(u'scope', None)
+ if scope and new_scope and scope != new_scope:
+ raise Warning("Scope has changed to %s." % new_scope)
diff --git a/tests/oauth2/draft25/test_parameters.py b/tests/oauth2/draft25/test_parameters.py
new file mode 100644
index 0000000..6edb143
--- /dev/null
+++ b/tests/oauth2/draft25/test_parameters.py
@@ -0,0 +1,160 @@
+from __future__ import absolute_import
+
+from ...unittest import TestCase
+from oauthlib.oauth2.draft25.parameters import *
+
+
+class ParameterTests(TestCase):
+
+ auth_grant = {
+ u'uri': u'http://server.example.com/authorize',
+ u'client_id': u's6BhdRkqt3',
+ u'response_type': u'code',
+ u'redirect_uri': u'https://client.example.com/cb',
+ u'scope': u'photos',
+ u'state': u'xyz'
+ }
+ auth_grant_uri = (u'http://server.example.com/authorize?response_type=code'
+ u'&client_id=s6BhdRkqt3&redirect_uri=https%3A%2F%2F'
+ u'client.example.com%2Fcb&scope=photos&state=xyz')
+
+ auth_implicit = {
+ u'uri': u'http://server.example.com/authorize',
+ u'client_id': u's6BhdRkqt3',
+ u'response_type': u'token',
+ u'redirect_uri': u'https://client.example.com/cb',
+ u'state': u'xyz',
+ u'extra': u'extra'
+ }
+ auth_implicit_uri = (u'http://server.example.com/authorize?'
+ u'response_type=token&client_id=s6BhdRkqt3&'
+ u'redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&'
+ u'state=xyz&extra=extra')
+
+ grant_body = {
+ u'grant_type': u'authorization_code',
+ u'code': u'SplxlOBeZQQYbYS6WxSbIA',
+ u'redirect_uri': u'https://client.example.com/cb'
+ }
+ auth_grant_body = (u'grant_type=authorization_code&'
+ u'code=SplxlOBeZQQYbYS6WxSbIA&'
+ u'redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb')
+
+ pwd_body = {
+ u'grant_type': u'password',
+ u'username': u'johndoe',
+ u'password': u'A3ddj3w'
+ }
+ password_body = u'grant_type=password&username=johndoe&password=A3ddj3w'
+
+ cred_grant = {
+ u'grant_type': u'client_credentials'
+ }
+ cred_body = u'grant_type=client_credentials'
+
+ grant_response = u'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz'
+
+ grant_dict = {
+ u'code': u'SplxlOBeZQQYbYS6WxSbIA',
+ u'state': u'xyz'
+ }
+
+ error_nocode = u'https://client.example.com/cb?state=xyz'
+ error_nostate = u'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA'
+ error_wrongstate = u'https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=abc'
+ error_response = u'https://client.example.com/cb?error=access_denied&state=xyz'
+
+ state = u'xyz'
+
+ implicit_response = (u'http://example.com/cb#access_token=2YotnFZFEjr1zCsi'
+ u'cMWpAA&state=xyz&token_type=example&expires_in=3600')
+ implicit_notoken = (u'http://example.com/cb#state=xyz&token_type=example&'
+ u'expires_in=3600')
+ implicit_notype = (u'http://example.com/cb#access_token=2YotnFZFEjr1zCsicM'
+ u'WpAA&state=xyz&expires_in=3600')
+ implicit_nostate = (u'http://example.com/cb#access_token=2YotnFZFEjr1zCsic'
+ u'MWpAA&token_type=example&expires_in=3600')
+ implicit_wrongstate = (u'http://example.com/cb#access_token=2YotnFZFEjr1zC'
+ u'sicMWpAA&state=abc&token_type=example&expires_in=3600')
+
+ implicit_dict = {
+ u'access_token': u'2YotnFZFEjr1zCsicMWpAA',
+ u'state': u'xyz',
+ u'token_type': 'example',
+ u'expires_in': u'3600',
+ }
+
+ json_response = (u'{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
+ u' "token_type": "example",'
+ u' "expires_in": 3600,'
+ u' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ u' "example_parameter": "example_value",'
+ u' "scope":"abc"}')
+
+ json_error = u'{ "error": "invalid_request" }'
+
+ json_notoken = (u'{ "token_type": "example",'
+ u' "expires_in": 3600,'
+ u' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ u' "example_parameter": "example_value" }')
+
+ json_notype = (u'{ "access_token": "2YotnFZFEjr1zCsicMWpAA",'
+ u' "expires_in": 3600,'
+ u' "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",'
+ u' "example_parameter": "example_value" }')
+
+ json_dict = {
+ u'access_token': u'2YotnFZFEjr1zCsicMWpAA',
+ u'token_type': u'example',
+ u'expires_in': 3600,
+ u'refresh_token': u'tGzv3JOkF0XG5Qx2TlKWIA',
+ u'example_parameter': u'example_value',
+ u'scope': u'abc'
+ }
+
+ def test_prepare_grant_uri(self):
+ """Verify correct authorization URI construction."""
+ self.assertEqual(prepare_grant_uri(**self.auth_grant), self.auth_grant_uri)
+ self.assertEqual(prepare_grant_uri(**self.auth_implicit), self.auth_implicit_uri)
+
+ def test_prepare_token_request(self):
+ """Verify correct access token request body construction."""
+ self.assertEqual(prepare_token_request(**self.grant_body), self.auth_grant_body)
+ self.assertEqual(prepare_token_request(**self.pwd_body), self.password_body)
+ self.assertEqual(prepare_token_request(**self.cred_grant), self.cred_body)
+
+ def test_grant_response(self):
+ """Verify correct parameter parsing and validation for auth code responses."""
+ params = parse_authorization_code_response(self.grant_response)
+ self.assertEqual(params, self.grant_dict)
+ params = parse_authorization_code_response(self.grant_response, state=self.state)
+ self.assertEqual(params, self.grant_dict)
+
+ self.assertRaises(KeyError, parse_authorization_code_response,
+ self.error_nocode)
+ self.assertRaises(KeyError, parse_authorization_code_response,
+ self.error_response)
+ self.assertRaises(ValueError, parse_authorization_code_response,
+ self.error_nostate, state=self.state)
+ self.assertRaises(ValueError, parse_authorization_code_response,
+ self.error_wrongstate, state=self.state)
+
+ def test_implicit_token_response(self):
+ """Verify correct parameter parsing and validation for implicit responses."""
+ self.assertEqual(parse_implicit_response(self.implicit_response),
+ self.implicit_dict)
+ self.assertRaises(KeyError, parse_implicit_response,
+ self.implicit_notoken)
+ self.assertRaises(KeyError, parse_implicit_response,
+ self.implicit_notype)
+ self.assertRaises(ValueError, parse_implicit_response,
+ self.implicit_nostate, state=self.state)
+ self.assertRaises(ValueError, parse_implicit_response,
+ self.implicit_wrongstate, state=self.state)
+
+ def test_json_token_response(self):
+ """Verify correct parameter parsing and validation for token responses. """
+ self.assertEqual(parse_token_response(self.json_response), self.json_dict)
+ self.assertRaises(KeyError, parse_token_response, self.json_error)
+ self.assertRaises(KeyError, parse_token_response, self.json_notoken)
+ self.assertRaises(Warning, parse_token_response, self.json_response, scope=u'aaa')