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

RAS Authentication Updates #291

Merged
merged 10 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ dcicutils
Change Log
----------

8.3.0
=========

* Updates for RAS to Redis API

8.2.0
=====
* 2023-11-02
Expand Down
4 changes: 2 additions & 2 deletions dcicutils/deployment_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ def build_ini_file_from_template(cls, template_file_name, init_file_name, *,
indexer=None, index_server=None, sentry_dsn=None, tibanna_cwls_bucket=None,
tibanna_output_bucket=None,
application_bucket_prefix=None, foursight_bucket_prefix=None,
auth0_domain=DEFAULT_AUTH0_DOMAIN, auth0_client=None, auth0_secret=None,
auth0_domain=None, auth0_client=None, auth0_secret=None,
auth0_allowed_connections=None,
re_captcha_key=None, re_captcha_secret=None,
redis_server=None,
Expand Down Expand Up @@ -680,7 +680,7 @@ def build_ini_stream_from_template(cls, template_file_name, init_file_stream, *,
sentry_dsn = sentry_dsn or os.environ.get("ENCODED_SENTRY_DSN", "")

# Auth0 Configuration
auth0_domain = auth0_domain or os.environ.get("ENCODED_AUTH0_DOMAIN", "")
auth0_domain = auth0_domain or os.environ.get("ENCODED_AUTH0_DOMAIN", cls.DEFAULT_AUTH0_DOMAIN)
auth0_client = auth0_client or os.environ.get("ENCODED_AUTH0_CLIENT", "")
auth0_secret = auth0_secret or os.environ.get("ENCODED_AUTH0_SECRET", "")
auth0_allowed_connections = auth0_allowed_connections or os.environ.get("ENCODED_AUTH0_ALLOWED_CONNECTIONS", "")
Expand Down
25 changes: 18 additions & 7 deletions dcicutils/redis_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ def _build_redis_key(namespace: str, token: str) -> str:
"""
return f'{namespace}:session:{token}'

def __init__(self, *, namespace: str, jwt: str, token=None, expiration=None):
def __init__(self, *, namespace: str, jwt: str, email: str, token=None, expiration=None):
""" Creates a Redis Session object, storing a hash of the JWT into Redis and returning this
value as the session token.
:param namespace: namespace to build key under, for example the env name
:param jwt: jwt generated for this user
:param email: email verified for this user
:param token: value of token if passed, if not one will be generated
:param expiration: expiration of token if passed, if not new expiration will be generated
"""
Expand All @@ -59,11 +60,12 @@ def __init__(self, *, namespace: str, jwt: str, token=None, expiration=None):
self.namespace = namespace
self.redis_key = self._build_redis_key(self.namespace, self.session_token)
self.jwt = jwt
self.email = email
self.expiration = expiration or self._build_session_expiration()

def __eq__(self, other):
""" Evaluates equality of two session objects based on the value of the session hset """
return (self.redis_key == other.redis_key) and (self.jwt == other.jwt)
return (self.redis_key == other.redis_key) and (self.jwt == other.jwt) and (self.email == other.email)

def get_session_token(self) -> str:
""" Extracts the session token stored on this object """
Expand All @@ -81,6 +83,10 @@ def get_jwt(self) -> str:
""" Returns the JWT set on this session token object """
return self.jwt

def get_email(self) -> str:
""" Returns the email set on this session token object """
return self.email

@classmethod
def from_redis(cls, *, redis_handler: RedisBase, namespace: str, token: str):
""" Builds a RedisSessionToken from an existing record - allows extracting JWT
Expand All @@ -93,27 +99,30 @@ def from_redis(cls, *, redis_handler: RedisBase, namespace: str, token: str):
redis_key = f'{namespace}:session:{token}'
redis_entry = redis_handler.get(redis_key)
if redis_entry:
jwt_and_email = redis_entry.split(':')
jwt = jwt_and_email[0]
email = jwt_and_email[1] if len(jwt_and_email) > 1 else None
expiration = redis_handler.ttl(redis_key)
return cls(namespace=namespace, jwt=redis_entry,
return cls(namespace=namespace, jwt=jwt, email=email,
token=token, expiration=expiration)

def decode_jwt(self, audience: str, secret: str, leeway: int = 30) -> dict:
def decode_jwt(self, audience: str, secret: str, leeway: int = 30, algorithms: list = ['HS256']) -> dict:
""" Decodes JWT to grab info such as the email
:param audience: audience under which to decode, typically Auth0Client
:param secret: secret to decrypt using, typically Auth0Secret
:param leeway: numerical value in seconds to account for clock drift
:return: a decoded JWT in dictionary format
"""
return jwt.decode(self.jwt, secret, audience=audience, leeway=leeway,
options={'verify_signature': True}, algorithms=['HS256'])
options={'verify_signature': True}, algorithms=algorithms)

def store_session_token(self, *, redis_handler: RedisBase) -> bool:
""" Stores the created session token object as an hset in Redis
:param redis_handler: handle to Redis API
:return: True if successful, raise Exception otherwise
"""
try:
redis_handler.set(self.redis_key, self.jwt, exp=self.expiration)
redis_handler.set(self.redis_key, f'{self.jwt}:{self.email or ""}', exp=self.expiration)
except Exception as e:
log.error(str(e))
raise RedisException()
Expand All @@ -129,10 +138,11 @@ def validate_session_token(self, *, redis_handler: RedisBase) -> bool:
return False # if it doesn't exist it's not valid
return True # if it does exist it must be valid since we always send with TTL

def update_session_token(self, *, redis_handler: RedisBase, jwt: str) -> bool:
def update_session_token(self, *, redis_handler: RedisBase, jwt: str, email: str) -> bool:
""" Refreshes the session token, jwt (if different) and expiration stored in Redis
:param redis_handler: handle to Redis API
:param jwt: jwt of user
:param email: email of user
:return: True if successful, raise Exception otherwise
"""
# remove old token
Expand All @@ -142,6 +152,7 @@ def update_session_token(self, *, redis_handler: RedisBase, jwt: str) -> bool:
self.redis_key = self._build_redis_key(self.namespace, self.session_token)
self.expiration = self._build_session_expiration()
self.jwt = jwt
self.email = email
return self.store_session_token(redis_handler=redis_handler)

def delete_session_token(self, *, redis_handler: RedisBase) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "dcicutils"
version = "8.2.0"
version = "8.3.0"
description = "Utility package for interacting with the 4DN Data Portal and other 4DN resources"
authors = ["4DN-DCIC Team <support@4dnucleome.org>"]
license = "MIT"
Expand Down
8 changes: 6 additions & 2 deletions test/test_redis_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_redis_session_basic(self, redisdb):
rd = RedisBase(redisdb)
session_token = RedisSessionToken(
namespace=self.NAMESPACE,
email=self.DUMMY_EMAIL,
jwt=self.DUMMY_JWT
)
session_token.store_session_token(redis_handler=rd)
Expand All @@ -34,7 +35,7 @@ def test_redis_session_basic(self, redisdb):
assert not session_token.validate_session_token(redis_handler=rd)
# update with a new token and expiration
session_token.redis_key = working_token
session_token.update_session_token(redis_handler=rd, jwt=self.DUMMY_JWT)
session_token.update_session_token(redis_handler=rd, email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT)
assert session_token.validate_session_token(redis_handler=rd)
session_token.redis_key = working_token
assert not session_token.validate_session_token(redis_handler=rd)
Expand All @@ -47,13 +48,14 @@ def test_redis_session_expired_token(self, redisdb):
with mock.patch.object(RedisSessionToken, '_build_session_expiration', self.mock_build_session_expiration):
session_token = RedisSessionToken(
namespace=self.NAMESPACE,
email=self.DUMMY_EMAIL,
jwt=self.DUMMY_JWT
)
session_token.store_session_token(redis_handler=rd)
time.sleep(2)
assert not session_token.validate_session_token(redis_handler=rd)
# update then should validate
session_token.update_session_token(redis_handler=rd, jwt=self.DUMMY_JWT)
session_token.update_session_token(redis_handler=rd, email=self.DUMMY_EMAIL, jwt=self.DUMMY_JWT)
assert session_token.validate_session_token(redis_handler=rd)

def test_redis_session_many_sessions(self, redisdb):
Expand All @@ -65,6 +67,7 @@ def test_redis_session_many_sessions(self, redisdb):
for _ in range(5):
session_token = RedisSessionToken(
namespace=self.NAMESPACE,
email=self.DUMMY_EMAIL,
jwt=self.DUMMY_JWT
)
session_token.store_session_token(redis_handler=rd)
Expand Down Expand Up @@ -92,6 +95,7 @@ def test_redis_session_from_redis_equality(self, redisdb):
rd = RedisBase(redisdb)
session_token_local = RedisSessionToken(
namespace=self.NAMESPACE,
email=self.DUMMY_EMAIL,
jwt=self.DUMMY_JWT
)
session_token_local.store_session_token(redis_handler=rd)
Expand Down
Loading