From 5b27e5b0d064d04ef8996a8a433d15446c2ff204 Mon Sep 17 00:00:00 2001 From: David Morton Date: Tue, 9 Sep 2014 13:26:28 -0500 Subject: [PATCH 01/12] Update response returned when authorization is missing --- ptero_common/auth.py | 39 +++++++++++++++++++++++++++++---------- tests/test_auth.py | 20 ++++++++++++++++++-- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index 52de074..51f1f21 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -1,19 +1,38 @@ from flask import request, Response +import re +class ProtectedEndpoint(object): + def __init__(self, scopes=[], claims=[], audiences=[]): + self.scopes = scopes + self.claims = claims + self.audiences = audiences -def protected_endpoint(target): - def handle_request(*args, **kwargs): - response, id_token = _parse_request(request) - if (response): - return response - else: - return target(*args, id_token=id_token, **kwargs) - return handle_request + def __call__(self, target): + def handle_request(*args, **kwargs): + response, id_token = _parse_request(request, self.scopes, + self.claims, self.audiences) + if (response): + return response + else: + return target(*args, id_token=id_token, **kwargs) + return handle_request -def _parse_request(request): +protected_endpoint = ProtectedEndpoint + + +def _parse_request(request, scopes, claims, audiences): if ('Authorization' not in request.headers or 'Identity' not in request.headers): return Response(status=401, - headers={'WWW-Authenticate': 'Bearer realm="PTero"'}), None + headers={'WWW-Authenticate': authenticate_value_text(scopes), + 'Identify': identify_value_text(claims, audiences)}), None else: return None, None + + +def authenticate_value_text(scopes): + return 'Bearer realm="PTero", scope="%s"' % ' '.join(scopes) + +def identify_value_text(claims, audiences): + return 'ID Token claims="%s", aud="%s"' % (', '.join(claims), + ', '.join(audiences)) diff --git a/tests/test_auth.py b/tests/test_auth.py index 9981534..b214f99 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -5,7 +5,7 @@ app = flask.Flask('test_app') -@protected_endpoint +@protected_endpoint(scopes=['a','b'], claims=['c','d'], audiences=['e','f']) def target(*args, **kwargs): return 'test_return_value' @@ -17,13 +17,29 @@ def test_request_with_no_authorization_returns_401(self): return_value = target() self.assertEqual(return_value.status, '401 UNAUTHORIZED') + def test_request_with_no_authorization_returns_authenticate_headers(self): + with app.test_request_context(headers=None): + return_value = target() + self.assertTrue('WWW-Authenticate' in return_value.headers) + + self.assertEqual(return_value.headers['WWW-Authenticate'], + 'Bearer realm="PTero", scope="a b"') + + def test_request_with_no_authorization_returns_identify_headers(self): + with app.test_request_context(headers=None): + return_value = target() + self.assertTrue('Identify' in return_value.headers) + + self.assertEqual(return_value.headers['Identify'], + 'ID Token claims="c, d", aud="e, f"') + def test_request_with_authorization_returns_targets_result(self): with app.test_request_context(headers=VALID_HEADER): return_value = target() self.assertEqual(return_value, 'test_return_value') def test_adds_id_token_as_kwarg(self): - @protected_endpoint + @protected_endpoint() def echo_target(*args, **kwargs): return (args, kwargs) From 2aed84cf960cc0628d0ff12c58f26e23b243e70f Mon Sep 17 00:00:00 2001 From: David Morton Date: Tue, 9 Sep 2014 14:05:33 -0500 Subject: [PATCH 02/12] Shorten test names --- tests/test_auth.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index b214f99..3171b36 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -12,12 +12,12 @@ def target(*args, **kwargs): VALID_HEADER = {'Authorization':'foo', 'Identity':'bar'} class TestRequestParser(TestCase): - def test_request_with_no_authorization_returns_401(self): + def test_no_authorization_returns_401(self): with app.test_request_context(headers=None): return_value = target() self.assertEqual(return_value.status, '401 UNAUTHORIZED') - def test_request_with_no_authorization_returns_authenticate_headers(self): + def test_no_authorization_returns_authenticate_headers(self): with app.test_request_context(headers=None): return_value = target() self.assertTrue('WWW-Authenticate' in return_value.headers) @@ -25,7 +25,7 @@ def test_request_with_no_authorization_returns_authenticate_headers(self): self.assertEqual(return_value.headers['WWW-Authenticate'], 'Bearer realm="PTero", scope="a b"') - def test_request_with_no_authorization_returns_identify_headers(self): + def test_no_authorization_returns_identify_headers(self): with app.test_request_context(headers=None): return_value = target() self.assertTrue('Identify' in return_value.headers) @@ -33,7 +33,7 @@ def test_request_with_no_authorization_returns_identify_headers(self): self.assertEqual(return_value.headers['Identify'], 'ID Token claims="c, d", aud="e, f"') - def test_request_with_authorization_returns_targets_result(self): + def test_returns_targets_result(self): with app.test_request_context(headers=VALID_HEADER): return_value = target() self.assertEqual(return_value, 'test_return_value') From bb2536b3ca3f96fc83827fd4a882e7e1fba75fcb Mon Sep 17 00:00:00 2001 From: David Morton Date: Tue, 9 Sep 2014 14:10:02 -0500 Subject: [PATCH 03/12] return 400 response if cannot parse Authorization header --- ptero_common/auth.py | 20 ++++++++++++++++++-- tests/test_auth.py | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index 51f1f21..9dd306c 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -26,8 +26,17 @@ def _parse_request(request, scopes, claims, audiences): return Response(status=401, headers={'WWW-Authenticate': authenticate_value_text(scopes), 'Identify': identify_value_text(claims, audiences)}), None - else: - return None, None + + try: + access_token = parse_authorization_text(request.headers['Authorization']) + except ValueError as e: + return Response(status=400, + headers={'WWW-Authenticate': + '%s, error="invalid_request", error_description="The Bearer token is malformed"' % + (authenticate_value_text(scopes)), + 'Identify': identify_value_text(claims, audiences)}), None + + return None, None def authenticate_value_text(scopes): @@ -36,3 +45,10 @@ def authenticate_value_text(scopes): def identify_value_text(claims, audiences): return 'ID Token claims="%s", aud="%s"' % (', '.join(claims), ', '.join(audiences)) + +def parse_authorization_text(text): + match_object = re.search('Bearer (.*)', text) + if match_object is None: + raise ValueError('Cannot parse authorization_text (%s)' % text) + else: + return match_object.groups()[0] diff --git a/tests/test_auth.py b/tests/test_auth.py index 3171b36..3fd2000 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -9,7 +9,8 @@ def target(*args, **kwargs): return 'test_return_value' -VALID_HEADER = {'Authorization':'foo', 'Identity':'bar'} +VALID_HEADER = {'Authorization':'Bearer 1234', 'Identity':'bar'} +BAD_AUTHORIZATION_HEADER = {'Authorization':'foo', 'Identity':'bar'} class TestRequestParser(TestCase): def test_no_authorization_returns_401(self): @@ -33,6 +34,7 @@ def test_no_authorization_returns_identify_headers(self): self.assertEqual(return_value.headers['Identify'], 'ID Token claims="c, d", aud="e, f"') + def test_returns_targets_result(self): with app.test_request_context(headers=VALID_HEADER): return_value = target() @@ -46,3 +48,17 @@ def echo_target(*args, **kwargs): with app.test_request_context(headers=VALID_HEADER): return_value = echo_target(1,2,3, kw='foo') self.assertEqual(return_value, ((1,2,3), {'kw':'foo','id_token':None})) + + + def test_invalid_authorization_header_returns_400(self): + with app.test_request_context(headers=BAD_AUTHORIZATION_HEADER): + return_value = target() + self.assertEqual(return_value.status, '400 BAD REQUEST') + + def test_invalid_authorization_header_returns_authenticate_headers(self): + with app.test_request_context(headers=BAD_AUTHORIZATION_HEADER): + return_value = target() + self.assertTrue('WWW-Authenticate' in return_value.headers) + + self.assertEqual(return_value.headers['WWW-Authenticate'], + 'Bearer realm="PTero", scope="a b", error="invalid_request", error_description="The Bearer token is malformed"') From a2b5d34106a3db1c305f40d31458aa744f610105 Mon Sep 17 00:00:00 2001 From: David Morton Date: Tue, 9 Sep 2014 16:16:59 -0500 Subject: [PATCH 04/12] Return 400 response if ID Token cannot be deserialized --- ptero_common/auth.py | 9 +++++++++ tests/test_auth.py | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index 9dd306c..6ea16c2 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -1,4 +1,5 @@ from flask import request, Response +import jot import re class ProtectedEndpoint(object): @@ -35,6 +36,14 @@ def _parse_request(request, scopes, claims, audiences): '%s, error="invalid_request", error_description="The Bearer token is malformed"' % (authenticate_value_text(scopes)), 'Identify': identify_value_text(claims, audiences)}), None + try: + id_token = jot.deserialize(request.headers['Identity']) + except: + return Response(status=400, + headers={'WWW-Authenticate': authenticate_value_text(scopes), + 'Identify': '%s, error="invalid_request", error_description="The ID token is malformed"' + % identify_value_text(claims, audiences)}), None + return None, None diff --git a/tests/test_auth.py b/tests/test_auth.py index 3fd2000..fb2f133 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -9,8 +9,31 @@ def target(*args, **kwargs): return 'test_return_value' -VALID_HEADER = {'Authorization':'Bearer 1234', 'Identity':'bar'} -BAD_AUTHORIZATION_HEADER = {'Authorization':'foo', 'Identity':'bar'} +VALID_ID_TOKEN = ('eyJhbGciOiJSU0ExXzUiLCJjdHkiOiJKV1QiLCJlbmMiOiJBMTI4Q0JDLU' + 'hTMjU2Iiwia2lkIjoiU09NRSBGQU5DWSBLSUQifQ.CSQDZ8fcAgUnioPazyirXFwzZd' + 'sEiVLFpHVNJHkilzEaiI_PnPM8vkh9TqULxJcyzZDrAUBDhtc4Q_FHNK0FN4ngJccY4' + 'Ns2qOA_DSsBnYVuB1aG-EUQ85cqoWQvxS1UjZ7zXV0N3D_KS2UX92argCjg055GUiMm' + 'IIzvntND4k9bMc4pL9jEUGKsTNrkHtZLjd3r--DMfHhnRhmGNwRbaFrEGuJshNJZAyp' + '3DlwVcD_-CHmdrST3_1M49DnSnI3ytthSuAHGKeths5piHpDBVJsQ58WiZgcKJeNPlZ' + 'XHQRqgwZj4gVyzVd4lHG2yHraa21P41gVM81lSCJisKGPtTw.GXcxNkIbM21Rd2OQj2' + 'ib5w.En89jY7MnIcUgMZt2l9rlbgARVSxgaRPwNO3UPoYQO5-W0rmfk4GLIpbaXkfMF' + 'wK1JdGlefyKW0me7T3Wsd2FSWI0zYV8B5_EduL6GLsUBQpJH2eD-mSSyGjR9D3GV9Fo' + 'AWOAMXoBtk4iLPTQnOLMGL5J3oRKRcUKvFO2zBGciC4b-EDn-zUJJEqHwl4BJ7Rr-mI' + 'clhDAMYNSO4LN0cgS8dSTyfQhMorsuahptOSoTP2mJaA2v2bG0VKeIyEOx8RgwK1z6D' + 'yoTsMlgFrpteFbzRz2H2w7gKrItnqSAFQa8JUd-BvpVEXdPYfH41MHPGeUOHd9L7RO6' + 'HSTrsoQb_wf6A3NYtIuMfTIR6nwsKBMB8i6QtkhoTTAFq2xneAg4JbmljiM9As9CbxW' + 'a7SAH3Xs9sVi2F0-1lZYFaKRTxswadhn64X0KNf6jUIMFYamkGkNcHApsERz1PROmCF' + 'k9WusVrZBW5eirunPIAEdEYC8QxpqmicHeRrtFa0Vjr7FojJ6o8bUsblKaGXDXhpQGa' + 'F40H0TNqBEllIXTln9J6jz8u0DjocPCMy8v0_QGlshGJypjo_NehwvKRrwaSTv3NoEl' + 'VGb10L7ZFF9RincIMJG7nFSfuTzMNfYVn0CvOwwlkIkoYm2oaIoilxlOTSXf63ypd45' + 'Mp5DF99vFqL_-Zp1T-QIDsVSvm3MIOo1hc5XgxvdGsbAl3ZkCF9GUU6yXDnXX56Qp9a' + 'Nw-TuhQyaW1J-Nw4MeHeg7JVFf5oqwrLS60iJ7kWYSY5qV58keAfbX4gYS44y7GI_H6' + 'ctq62BiFSivDRxaqp2tKNhFQzBSiXNLVwCaRsVhTV_tC0FpOkcQlh0wHOhv_brPK42M' + 'q_GlAOYo--q5glR7_7cmhErVfQt6HMiZZ3omwJ9jLGrFsfvh_TCA.hzX35A0ZuHOqyT' + '4xI9GZiA') +VALID_HEADER = {'Authorization':'Bearer 1234', 'Identity':VALID_ID_TOKEN} +BAD_AUTHORIZATION_HEADER = {'Authorization':'foo', 'Identity':VALID_ID_TOKEN} +BAD_IDENTITY_HEADER = {'Authorization':'Bearer 1234', 'Identity':'foo'} class TestRequestParser(TestCase): def test_no_authorization_returns_401(self): @@ -62,3 +85,16 @@ def test_invalid_authorization_header_returns_authenticate_headers(self): self.assertEqual(return_value.headers['WWW-Authenticate'], 'Bearer realm="PTero", scope="a b", error="invalid_request", error_description="The Bearer token is malformed"') + + def test_invalid_identity_header_returns_400(self): + with app.test_request_context(headers=BAD_IDENTITY_HEADER): + return_value = target() + self.assertEqual(return_value.status, '400 BAD REQUEST') + + def test_invalid_identity_header_returns_identify_headers(self): + with app.test_request_context(headers=BAD_IDENTITY_HEADER): + return_value = target() + self.assertTrue('WWW-Authenticate' in return_value.headers) + + self.assertEqual(return_value.headers['Identify'], + 'ID Token claims="c, d", aud="e, f", error="invalid_request", error_description="The ID token is malformed"') From 31e0e0bd8d56c2b61fcb0e769913b421f5bc8c41 Mon Sep 17 00:00:00 2001 From: David Morton Date: Wed, 10 Sep 2014 12:41:39 -0500 Subject: [PATCH 05/12] Define custom exception class --- ptero_common/auth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index 6ea16c2..9c33103 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -2,6 +2,8 @@ import jot import re +class MalformedAccessTokenError(Exception): pass + class ProtectedEndpoint(object): def __init__(self, scopes=[], claims=[], audiences=[]): self.scopes = scopes @@ -30,7 +32,7 @@ def _parse_request(request, scopes, claims, audiences): try: access_token = parse_authorization_text(request.headers['Authorization']) - except ValueError as e: + except MalformedAccessTokenError as e: return Response(status=400, headers={'WWW-Authenticate': '%s, error="invalid_request", error_description="The Bearer token is malformed"' % @@ -58,6 +60,6 @@ def identify_value_text(claims, audiences): def parse_authorization_text(text): match_object = re.search('Bearer (.*)', text) if match_object is None: - raise ValueError('Cannot parse authorization_text (%s)' % text) + raise MalformedAccessTokenError else: return match_object.groups()[0] From f26e26df50013659a78610f3a559eee4c51d4f04 Mon Sep 17 00:00:00 2001 From: David Morton Date: Wed, 10 Sep 2014 12:53:00 -0500 Subject: [PATCH 06/12] Refactor to avoid defining function in method --- ptero_common/auth.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index 9c33103..b9e5076 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -11,14 +11,16 @@ def __init__(self, scopes=[], claims=[], audiences=[]): self.audiences = audiences def __call__(self, target): - def handle_request(*args, **kwargs): - response, id_token = _parse_request(request, self.scopes, - self.claims, self.audiences) - if (response): - return response - else: - return target(*args, id_token=id_token, **kwargs) - return handle_request + self.target = target + return self._execute_target + + def _execute_target(self, *args, **kwargs): + response, id_token = _parse_request(request, self.scopes, + self.claims, self.audiences) + if (response): + return response + else: + return self.target(*args, id_token=id_token, **kwargs) protected_endpoint = ProtectedEndpoint From 6abe1b7ffaf52352306e2d4424b0578cf28172ae Mon Sep 17 00:00:00 2001 From: David Morton Date: Wed, 10 Sep 2014 13:29:29 -0500 Subject: [PATCH 07/12] Define and use MissingAuthHeadersError --- ptero_common/auth.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index b9e5076..c5c53c2 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -2,6 +2,7 @@ import jot import re +class MissingAuthHeadersError(Exception): pass class MalformedAccessTokenError(Exception): pass class ProtectedEndpoint(object): @@ -26,8 +27,9 @@ def _execute_target(self, *args, **kwargs): def _parse_request(request, scopes, claims, audiences): - if ('Authorization' not in request.headers or - 'Identity' not in request.headers): + try: + ensure_headers_are_present(request) + except MissingAuthHeadersError: return Response(status=401, headers={'WWW-Authenticate': authenticate_value_text(scopes), 'Identify': identify_value_text(claims, audiences)}), None @@ -51,6 +53,10 @@ def _parse_request(request, scopes, claims, audiences): return None, None +def ensure_headers_are_present(request): + if ('Authorization' not in request.headers or + 'Identity' not in request.headers): + raise MissingAuthHeadersError def authenticate_value_text(scopes): return 'Bearer realm="PTero", scope="%s"' % ' '.join(scopes) From 26a250927c112b1bc4cb820214bbe5a2d75cabde Mon Sep 17 00:00:00 2001 From: David Morton Date: Wed, 10 Sep 2014 13:40:27 -0500 Subject: [PATCH 08/12] Use exceptions instead of returning tuples --- ptero_common/auth.py | 46 +++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index c5c53c2..975d7f2 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -1,6 +1,7 @@ from flask import request, Response import jot import re +from jot.exceptions import InvalidSerialization class MissingAuthHeadersError(Exception): pass class MalformedAccessTokenError(Exception): pass @@ -10,48 +11,45 @@ def __init__(self, scopes=[], claims=[], audiences=[]): self.scopes = scopes self.claims = claims self.audiences = audiences + self.exception_map = construct_exception_map(scopes, claims, audiences) def __call__(self, target): self.target = target return self._execute_target def _execute_target(self, *args, **kwargs): - response, id_token = _parse_request(request, self.scopes, - self.claims, self.audiences) - if (response): - return response - else: - return self.target(*args, id_token=id_token, **kwargs) + try: + id_token = self._extract_id_token() + except Exception as e: + return self.exception_map[e.__class__] + return self.target(*args, id_token=id_token, **kwargs) -protected_endpoint = ProtectedEndpoint + def _extract_id_token(self): + ensure_headers_are_present(request) + access_token = parse_authorization_text(request.headers['Authorization']) + jwe_or_jws = jot.deserialize(request.headers['Identity']) + return None +protected_endpoint = ProtectedEndpoint -def _parse_request(request, scopes, claims, audiences): - try: - ensure_headers_are_present(request) - except MissingAuthHeadersError: - return Response(status=401, +def construct_exception_map(scopes, claims, audiences): + result = {} + result[MissingAuthHeadersError] = Response(status=401, headers={'WWW-Authenticate': authenticate_value_text(scopes), - 'Identify': identify_value_text(claims, audiences)}), None + 'Identify': identify_value_text(claims, audiences)}) - try: - access_token = parse_authorization_text(request.headers['Authorization']) - except MalformedAccessTokenError as e: - return Response(status=400, + result[MalformedAccessTokenError] = Response(status=400, headers={'WWW-Authenticate': '%s, error="invalid_request", error_description="The Bearer token is malformed"' % (authenticate_value_text(scopes)), - 'Identify': identify_value_text(claims, audiences)}), None - try: - id_token = jot.deserialize(request.headers['Identity']) - except: - return Response(status=400, + 'Identify': identify_value_text(claims, audiences)}) + result[InvalidSerialization] = Response(status=400, headers={'WWW-Authenticate': authenticate_value_text(scopes), 'Identify': '%s, error="invalid_request", error_description="The ID token is malformed"' - % identify_value_text(claims, audiences)}), None + % identify_value_text(claims, audiences)}) + return result - return None, None def ensure_headers_are_present(request): if ('Authorization' not in request.headers or From 8fe2f6f21dc430a444cb0d2d478b6c14a5df4573 Mon Sep 17 00:00:00 2001 From: David Morton Date: Wed, 10 Sep 2014 13:44:59 -0500 Subject: [PATCH 09/12] Add realm kwarg to decorator --- ptero_common/auth.py | 18 ++++++++++-------- tests/test_auth.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index 975d7f2..a664b4e 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -7,11 +7,13 @@ class MissingAuthHeadersError(Exception): pass class MalformedAccessTokenError(Exception): pass class ProtectedEndpoint(object): - def __init__(self, scopes=[], claims=[], audiences=[]): + def __init__(self, realm=None, scopes=[], claims=[], audiences=[]): + self.realm = realm self.scopes = scopes self.claims = claims self.audiences = audiences - self.exception_map = construct_exception_map(scopes, claims, audiences) + self.exception_map = construct_exception_map(realm, scopes, + claims, audiences) def __call__(self, target): self.target = target @@ -32,19 +34,19 @@ def _extract_id_token(self): protected_endpoint = ProtectedEndpoint -def construct_exception_map(scopes, claims, audiences): +def construct_exception_map(realm, scopes, claims, audiences): result = {} result[MissingAuthHeadersError] = Response(status=401, - headers={'WWW-Authenticate': authenticate_value_text(scopes), + headers={'WWW-Authenticate': authenticate_value_text(realm, scopes), 'Identify': identify_value_text(claims, audiences)}) result[MalformedAccessTokenError] = Response(status=400, headers={'WWW-Authenticate': '%s, error="invalid_request", error_description="The Bearer token is malformed"' % - (authenticate_value_text(scopes)), + (authenticate_value_text(realm, scopes)), 'Identify': identify_value_text(claims, audiences)}) result[InvalidSerialization] = Response(status=400, - headers={'WWW-Authenticate': authenticate_value_text(scopes), + headers={'WWW-Authenticate': authenticate_value_text(realm, scopes), 'Identify': '%s, error="invalid_request", error_description="The ID token is malformed"' % identify_value_text(claims, audiences)}) return result @@ -56,8 +58,8 @@ def ensure_headers_are_present(request): 'Identity' not in request.headers): raise MissingAuthHeadersError -def authenticate_value_text(scopes): - return 'Bearer realm="PTero", scope="%s"' % ' '.join(scopes) +def authenticate_value_text(realm, scopes): + return 'Bearer realm="%s", scope="%s"' % (realm, ' '.join(scopes)) def identify_value_text(claims, audiences): return 'ID Token claims="%s", aud="%s"' % (', '.join(claims), diff --git a/tests/test_auth.py b/tests/test_auth.py index fb2f133..fce1156 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -5,7 +5,7 @@ app = flask.Flask('test_app') -@protected_endpoint(scopes=['a','b'], claims=['c','d'], audiences=['e','f']) +@protected_endpoint(realm='PTero', scopes=['a','b'], claims=['c','d'], audiences=['e','f']) def target(*args, **kwargs): return 'test_return_value' From 78d4c16a4db4cb0509c6073870363e33fff50a44 Mon Sep 17 00:00:00 2001 From: David Morton Date: Wed, 10 Sep 2014 22:48:02 -0500 Subject: [PATCH 10/12] Log unexpected exceptions and return 400 --- ptero_common/auth.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index a664b4e..ee5b4ed 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -1,7 +1,11 @@ from flask import request, Response +from collections import defaultdict import jot import re from jot.exceptions import InvalidSerialization +import logging + +LOG = logging.getLogger(__name__) class MissingAuthHeadersError(Exception): pass class MalformedAccessTokenError(Exception): pass @@ -23,7 +27,11 @@ def _execute_target(self, *args, **kwargs): try: id_token = self._extract_id_token() except Exception as e: - return self.exception_map[e.__class__] + if (e.__class__ in self.exception_map): + return self.exception_map[e.__class__] + else: + LOG.exception("Unexpected exception occured while processing request url=(%s) headers=(%s)", + request.url, request.headers) return self.target(*args, id_token=id_token, **kwargs) def _extract_id_token(self): @@ -35,7 +43,10 @@ def _extract_id_token(self): protected_endpoint = ProtectedEndpoint def construct_exception_map(realm, scopes, claims, audiences): - result = {} + result = defaultdict(lambda :Response(status=400, + headers={'WWW-Authenticate': authenticate_value_text(realm, scopes), + 'Identify': identify_value_text(claims, audiences)})) + result[MissingAuthHeadersError] = Response(status=401, headers={'WWW-Authenticate': authenticate_value_text(realm, scopes), 'Identify': identify_value_text(claims, audiences)}) From 87f13113c8136978ca6434b0db0c275a70a934f8 Mon Sep 17 00:00:00 2001 From: David Morton Date: Thu, 11 Sep 2014 12:49:21 -0500 Subject: [PATCH 11/12] Revert "Log unexpected exceptions and return 400" This reverts commit 78d4c16a4db4cb0509c6073870363e33fff50a44. --- ptero_common/auth.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index ee5b4ed..a664b4e 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -1,11 +1,7 @@ from flask import request, Response -from collections import defaultdict import jot import re from jot.exceptions import InvalidSerialization -import logging - -LOG = logging.getLogger(__name__) class MissingAuthHeadersError(Exception): pass class MalformedAccessTokenError(Exception): pass @@ -27,11 +23,7 @@ def _execute_target(self, *args, **kwargs): try: id_token = self._extract_id_token() except Exception as e: - if (e.__class__ in self.exception_map): - return self.exception_map[e.__class__] - else: - LOG.exception("Unexpected exception occured while processing request url=(%s) headers=(%s)", - request.url, request.headers) + return self.exception_map[e.__class__] return self.target(*args, id_token=id_token, **kwargs) def _extract_id_token(self): @@ -43,10 +35,7 @@ def _extract_id_token(self): protected_endpoint = ProtectedEndpoint def construct_exception_map(realm, scopes, claims, audiences): - result = defaultdict(lambda :Response(status=400, - headers={'WWW-Authenticate': authenticate_value_text(realm, scopes), - 'Identify': identify_value_text(claims, audiences)})) - + result = {} result[MissingAuthHeadersError] = Response(status=401, headers={'WWW-Authenticate': authenticate_value_text(realm, scopes), 'Identify': identify_value_text(claims, audiences)}) From 4a02b52ebb7ac5036519fde68cacc0104c35f2ff Mon Sep 17 00:00:00 2001 From: David Morton Date: Thu, 11 Sep 2014 12:51:07 -0500 Subject: [PATCH 12/12] Only catch the exceptions we're mapping to responses --- ptero_common/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ptero_common/auth.py b/ptero_common/auth.py index a664b4e..9c06913 100644 --- a/ptero_common/auth.py +++ b/ptero_common/auth.py @@ -14,6 +14,7 @@ def __init__(self, realm=None, scopes=[], claims=[], audiences=[]): self.audiences = audiences self.exception_map = construct_exception_map(realm, scopes, claims, audiences) + self.mapped_exceptions = tuple(self.exception_map.keys()) def __call__(self, target): self.target = target @@ -22,7 +23,7 @@ def __call__(self, target): def _execute_target(self, *args, **kwargs): try: id_token = self._extract_id_token() - except Exception as e: + except self.mapped_exceptions as e: return self.exception_map[e.__class__] return self.target(*args, id_token=id_token, **kwargs)