diff --git a/docs/source/_static/rest-api.yml b/docs/source/_static/rest-api.yml index aee43414..be222ed9 100644 --- a/docs/source/_static/rest-api.yml +++ b/docs/source/_static/rest-api.yml @@ -572,9 +572,10 @@ paths: properties: expires_in: type: number + example: 3600 description: lifetime (in seconds) after which the requested token - will expire. + will expire. Omit, or specify null or 0 for no expiration. note: type: string description: A note attached to the token for future bookkeeping diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 03e91ad5..22f81a22 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -472,7 +472,14 @@ class UserTokenListAPIHandler(APIHandler): user_kind = 'user' if isinstance(user, User) else 'service' self.log.info("%s %s requested new API token", user_kind.title(), user.name) # retrieve the model - token_model = self.token_model(orm.APIToken.find(self.db, api_token)) + orm_token = orm.APIToken.find(self.db, api_token) + if orm_token is None: + self.log.error( + "Failed to find token after creating it: %r. Maybe it expired already?", + body, + ) + raise web.HTTPError(500, "Failed to create token") + token_model = self.token_model(orm_token) token_model['token'] = api_token self.write(json.dumps(token_model)) self.set_status(201) diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 1076653d..b45a5c17 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -3,6 +3,7 @@ # Distributed under the terms of the Modified BSD License. import enum import json +import numbers from base64 import decodebytes, encodebytes from datetime import timedelta from functools import partial @@ -813,7 +814,18 @@ class APIToken(Hashed, Base): else: assert service.id is not None orm_token.service = service - if expires_in is not None: + if expires_in: + if not isinstance(expires_in, numbers.Real): + raise TypeError( + f"expires_in must be a positive integer or null, not {expires_in!r}" + ) + expires_in = int(expires_in) + # tokens must always expire in the future + if expires_in < 1: + raise ValueError( + f"expires_in must be a positive integer or null, not {expires_in!r}" + ) + orm_token.expires_at = cls.now() + timedelta(seconds=expires_in) db.commit()