Skip to content

Commit

Permalink
Cache last used token
Browse files Browse the repository at this point in the history
  • Loading branch information
enolfc committed Jul 17, 2024
1 parent c8c963f commit 76d5220
Showing 1 changed file with 68 additions and 34 deletions.
102 changes: 68 additions & 34 deletions egi_notebooks_hub/egiauthenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from urllib.parse import urlencode

import jwt
import jwt.exceptions
from jupyterhub.handlers import BaseHandler
from oauthenticator.generic import GenericOAuthenticator
from tornado import web
Expand Down Expand Up @@ -55,11 +56,23 @@ async def exchange_for_refresh_token(self, access_token):
token_info = json.loads(resp.body.decode("utf8", "replace"))
return token_info.get("refresh_token", None)

async def get(self):
async def _get_previous_hub_token(self, user, jwt_token):
if not user:
return None
auth_state = await user.get_auth_state()
if auth_state and auth_state.get("access_token", None) == jwt_token:
self.log.debug("JWT previously validated, reusing API token if available")
print("*" * 80)
print("*" * 80)
print("*" * 80)
print("*" * 80)
return auth_state.get("jwt_api_token", None)

def _get_token(self):
auth_header = self.request.headers.get("Authorization", "")
if auth_header:
try:
bearer, token = auth_header.split()
bearer, jwt_token = auth_header.split()
if bearer.lower() != "bearer":
self.log.debug("Unexpected authorization header format")
raise HTTPError(401)
Expand All @@ -69,38 +82,59 @@ async def get(self):
else:
self.log.debug("No authorization header")
raise HTTPError(401)
token_info = {
"access_token": token,
"token_type": "bearer",
}
user = await self.login_user(token_info)
if user is None:
raise web.HTTPError(403, self.authenticator.custom_403_message)
auth_state = await user.get_auth_state()
if auth_state and not auth_state.get("refresh_token", None):
self.log.debug("Refresh token is not available")
refresh_token = await self.exchange_for_refresh_token(token)
if refresh_token:
self.log.debug("Got refresh token from exchange")
auth_state["refresh_token"] = refresh_token

# extract from the jwt token (without verification!)
decoded_token = jwt.decode(token, options={"verify_signature": False})
# default: 1h token
expires_in = 3600
if "exp" in decoded_token and "iat" in decoded_token:
expires_in = decoded_token["exp"] - decoded_token["iat"]

# Possible optimisation here: instead of creating a new token every time,
# go through user.api_tokens and get one from there
api_token = user.new_api_token(
note="JWT auth token",
expires_in=expires_in,
# TODO: this may be tuned, but should be a post
# call with a body specifying the roles and scopes
# roles=token_roles,
# scopes=token_scopes,
)
try:
decoded_token = jwt.decode(
jwt_token,
options=dict(verify_signature=False, verify_exp=True),
)
except jwt.exceptions.InvalidTokenError as e:
self.log.debug(f"Invalid token {e}")
raise web.HTTPError(401)
return jwt_token, decoded_token

async def get(self):
user = None
jwt_token, decoded_token = self._get_token()
try:
username = self.authenticator.user_info_to_username(decoded_token)
user = self.find_user(username)
except ValueError as e:
self.log.debug(f"Unable to get username from token: {e}")
api_token = await self._get_previous_hub_token(user, jwt_token)
if not api_token:
self.log.debug("Authenticating user")
token_info = {
"access_token": jwt_token,
"token_type": "bearer",
}
user = await self.login_user(token_info)
if user is None:
raise web.HTTPError(403, self.authenticator.custom_403_message)
auth_state = await user.get_auth_state()
if auth_state and not auth_state.get("refresh_token", None):
self.log.debug("Refresh token is not available")
refresh_token = await self.exchange_for_refresh_token(jwt_token)
if refresh_token:
self.log.debug("Got refresh token from exchange")
auth_state["refresh_token"] = refresh_token

# default: 1h token
expires_in = 3600
if "exp" in decoded_token and "iat" in decoded_token:
expires_in = decoded_token["exp"] - decoded_token["iat"]

# Possible optimisation here: instead of creating a new token every time,
# go through user.api_tokens and get one from there
api_token = user.new_api_token(
note="JWT auth token",
expires_in=expires_in,
# TODO: this may be tuned, but should be a post
# call with a body specifying the roles and scopes
# roles=token_roles,
# scopes=token_scopes,
)
auth_state["jwt_api_token"] = api_token
await user.save_auth_state(auth_state)
self.finish({"token": api_token, "user": user.name})


Expand Down

0 comments on commit 76d5220

Please sign in to comment.