2020import hashlib
2121import uuid
2222import requests
23- from typing import Callable , cast
23+ from typing import Callable , cast , Union
2424from inspect import Parameter , signature
2525from functools import wraps
2626from fastapi import APIRouter , Depends , Query as FastApiQuery , Request , HTTPException , status
@@ -51,6 +51,10 @@ class SignatureToken(BaseModel):
5151 signature_token : str
5252
5353
54+ class AppToken (BaseModel ):
55+ app_token : str
56+
57+
5458oauth2_scheme = OAuth2PasswordBearer (tokenUrl = f'{ root_path } /auth/token' , auto_error = False )
5559
5660
@@ -64,7 +68,6 @@ def create_user_dependency(
6468 Creates a dependency for getting the authenticated user. The parameters define if
6569 the authentication is required or not, and which authentication methods are allowed.
6670 '''
67-
6871 def user_dependency (** kwargs ) -> User :
6972 user = None
7073 if basic_auth_allowed :
@@ -180,17 +183,26 @@ def _get_user_basic_auth(form_data: OAuth2PasswordRequestForm) -> User:
180183def _get_user_bearer_token_auth (bearer_token : str ) -> User :
181184 '''
182185 Verifies bearer_token (throwing exception if illegal value provided) and returns the
183- corresponding user object, or None, if no bearer_token provided.
186+ corresponding user object, or None if no bearer_token provided.
184187 '''
185- if bearer_token :
186- try :
187- user = cast (datamodel .User , infrastructure .keycloak .tokenauth (bearer_token ))
188+ if not bearer_token :
189+ return None
190+
191+ try :
192+ unverified_payload = jwt .decode (bearer_token , options = {"verify_signature" : False })
193+ if unverified_payload .keys () == set (['user' , 'exp' ]):
194+ user = _get_user_from_simple_token (bearer_token )
188195 return user
189- except infrastructure .KeycloakError as e :
190- raise HTTPException (
191- status_code = status .HTTP_401_UNAUTHORIZED ,
192- detail = str (e ), headers = {'WWW-Authenticate' : 'Bearer' })
193- return None
196+ except jwt .exceptions .DecodeError :
197+ pass # token could be non-JWT, e.g. for testing
198+
199+ try :
200+ user = cast (datamodel .User , infrastructure .keycloak .tokenauth (bearer_token ))
201+ return user
202+ except infrastructure .KeycloakError as e :
203+ raise HTTPException (
204+ status_code = status .HTTP_401_UNAUTHORIZED ,
205+ detail = str (e ), headers = {'WWW-Authenticate' : 'Bearer' })
194206
195207
196208def _get_user_upload_token_auth (upload_token : str ) -> User :
@@ -227,21 +239,8 @@ def _get_user_signature_token_auth(signature_token: str, request: Request) -> Us
227239 corresponding user object, or None, if no upload_token provided.
228240 '''
229241 if signature_token :
230- try :
231- decoded = jwt .decode (signature_token , config .services .api_secret , algorithms = ['HS256' ])
232- return datamodel .User .get (user_id = decoded ['user' ])
233- except KeyError :
234- raise HTTPException (
235- status_code = status .HTTP_401_UNAUTHORIZED ,
236- detail = 'Token with invalid/unexpected payload.' )
237- except jwt .ExpiredSignatureError :
238- raise HTTPException (
239- status_code = status .HTTP_401_UNAUTHORIZED ,
240- detail = 'Expired token.' )
241- except jwt .InvalidTokenError :
242- raise HTTPException (
243- status_code = status .HTTP_401_UNAUTHORIZED ,
244- detail = 'Invalid token.' )
242+ user = _get_user_from_simple_token (signature_token )
243+ return user
245244 elif request :
246245 auth_cookie = request .cookies .get ('Authorization' )
247246 if auth_cookie :
@@ -261,6 +260,28 @@ def _get_user_signature_token_auth(signature_token: str, request: Request) -> Us
261260 return None
262261
263262
263+ def _get_user_from_simple_token (token ):
264+ '''
265+ Verifies a simple token (throwing exception if illegal value provided) and returns the
266+ corresponding user object, or None if no token was provided.
267+ '''
268+ try :
269+ decoded = jwt .decode (token , config .services .api_secret , algorithms = ['HS256' ])
270+ return datamodel .User .get (user_id = decoded ['user' ])
271+ except KeyError :
272+ raise HTTPException (
273+ status_code = status .HTTP_401_UNAUTHORIZED ,
274+ detail = 'Token with invalid/unexpected payload.' )
275+ except jwt .ExpiredSignatureError :
276+ raise HTTPException (
277+ status_code = status .HTTP_401_UNAUTHORIZED ,
278+ detail = 'Expired token.' )
279+ except jwt .InvalidTokenError :
280+ raise HTTPException (
281+ status_code = status .HTTP_401_UNAUTHORIZED ,
282+ detail = 'Invalid token.' )
283+
284+
264285_bad_credentials_response = status .HTTP_401_UNAUTHORIZED , {
265286 'model' : HTTPExceptionModel ,
266287 'description' : strip ('''
@@ -287,7 +308,6 @@ async def get_token(form_data: OAuth2PasswordRequestForm = Depends()):
287308 You only need to provide `username` and `password` values. You can ignore the other
288309 parameters.
289310 '''
290-
291311 try :
292312 access_token = infrastructure .keycloak .basicauth (
293313 form_data .username , form_data .password )
@@ -311,7 +331,6 @@ async def get_token_via_query(username: str, password: str):
311331 This is an convenience alternative to the **POST** version of this operation.
312332 It allows you to retrieve an *access token* by providing username and password.
313333 '''
314-
315334 try :
316335 access_token = infrastructure .keycloak .basicauth (username , password )
317336 except infrastructure .KeycloakError :
@@ -328,21 +347,51 @@ async def get_token_via_query(username: str, password: str):
328347 tags = [default_tag ],
329348 summary = 'Get a signature token' ,
330349 response_model = SignatureToken )
331- async def get_signature_token (user : User = Depends (create_user_dependency ())):
350+ async def get_signature_token (
351+ user : Union [User , None ] = Depends (create_user_dependency (required = True ))):
332352 '''
333353 Generates and returns a signature token for the authenticated user. Authentication
334354 has to be provided with another method, e.g. access token.
335355 '''
356+ signature_token = generate_simple_token (user .user_id , expires_in = 10 )
357+ return {'signature_token' : signature_token }
336358
337- expires_at = datetime .datetime .utcnow () + datetime .timedelta (seconds = 10 )
338- signature_token = jwt .encode (
339- dict (user = user .user_id , exp = expires_at ),
340- config .services .api_secret , 'HS256' )
341359
342- return {'signature_token' : signature_token }
360+ @router .get (
361+ '/app_token' ,
362+ tags = [default_tag ],
363+ summary = 'Get an app token' ,
364+ response_model = AppToken )
365+ async def get_app_token (
366+ expires_in : int = FastApiQuery (gt = 0 , le = config .services .app_token_max_expires_in ),
367+ user : User = Depends (create_user_dependency (required = True ))):
368+ '''
369+ Generates and returns an app token with the requested expiration time for the
370+ authenticated user. Authentication has to be provided with another method,
371+ e.g. access token.
372+
373+ This app token can be used like the access token (see `/auth/token`) on subsequent API
374+ calls to authenticate you using the HTTP header `Authorization: Bearer <app token>`.
375+ It is provided for user convenience as a shorter token with a user-defined (probably
376+ longer) expiration time than the access token.
377+ '''
378+ app_token = generate_simple_token (user .user_id , expires_in )
379+ return {'app_token' : app_token }
380+
381+
382+ def generate_simple_token (user_id , expires_in : int ):
383+ '''
384+ Generates and returns JWT encoding just user_id and expiration time, signed with the
385+ API secret.
386+ '''
387+ expires_at = datetime .datetime .utcnow () + datetime .timedelta (seconds = expires_in )
388+ payload = dict (user = user_id , exp = expires_at )
389+ token = jwt .encode (payload , config .services .api_secret , 'HS256' )
390+ return token
343391
344392
345393def generate_upload_token (user ):
394+ '''Generates and returns upload token for user.'''
346395 payload = uuid .UUID (user .user_id ).bytes
347396 signature = hmac .new (
348397 bytes (config .services .api_secret , 'utf-8' ),
0 commit comments