diff options
-rw-r--r-- | oauthlib/oauth2/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/service_application.py | 179 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_service_application.py | 111 |
4 files changed, 292 insertions, 0 deletions
diff --git a/oauthlib/oauth2/__init__.py b/oauthlib/oauth2/__init__.py index 79be716..0238f1d 100644 --- a/oauthlib/oauth2/__init__.py +++ b/oauthlib/oauth2/__init__.py @@ -13,6 +13,7 @@ from .rfc6749.clients import WebApplicationClient from .rfc6749.clients import MobileApplicationClient from .rfc6749.clients import LegacyApplicationClient from .rfc6749.clients import BackendApplicationClient +from .rfc6749.clients import ServiceApplicationClient from .rfc6749.endpoints import AuthorizationEndpoint from .rfc6749.endpoints import TokenEndpoint from .rfc6749.endpoints import ResourceEndpoint diff --git a/oauthlib/oauth2/rfc6749/clients/__init__.py b/oauthlib/oauth2/rfc6749/clients/__init__.py index bedba25..65bea50 100644 --- a/oauthlib/oauth2/rfc6749/clients/__init__.py +++ b/oauthlib/oauth2/rfc6749/clients/__init__.py @@ -13,3 +13,4 @@ from .web_application import WebApplicationClient from .mobile_application import MobileApplicationClient from .legacy_application import LegacyApplicationClient from .backend_application import BackendApplicationClient +from .service_application import ServiceApplicationClient diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py new file mode 100644 index 0000000..f60ac4f --- /dev/null +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth2.rfc6749 +~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of various logic needed +for consuming and providing OAuth 2.0 RFC6749. +""" +from __future__ import absolute_import, unicode_literals + +import time + +from oauthlib.common import to_unicode + +from .base import Client +from ..parameters import prepare_token_request +from ..parameters import parse_token_response + + +class ServiceApplicationClient(Client): + """A public client utilizing the JWT bearer grant. + + JWT bearer tokes can be used to request an access token when a client + wishes to utilize an existing trust relationship, expressed through the + semantics of (and digital signature or keyed message digest calculated + over) the JWT, without a direct user approval step at the authorization + server. + + This grant type does not involve an authorization step. It may be + used by both public and confidential clients. + """ + + grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer' + + def __init__(self, client_id, private_key=None, subject=None, issuer=None, + audience=None, **kwargs): + """Initalize a JWT client with defaults for implicit use later. + + :param client_id: Client identifier given by the OAuth provider upon + registration. + + :param private_key: Private key used for signing and encrypting. + Must be given as a string. + + :param subject: The principal that is the subject of the JWT, i.e. + which user is the token requested on behalf of. + For example, ``foo@example.com. + + :param issuer: The JWT MUST contain an "iss" (issuer) claim that + contains a unique identifier for the entity that issued + the JWT. For example, ``your-client@provider.com``. + + :param audience: A value identifying the authorization server as an + intended audience, e.g. + ``https://provider.com/oauth2/token``. + + :param kwargs: Additional arguments to pass to base client, such as + state and token. See Client.__init__.__doc__ for + details. + """ + super(ServiceApplicationClient, self).__init__(client_id, **kwargs) + self.private_key = private_key + self.subject = subject + self.issuer = issuer + self.audience = audience + + def prepare_request_body(self, + private_key=None, + subject=None, + issuer=None, + audience=None, + expires_at=None, + issued_at=None, + extra_claims=None, + body='', + scope=None, + **kwargs): + """Create and add a JWT assertion to the request body. + + :param private_key: Private key used for signing and encrypting. + Must be given as a string. + + :param subject: (sub) The principal that is the subject of the JWT, + i.e. which user is the token requested on behalf of. + For example, ``foo@example.com. + + :param issuer: (iss) The JWT MUST contain an "iss" (issuer) claim that + contains a unique identifier for the entity that issued + the JWT. For example, ``your-client@provider.com``. + + :param audience: (aud) A value identifying the authorization server as an + intended audience, e.g. + ``https://provider.com/oauth2/token``. + + :param expires_at: A unix expiration timestamp for the JWT. Defaults + to an hour from now, i.e. ``time.time() + 3600``. + + :param issued_at: A unix timestamp of when the JWT was created. + Defaults to now, i.e. ``time.time()``. + + :param not_before: A unix timestamp after which the JWT may be used. + Not included unless provided. + + :param jwt_id: A unique JWT token identifier. Not included unless + provided. + + :param extra_claims: A dict of additional claims to include in the JWT. + + :param scope: The scope of the access request. + + :param body: Request body (string) with extra parameters. + + :param kwargs: Extra credentials to include in the token request. + + The "scope" parameter may be used, as defined in the Assertion + Framework for OAuth 2.0 Client Authentication and Authorization Grants + [I-D.ietf-oauth-assertions] specification, to indicate the requested + scope. + + Authentication of the client is optional, as described in + `Section 3.2.1`_ of OAuth 2.0 [RFC6749] and consequently, the + "client_id" is only needed when a form of client authentication that + relies on the parameter is used. + + The following non-normative example demonstrates an Access Token + Request with a JWT as an authorization grant (with extra line breaks + for display purposes only): + + .. code-block: http + + POST /token.oauth2 HTTP/1.1 + Host: as.example.com + Content-Type: application/x-www-form-urlencoded + + grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer + &assertion=eyJhbGciOiJFUzI1NiJ9. + eyJpc3Mi[...omitted for brevity...]. + J9l-ZhwP[...omitted for brevity...] + + .. _`Section 3.2.1`: http://tools.ietf.org/html/rfc6749#section-3.2.1 + """ + import jwt + import Crypto.PublicKey.RSA as RSA + + key = private_key or self.private_key + if not key: + raise ValueError('An encryption key must be supplied to make JWT' + ' token requests.') + key = RSA.importKey(key) + + claim = { + 'iss': issuer or self.issuer, + 'aud': audience or self.issuer, + 'sub': subject or self.issuer, + 'exp': int(expires_at or time.time() + 3600), + 'iat': int(issued_at or time.time()), + } + + for attr in ('iss', 'aud', 'sub'): + if claim[attr] is None: + raise ValueError( + 'Claim must include %s but none was given.' % attr) + + if 'not_before' in kwargs: + claim['nbf'] = kwargs.pop('not_before') + + if 'jwt_id' in kwargs: + claim['jti'] = kwargs.pop('jwt_id') + + claim.update(extra_claims or {}) + + assertion = jwt.encode(claim, key, 'RS256') + assertion = to_unicode(assertion) + + return prepare_token_request(self.grant_type, + body=body, + assertion=assertion, + scope=scope, + **kwargs) diff --git a/tests/oauth2/rfc6749/clients/test_service_application.py b/tests/oauth2/rfc6749/clients/test_service_application.py new file mode 100644 index 0000000..2db94d3 --- /dev/null +++ b/tests/oauth2/rfc6749/clients/test_service_application.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals + +from time import time + +import jwt +from Crypto.PublicKey import RSA +from mock import patch + +from oauthlib.common import Request +from oauthlib.oauth2 import ServiceApplicationClient + +from ....unittest import TestCase + + +class ServiceApplicationClientTest(TestCase): + + gt = ServiceApplicationClient.grant_type + + private_key = ( + "-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQDk1/bxy" + "S8Q8jiheHeYYp/4rEKJopeQRRKKpZI4s5i+UPwVpupG\nAlwXWfzXw" + "SMaKPAoKJNdu7tqKRniqst5uoHXw98gj0x7zamu0Ck1LtQ4c7pFMVa" + "h\n5IYGhBi2E9ycNS329W27nJPWNCbESTu7snVlG8V8mfvGGg3xNjT" + "MO7IdrwIDAQAB\nAoGBAOQ2KuH8S5+OrsL4K+wfjoCi6MfxCUyqVU9" + "GxocdM1m30WyWRFMEz2nKJ8fR\np3vTD4w8yplTOhcoXdQZl0kRoaD" + "zrcYkm2VvJtQRrX7dKFT8dR8D/Tr7dNQLOXfC\nDY6xveQczE7qt7V" + "k7lp4FqmxBsaaEuokt78pOOjywZoInjZhAkEA9wz3zoZNT0/i\nrf6" + "qv2qTIeieUB035N3dyw6f1BGSWYaXSuerDCD/J1qZbAPKKhyHZbVaw" + "Ft3UMhe\n542UftBaxQJBAO0iJy1I8GQjGnS7B3yvyH3CcLYGy296+" + "XO/2xKp/d/ty1OIeovx\nC60pLNwuFNF3z9d2GVQAdoQ89hUkOtjZL" + "eMCQQD0JO6oPHUeUjYT+T7ImAv7UKVT\nSuy30sKjLzqoGw1kR+wv7" + "C5PeDRvscs4wa4CW9s6mjSrMDkDrmCLuJDtmf55AkEA\nkmaMg2PNr" + "jUR51F0zOEFycaaqXbGcFwe1/xx9zLmHzMDXd4bsnwt9kk+fe0hQzV" + "S\nJzatanQit3+feev1PN3QewJAWv4RZeavEUhKv+kLe95Yd0su7lT" + "LVduVgh4v5yLT\nGa6FHdjGPcfajt+nrpB1n8UQBEH9ZxniokR/IPv" + "dMlxqXA==\n-----END RSA PRIVATE KEY-----" + ) + + subject = 'resource-owner@provider.com' + + issuer = 'the-client@provider.com' + + audience = 'https://provider.com/token' + + client_id = "someclientid" + scope = ["/profile"] + kwargs = { + "some": "providers", + "require": "extra arguments" + } + + body = "isnot=empty" + + body_up = "not=empty&grant_type=%s" % gt + body_kwargs = body_up + "&some=providers&require=extra+arguments" + + token_json = ('{ "access_token":"2YotnFZFEjr1zCsicMWpAA",' + ' "token_type":"example",' + ' "expires_in":3600,' + ' "scope":"/profile",' + ' "example_parameter":"example_value"}') + token = { + "access_token": "2YotnFZFEjr1zCsicMWpAA", + "token_type": "example", + "expires_in": 3600, + "scope": ["/profile"], + "example_parameter": "example_value" + } + + @patch('time.time') + def test_request_body(self, t): + t.return_value = time() + self.token['expires_at'] = self.token['expires_in'] + t.return_value + + client = ServiceApplicationClient( + self.client_id, private_key=self.private_key) + + # Basic with min required params + body = client.prepare_request_body(issuer=self.issuer, + subject=self.subject, + audience=self.audience, + body=self.body) + r = Request('https://a.b', body=body) + self.assertEqual(r.isnot, 'empty') + self.assertEqual(r.grant_type, ServiceApplicationClient.grant_type) + + key = RSA.importKey(self.private_key).publickey() + claim = jwt.decode(r.assertion, key) + + self.assertEqual(claim['iss'], self.issuer) + self.assertEqual(claim['aud'], self.audience) + self.assertEqual(claim['sub'], self.subject) + self.assertEqual(claim['iat'], int(t.return_value)) + + @patch('time.time') + def test_parse_token_response(self, t): + t.return_value = time() + self.token['expires_at'] = self.token['expires_in'] + t.return_value + + client = ServiceApplicationClient(self.client_id) + + # Parse code and state + response = client.parse_request_body_response(self.token_json, scope=self.scope) + self.assertEqual(response, self.token) + self.assertEqual(client.access_token, response.get("access_token")) + self.assertEqual(client.refresh_token, response.get("refresh_token")) + self.assertEqual(client.token_type, response.get("token_type")) + + # Mismatching state + self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid") |