From 8d3758fde99380d1258f72d489a22edacde331b7 Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Mon, 15 May 2017 14:30:24 +0530 Subject: [PATCH] Change url scheme passed to oauth signature verifier Change 461736 modifies the url passed to oauth signature verifier to request url. But in some deployments, https endpoints are terminated at haproxy and http request is sent to keystone. So request url will have http as url scheme whereas the endpoint is registered with https and signature at client is done with https url. This results in OAUTH signature validation failure. Update URL sent for OAUTH signature verification with the scheme of the base url. Change-Id: Iaba285985b616a35e3dfe33cdd45667174e7c69d Partial-Bug: #1687593 (cherry picked from commit b7aece57d2845fcfa45a84e6d21a6188ddd192cc) --- keystone/oauth1/controllers.py | 16 +++++++++-- keystone/tests/unit/test_v3_oauth1.py | 38 ++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/keystone/oauth1/controllers.py b/keystone/oauth1/controllers.py index d774cd7770..4e7413bf53 100644 --- a/keystone/oauth1/controllers.py +++ b/keystone/oauth1/controllers.py @@ -18,6 +18,7 @@ from oslo_log import log from oslo_serialization import jsonutils from oslo_utils import timeutils from six.moves import http_client +from six.moves.urllib import parse as urlparse from keystone.common import controller from keystone.common import dependency @@ -214,6 +215,15 @@ class OAuthControllerV3(controller.V3Controller): collection_name = 'not_used' member_name = 'not_used' + def _update_url_scheme(self, request): + """Update request url scheme with base url scheme.""" + url = self.base_url(request.context_dict, request.context_dict['path']) + url_scheme = list(urlparse.urlparse(url))[0] + req_url_list = list(urlparse.urlparse(request.url)) + req_url_list[0] = url_scheme + req_url = urlparse.urlunparse(req_url_list) + return req_url + def create_request_token(self, request): oauth_headers = oauth1.get_oauth_headers(request.headers) consumer_id = oauth_headers.get('oauth_consumer_key') @@ -230,13 +240,14 @@ class OAuthControllerV3(controller.V3Controller): self.resource_api.get_project(requested_project_id) self.oauth_api.get_consumer(consumer_id) + url = self._update_url_scheme(request) req_headers = {'Requested-Project-Id': requested_project_id} req_headers.update(request.headers) request_verifier = oauth1.RequestTokenEndpoint( request_validator=validator.OAuthValidator(), token_generator=oauth1.token_generator) h, b, s = request_verifier.create_request_token_response( - request.url, + url, http_method='POST', body=request.params, headers=req_headers) @@ -296,12 +307,13 @@ class OAuthControllerV3(controller.V3Controller): if now > expires: raise exception.Unauthorized(_('Request token is expired')) + url = self._update_url_scheme(request) access_verifier = oauth1.AccessTokenEndpoint( request_validator=validator.OAuthValidator(), token_generator=oauth1.token_generator) try: h, b, s = access_verifier.create_access_token_response( - request.url, + url, http_method='POST', body=request.params, headers=request.headers) diff --git a/keystone/tests/unit/test_v3_oauth1.py b/keystone/tests/unit/test_v3_oauth1.py index 235d0fe875..ee3bfd8452 100644 --- a/keystone/tests/unit/test_v3_oauth1.py +++ b/keystone/tests/unit/test_v3_oauth1.py @@ -22,6 +22,7 @@ from oslo_serialization import jsonutils from pycadf import cadftaxonomy from six.moves import http_client from six.moves import urllib +from six.moves.urllib import parse as urlparse import keystone.conf from keystone.contrib.oauth1 import routers @@ -646,6 +647,13 @@ class UUIDAuthTokenTests(AuthTokenTests, OAuthFlowTests): class MaliciousOAuth1Tests(OAuth1Tests): + def _switch_baseurl_scheme(self): + """Switch the base url scheme.""" + base_url_list = list(urlparse.urlparse(self.base_url)) + base_url_list[0] = 'https' if base_url_list[0] == 'http' else 'http' + bad_url = urlparse.urlunparse(base_url_list) + return bad_url + def test_bad_consumer_secret(self): consumer = self._create_single_consumer() consumer_id = consumer['id'] @@ -665,6 +673,17 @@ class MaliciousOAuth1Tests(OAuth1Tests): self.post(url, headers=headers, expected_status=http_client.UNAUTHORIZED) + def test_bad_request_url_scheme(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + consumer = {'key': consumer_id, 'secret': consumer_secret} + bad_url_scheme = self._switch_baseurl_scheme() + url, headers = self._create_request_token(consumer, self.project_id, + base_url=bad_url_scheme) + self.post(url, headers=headers, + expected_status=http_client.UNAUTHORIZED) + def test_bad_request_token_key(self): consumer = self._create_single_consumer() consumer_id = consumer['id'] @@ -758,7 +777,18 @@ class MaliciousOAuth1Tests(OAuth1Tests): self.assertIn('Invalid signature', resp_data.get('error', {}).get('message')) - # 2. Invalid signature. + # 2. Invalid base url scheme. + # Update the base url scheme, so it will fail to validate signature. + bad_url_scheme = self._switch_baseurl_scheme() + url, headers = self._create_access_token(consumer, request_token, + base_url=bad_url_scheme) + resp = self.post(url, headers=headers, + expected_status=http_client.UNAUTHORIZED) + resp_data = jsonutils.loads(resp.body) + self.assertIn('Invalid signature', + resp_data.get('error', {}).get('message')) + + # 3. Invalid signature. # Update the secret, so it will fail to validate the signature. consumer.update({'secret': uuid.uuid4().hex}) url, headers = self._create_access_token(consumer, request_token) @@ -768,7 +798,7 @@ class MaliciousOAuth1Tests(OAuth1Tests): self.assertIn('Invalid signature', resp_data.get('error', {}).get('message')) - # 3. Invalid verifier. + # 4. Invalid verifier. # Even though the verifier is well formatted, it is not verifier # that is stored in the backend, this is different with the testcase # above `test_bad_verifier` where it test that `verifier` is not @@ -783,7 +813,7 @@ class MaliciousOAuth1Tests(OAuth1Tests): self.assertIn('Provided verifier', resp_data.get('error', {}).get('message')) - # 4. The provided consumer does not exist. + # 5. The provided consumer does not exist. consumer.update({'key': uuid.uuid4().hex}) url, headers = self._create_access_token(consumer, request_token) resp = self.post(url, headers=headers, @@ -792,7 +822,7 @@ class MaliciousOAuth1Tests(OAuth1Tests): self.assertIn('Provided consumer does not exist', resp_data.get('error', {}).get('message')) - # 5. The consumer key provided does not match stored consumer key. + # 6. The consumer key provided does not match stored consumer key. consumer2 = self._create_single_consumer() consumer.update({'key': consumer2['id']}) url, headers = self._create_access_token(consumer, request_token)