summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--oauthlib/oauth2/__init__.py1
-rw-r--r--oauthlib/oauth2/rfc6749/clients/__init__.py1
-rw-r--r--oauthlib/oauth2/rfc6749/clients/service_application.py179
-rw-r--r--tests/oauth2/rfc6749/clients/test_service_application.py111
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")