diff options
author | Ryan Hiebert <ryan@ryanhiebert.com> | 2014-09-03 22:57:14 -0500 |
---|---|---|
committer | Ryan Hiebert <ryan@ryanhiebert.com> | 2014-09-11 11:31:58 -0500 |
commit | 988d08284989226577d30ae07550278dbf50d751 (patch) | |
tree | e70f645b4b9c857d2c0a640d37c4f3689e39912a | |
parent | 80ada0db6ddade8fe31c641c59a8dff929ef019c (diff) | |
download | oauthlib-988d08284989226577d30ae07550278dbf50d751.tar.gz |
Create Signature Only OAuth1 Endpoint
In certain cases a provider may wish to verify the signature of an
oauth request without doing anything more with it. Learning Tools
Interoperability (LTI), for example, uses "0-legged OAuth" for it's
signature verification process.
http://www.imsglobal.org/lti/
http://andyfmiller.com/2013/02/10/does-lti-use-oauth/
This adds a new ``SignatureOnlyEndpoint`` that implements only the
client validation and signature verification, and leaves off the other
parts that would need to be validated.
-rw-r--r-- | docs/oauth1/endpoints/endpoints.rst | 1 | ||||
-rw-r--r-- | docs/oauth1/endpoints/signature_only.rst | 5 | ||||
-rw-r--r-- | oauthlib/oauth1/__init__.py | 2 | ||||
-rw-r--r-- | oauthlib/oauth1/rfc5849/endpoints/__init__.py | 1 | ||||
-rw-r--r-- | oauthlib/oauth1/rfc5849/endpoints/signature_only.py | 71 | ||||
-rw-r--r-- | oauthlib/oauth1/rfc5849/request_validator.py | 5 | ||||
-rw-r--r-- | tests/oauth1/rfc5849/endpoints/test_signature_only.py | 51 |
7 files changed, 135 insertions, 1 deletions
diff --git a/docs/oauth1/endpoints/endpoints.rst b/docs/oauth1/endpoints/endpoints.rst index d869824..33261cc 100644 --- a/docs/oauth1/endpoints/endpoints.rst +++ b/docs/oauth1/endpoints/endpoints.rst @@ -14,3 +14,4 @@ See :doc:`../preconfigured_servers` for available composite endpoints/servers. authorization access_token resource + signature_only diff --git a/docs/oauth1/endpoints/signature_only.rst b/docs/oauth1/endpoints/signature_only.rst new file mode 100644 index 0000000..0d97df4 --- /dev/null +++ b/docs/oauth1/endpoints/signature_only.rst @@ -0,0 +1,5 @@ +Signature Only +-------------- + +.. autoclass:: oauthlib.oauth1.SignatureOnlyEndpoint + :members: diff --git a/oauthlib/oauth1/__init__.py b/oauthlib/oauth1/__init__.py index e58ccf0..b2bc0f9 100644 --- a/oauthlib/oauth1/__init__.py +++ b/oauthlib/oauth1/__init__.py @@ -15,5 +15,5 @@ from .rfc5849 import SIGNATURE_TYPE_BODY from .rfc5849.request_validator import RequestValidator from .rfc5849.endpoints import RequestTokenEndpoint, AuthorizationEndpoint from .rfc5849.endpoints import AccessTokenEndpoint, ResourceEndpoint -from .rfc5849.endpoints import WebApplicationServer +from .rfc5849.endpoints import SignatureOnlyEndpoint, WebApplicationServer from .rfc5849.errors import * diff --git a/oauthlib/oauth1/rfc5849/endpoints/__init__.py b/oauthlib/oauth1/rfc5849/endpoints/__init__.py index c1b57d6..b16ccba 100644 --- a/oauthlib/oauth1/rfc5849/endpoints/__init__.py +++ b/oauthlib/oauth1/rfc5849/endpoints/__init__.py @@ -5,4 +5,5 @@ from .request_token import RequestTokenEndpoint from .authorization import AuthorizationEndpoint from .access_token import AccessTokenEndpoint from .resource import ResourceEndpoint +from .signature_only import SignatureOnlyEndpoint from .pre_configured import WebApplicationServer diff --git a/oauthlib/oauth1/rfc5849/endpoints/signature_only.py b/oauthlib/oauth1/rfc5849/endpoints/signature_only.py new file mode 100644 index 0000000..33dc83d --- /dev/null +++ b/oauthlib/oauth1/rfc5849/endpoints/signature_only.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +oauthlib.oauth1.rfc5849.endpoints.signature_only +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module is an implementation of the signing logic of OAuth 1.0 RFC 5849. +""" + +from __future__ import absolute_import, unicode_literals + +from oauthlib.common import log + +from .base import BaseEndpoint +from .. import errors + + +class SignatureOnlyEndpoint(BaseEndpoint): + """An endpoint only responsible for verifying an oauth signature.""" + + def validate_request(self, uri, http_method='GET', + body=None, headers=None): + """Validate a signed OAuth request. + + :param uri: The full URI of the token request. + :param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc. + :param body: The request body as a string. + :param headers: The request headers as a dict. + :returns: A tuple of 2 elements. + 1. True if valid, False otherwise. + 2. An oauthlib.common.Request object. + """ + try: + request = self._create_request(uri, http_method, body, headers) + except errors.OAuth1Error: + return False, None + + try: + self._check_transport_security(request) + self._check_mandatory_parameters(request) + except errors.OAuth1Error: + return False, request + + if not self.request_validator.validate_timestamp_and_nonce( + request.client_key, request.timestamp, request.nonce, request): + return False, request + + # The server SHOULD return a 401 (Unauthorized) status code when + # receiving a request with invalid client credentials. + # Note: This is postponed in order to avoid timing attacks, instead + # a dummy client is assigned and used to maintain near constant + # time request verification. + # + # Note that early exit would enable client enumeration + valid_client = self.request_validator.validate_client_key( + request.client_key, request) + if not valid_client: + request.client_key = self.request_validator.dummy_client + + valid_signature = self._check_signature(request) + + # We delay checking validity until the very end, using dummy values for + # calculations and fetching secrets/keys to ensure the flow of every + # request remains almost identical regardless of whether valid values + # have been supplied. This ensures near constant time execution and + # prevents malicious users from guessing sensitive information + v = all((valid_client, valid_signature)) + if not v: + log.info("[Failure] request verification failed.") + log.info("Valid client: %s", valid_client) + log.info("Valid signature: %s", valid_signature) + return v, request diff --git a/oauthlib/oauth1/rfc5849/request_validator.py b/oauthlib/oauth1/rfc5849/request_validator.py index c425ff6..ef4cc92 100644 --- a/oauthlib/oauth1/rfc5849/request_validator.py +++ b/oauthlib/oauth1/rfc5849/request_validator.py @@ -216,6 +216,7 @@ class RequestValidator(object): * AccessTokenEndpoint * RequestTokenEndpoint * ResourceEndpoint + * SignatureOnlyEndpoint """ raise NotImplementedError("Subclasses must implement this function.") @@ -282,6 +283,7 @@ class RequestValidator(object): * AccessTokenEndpoint * RequestTokenEndpoint * ResourceEndpoint + * SignatureOnlyEndpoint """ raise NotImplementedError("Subclasses must implement this function.") @@ -415,6 +417,7 @@ class RequestValidator(object): * AccessTokenEndpoint * RequestTokenEndpoint * ResourceEndpoint + * SignatureOnlyEndpoint """ raise NotImplementedError("Subclasses must implement this function.") @@ -476,6 +479,7 @@ class RequestValidator(object): * AccessTokenEndpoint * RequestTokenEndpoint * ResourceEndpoint + * SignatureOnlyEndpoint """ raise NotImplementedError("Subclasses must implement this function.") @@ -593,6 +597,7 @@ class RequestValidator(object): * AccessTokenEndpoint * RequestTokenEndpoint * ResourceEndpoint + * SignatureOnlyEndpoint """ raise NotImplementedError("Subclasses must implement this function.") diff --git a/tests/oauth1/rfc5849/endpoints/test_signature_only.py b/tests/oauth1/rfc5849/endpoints/test_signature_only.py new file mode 100644 index 0000000..2fb18e7 --- /dev/null +++ b/tests/oauth1/rfc5849/endpoints/test_signature_only.py @@ -0,0 +1,51 @@ +from __future__ import unicode_literals, absolute_import + +from mock import MagicMock, ANY +from ....unittest import TestCase + +from oauthlib.oauth1.rfc5849 import Client +from oauthlib.oauth1 import RequestValidator +from oauthlib.oauth1.rfc5849.endpoints import SignatureOnlyEndpoint + + +class SignatureOnlyEndpointTest(TestCase): + + def setUp(self): + self.validator = MagicMock(wraps=RequestValidator()) + self.validator.check_client_key.return_value = True + self.validator.allowed_signature_methods = ['HMAC-SHA1'] + self.validator.get_client_secret.return_value = 'bar' + self.validator.timestamp_lifetime = 600 + self.validator.validate_client_key.return_value = True + self.validator.validate_timestamp_and_nonce.return_value = True + self.validator.dummy_client = 'dummy' + self.validator.dummy_secret = 'dummy' + self.endpoint = SignatureOnlyEndpoint(self.validator) + self.client = Client('foo', client_secret='bar') + self.uri, self.headers, self.body = self.client.sign( + 'https://i.b/protected_resource') + + def test_missing_parameters(self): + v, r = self.endpoint.validate_request( + self.uri) + self.assertFalse(v) + + def test_validate_client_key(self): + self.validator.validate_client_key.return_value = False + v, r = self.endpoint.validate_request( + self.uri, headers=self.headers) + self.assertFalse(v) + + def test_validate_signature(self): + client = Client('foo') + _, headers, _ = client.sign(self.uri + '/extra') + v, r = self.endpoint.validate_request( + self.uri, headers=headers) + self.assertFalse(v) + + def test_valid_request(self): + v, r = self.endpoint.validate_request( + self.uri, headers=self.headers) + self.assertTrue(v) + self.validator.validate_timestamp_and_nonce.assert_called_once_with( + self.client.client_key, ANY, ANY, ANY) |