Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support Bearer Token Authentication #165

Merged
merged 12 commits into from
Nov 2, 2020
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

Added
~~~~~

- Support bearer token authentication

Updated
~~~~~~~

Expand Down
58 changes: 58 additions & 0 deletions test/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,61 @@ def test_signing(self):
set(re.search(r'headers="(.+?)"', post_sig).group(1).split(" ")),
{"(request-target)", "date", "host", "content-length", "digest"},
)

def test_bearer_token(self):
"""Verify that the authorization header is set when a bearer token is provided"""

bearer_token = "Bearer eyJraWQiOiJWcmVsOE9zZ0JXaUpHeEpMeFJ4bE1UaVwvbjgyc1hwWktUaTd2UExUNFQ0TT0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2IiwidG9rZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJ3ZWJcL2dldCB3ZWJcL3Bvc3QiLCJhdXRoX3RpbWUiOjE1OTM3MjM1NDgsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy1lYXN0LTEuYW1hem9uYXdzLmNvbVwvdXMtZWFzdC0xX1d6aEZzTGlPRyIsImV4cCI6MTU5MzcyNzE0OCwiaWF0IjoxNTkzNzIzNTQ4LCJ2ZXJzaW9uIjoyLCJqdGkiOiI4Njk5ZDEwYy05Mjg4LTQ0YmEtODIxNi01OTJjZGU3MDBhY2MiLCJjbGllbnRfaWQiOiJoMTBlM2hwajliNjc4bXMwOG8zbGlibHQ2In0.YA_yiD-x6UuBMShprUbUKuB_DO6ogCtd5srfgpJA6Ve_qsf8n19nVMmFsZBy3GxzN92P1ZXiFY99FfNPohhQtaRRhpeUkir08hgJN2bEHCJ5Ym8r9mr9mlwSG6FoiedgLaUVGwJujD9c2rcA83NEo8ayTyfCynF2AZ2pMxLHvqOYtvscGMiMzIwlZfJV301iKUVgPODJM5lpJ4iKCpOy2ByCl2_KL1uxIxgMkglpB-i7kgJc-WmYoJFoN88D89ugnEoAxNfK14N4_RyEkrLNGape9kew79nUeR6fWbVFLiGDDu25_9z-7VB-GGGk7L_Hb7YgVJ5W2FwESnkDvV1T4Q"

with tempfile.NamedTemporaryFile() as config_file, tempfile.NamedTemporaryFile() as key_file:
with open(config_file.name, "w") as f:
json.dump(
{
"email": "[email protected]",
"token": bearer_token,
"organization_id": "transcriptic",
"api_root": "http://foo:5555",
"analytics": True,
"user_id": "ufoo2",
"feature_groups": [
"can_submit_autoprotocol",
"can_upload_packages",
],
},
f,
)

connection = transcriptic.config.Connection.from_file(config_file.name)

get_request = requests.Request("GET", "http://foo:5555/get")
prepared_get = connection.session.prepare_request(get_request)

authorization_header_value = prepared_get.headers["authorization"]
self.assertEqual(authorization_header_value, bearer_token)
self.assertNotIn("x-user-token", prepared_get.headers)

def test_malformed_bearer_token(self):
"""Verify that an exception is thrown when a malformed JWT bearer token is provided"""

bearer_token = "Bearer myBigBadBearerToken"

with tempfile.NamedTemporaryFile() as config_file, tempfile.NamedTemporaryFile() as key_file:
with open(config_file.name, "w") as f:
json.dump(
{
"email": "[email protected]",
"token": bearer_token,
"organization_id": "transcriptic",
"api_root": "http://foo:5555",
"analytics": True,
"user_id": "ufoo2",
"feature_groups": [
"can_submit_autoprotocol",
"can_upload_packages",
],
},
f,
)

with self.assertRaisesRegexp(ValueError, "Malformed JWT Bearer Token"):
transcriptic.config.Connection.from_file(config_file.name)
21 changes: 13 additions & 8 deletions transcriptic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from . import routes
from .signing import StrateosSign
from .util import is_valid_jwt_token
from .version import __version__

try:
Expand Down Expand Up @@ -131,7 +132,7 @@ def __init__(

# NB: These many setattr calls update self.session.headers
# cookie authentication is mutually exclusive from token authentication
if cookie:
if cookie is not None:
if email is not None or token is not None:
warnings.warn(
"Cookie and token authentication is mutually "
Expand All @@ -142,11 +143,6 @@ def __init__(
self.cookie = cookie
self.update_session_auth(use_signature=False)
else:
if cookie is not None:
warnings.warn(
"Cookie and token authentication is mutually "
"exclusive. Ignoring cookie"
)
self.session.headers["Cookie"] = None
self.email = email
self.token = token
Expand Down Expand Up @@ -248,14 +244,23 @@ def token(self):
raise ValueError("token is not set.")

@token.setter
def token(self, value):
def token(self, value: str):
if self.cookie is not None:
warnings.warn(
"Cookie and token authentication is mutually "
"exclusive. Clearing cookie from headers"
)
self.update_headers(**{"Cookie": None})
self.update_headers(**{"X-User-Token": value})

if value is not None:
is_bearer_auth = value.startswith("Bearer")
if is_bearer_auth:
if is_valid_jwt_token(value):
self.update_headers(**{"Authorization": value})
else:
raise ValueError("Malformed JWT Bearer Token")
else:
self.update_headers(**{"X-User-Token": value})

@property
def cookie(self):
Expand Down
5 changes: 5 additions & 0 deletions transcriptic/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,8 @@ def makedirs(name, mode=None, exist_ok=False):

mode = mode if mode is not None else 0o777
makedirs(name, mode, exist_ok)


def is_valid_jwt_token(token: str):
regex = r"Bearer ([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-\+\/=]*)"
return re.fullmatch(regex, token) is not None