summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Hiebert <ryan@ryanhiebert.com>2014-09-03 22:57:14 -0500
committerRyan Hiebert <ryan@ryanhiebert.com>2014-09-11 11:31:58 -0500
commit988d08284989226577d30ae07550278dbf50d751 (patch)
treee70f645b4b9c857d2c0a640d37c4f3689e39912a
parent80ada0db6ddade8fe31c641c59a8dff929ef019c (diff)
downloadoauthlib-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.rst1
-rw-r--r--docs/oauth1/endpoints/signature_only.rst5
-rw-r--r--oauthlib/oauth1/__init__.py2
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/__init__.py1
-rw-r--r--oauthlib/oauth1/rfc5849/endpoints/signature_only.py71
-rw-r--r--oauthlib/oauth1/rfc5849/request_validator.py5
-rw-r--r--tests/oauth1/rfc5849/endpoints/test_signature_only.py51
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)