diff options
author | Ib Lundgren <ib.lundgren@gmail.com> | 2012-06-28 14:32:32 +0200 |
---|---|---|
committer | Ib Lundgren <ib.lundgren@gmail.com> | 2012-06-28 14:32:32 +0200 |
commit | db895be0dc532e410c302409a0a8c091f12a2aaa (patch) | |
tree | ee0b19c714a3dd4bf8c3eaf75af9f5cbb32fac4e | |
parent | 56b0cec481c7457a658b54f979f0fea3c1189c12 (diff) | |
download | oauthlib-db895be0dc532e410c302409a0a8c091f12a2aaa.tar.gz |
OAuth 2 parameter handlers
-rw-r--r-- | oauthlib/oauth2/draft25/parameters.py | 256 | ||||
-rw-r--r-- | tests/oauth2/draft25/test_parameters.py | 160 |
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') |