Skip to content

Commit

Permalink
Merge pull request #291 from 4dn-dcic/ras_updates
Browse files Browse the repository at this point in the history
RAS Authentication Updates
  • Loading branch information
utku-ozturk committed Nov 17, 2023
2 parents 15e795f + 0faf224 commit 1de66d4
Show file tree
Hide file tree
Showing 5 changed files with 32 additions and 12 deletions.
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

0 comments on commit 1de66d4

Please sign in to comment.