1010from openapi_core import openapi_request_validator
1111from openapi_core .contrib .flask import FlaskOpenAPIRequest
1212from jwcrypto import jwk
13+ import jwt
1314import sqlalchemy
1415import secrets
1516import random
@@ -203,19 +204,74 @@ def get(self):
203204 return resp
204205
205206
207+ def _handle_userinfo_request (request , oidc = False ):
208+ tenant_id = g .request_tenant_id
209+ if oidc :
210+ logger .debug (f'top of GET /v3/oauth2/userinfo/oidc - tenant_id: { tenant_id } ' )
211+ else :
212+ logger .debug (f'top of GET /v3/oauth2/userinfo - tenant_id: { tenant_id } ' )
213+ # note that the user info endpoint is more limited for custom oauth idp extensions in general because the
214+ # custom OAuth server may not provide a profile endpoint.
215+ custom_oa2_extension_type = tenant_configs_cache .get_custom_oa2_extension_type (tenant_id = tenant_id )
216+ ## token should maybe already have:
217+ # jti iss sub exp tapis/tenant_id tapis/token_type
218+ # tapis/delegation tapis/delegation_sub tapis/username
219+ # tapis/account_type tapis/client_id tapis/grant_type
220+
221+ if custom_oa2_extension_type and not custom_oa2_extension_type == 'ldap' :
222+ logger .debug (f"Using custom auth for userinfo; custom_oa2_extension_type: { custom_oa2_extension_type } " )
223+ logger .debug (f"g.token_claims - { g .token_claims } " )
224+ result = {"username" : g .username }
225+ return utils .ok (result = result , msg = "User profile retrieved successfully - custom auth extension provider" )
226+
227+ userinfo = get_tenant_user (tenant_id = tenant_id , username = g .username )
228+
229+ ## Rubin Science place needs
230+ # rubin scope with info via data_rights
231+ # adding data rights for specific users for rubin - test
232+ logger .debug (f"userinfo: { userinfo .serialize } " )
233+ try :
234+ username = userinfo .get ('username' )
235+ except :
236+ username = "TALKTODEV"
237+ if oidc :
238+ logger .debug (f"inside of oidc userinfo; username: { username } " )
239+ ## This code still doesn't matter, was attempting some debugging for rubin place
240+ # Kevin got Gafaelfawr to look in the "correct field" to map groups
241+ if username and username in ["cgarcia" , "mpackard" , "kprice" , "jstubbs" ]:
242+ data_rights = get_user_data_rights (username )
243+ if data_rights :
244+ userinfo ["data_rights" ] = " " .join (data_rights )
245+
246+ # return token + userinfo as return for bookstack OIDC userinfo call.
247+ # bookstack at leasts needs sub claim.
248+ try :
249+ token_dict = jwt .decode (g .x_tapis_token , options = {"verify_signature" : False })
250+ newinfo = userinfo .serialize
251+ newinfo .update (token_dict )
252+ except Exception as e :
253+ logger .debug (f"Error creating userinfo+token object: { e } , token: { g .x_tapis_token } " )
254+ raise errors .ResourceError ("Error with token and userinfo objects." )
255+ return jsonify (newinfo )
256+
257+ return utils .ok (result = userinfo .serialize , msg = "User profile retrieved successfully." )
258+
259+
260+ def get_user_data_rights (username ):
261+ # Implement logic to retrieve the list of data releases the user has access to
262+ # This function should return a list of strings representing data releases
263+ return ["release1" , "release2" , "lsst-sqre" , "admin:jupyterlab" , "admin" , "jupyterlab" , "square" , "tacc-spherex" ]
264+
265+
206266class UserInfoResource (Resource ):
207267 def get (self ):
208- logger .debug (f'top of GET /v3/oauth2/userinfo' )
209- tenant_id = g .request_tenant_id
210- # note that the user info endpoint is more limited for custom oauth idp extensions in general because the
211- # custom OAuth server may not provider a profile endpoint.
212- custom_oa2_extension_type = tenant_configs_cache .get_custom_oa2_extension_type (tenant_id = tenant_id )
213- if custom_oa2_extension_type and not custom_oa2_extension_type == 'ldap' :
214- result = {"username" : g .username }
215- return utils .ok (result = result , msg = "User profile retrieved successfully." )
268+ return _handle_userinfo_request (request , oidc = False )
269+
270+
271+ class OIDCUserInfoResource (Resource ):
272+ def get (self ):
273+ return _handle_userinfo_request (request , oidc = True )
216274
217- user = get_tenant_user (tenant_id = tenant_id , username = g .username )
218- return utils .ok (result = user .serialize , msg = "User profile retrieved successfully." )
219275
220276
221277class ProfileResource (Resource ):
@@ -358,29 +414,6 @@ def put(self):
358414# OIDC endpoints
359415# ---------------------------------
360416
361- # class OIDCMetadataResource(Resource):
362- # """
363- # Provides the OIDC .well-known endpoint.
364- # """
365- # def get(self):
366- # logger.info("top of GET /v3/oauth2/.well-known/openid-configuration")
367- # tenant_id = g.request_tenant_id
368- # config = tenant_configs_cache.get_config(tenant_id)
369- # allowable_grant_types = json.loads(config.allowable_grant_types)
370- # tenant = t.tenant_cache.get_tenant_config(tenant_id=tenant_id)
371- # base_url = tenant.base_url
372- # json_response = {
373- # 'issuer': f'{base_url}/v3/tokens',
374- # 'authorization_endpoint': f'{base_url}/v3/oauth2/authorize',
375- # 'token_endpoint': f'{base_url}/v3/oauth2/tokens/oidc?oidc=true',
376- # 'jwks_uri': f'{base_url}/v3/oauth2/jwks',
377- # 'registration_endpoint': f'{base_url}/v3/oauth2/clients',
378- # 'grant_types_supported': allowable_grant_types,
379- # 'userinfo_endpoint': f'{base_url}/v3/oauth2/userinfo/oidc',
380- # }
381- # return json_response #utils.ok(result=metadata, msg='OAuth OIDC metadata retrieved successfully.')
382-
383-
384417class OIDCjwksResource (Resource ):
385418 """
386419 Provides the OIDC jwks endpoint.
@@ -397,11 +430,17 @@ def get(self):
397430 pem_key = tenant .public_key
398431 key = jwk .JWK .from_pem (pem_key .encode ('utf-8' ))
399432 jwk_json = key .export (as_dict = True )
433+ # check for required values:
434+ if 'alg' not in jwk_json .keys ():
435+ jwk_json ['alg' ] = 'RS256'
436+ if 'typ' not in jwk_json .keys ():
437+ jwk_json ['typ' ] = 'JWT'
438+ # NOTE 2025.3.28 kprice -- these values can be hard coded since they are also hard coded in tokens. If these values ever change in tokens we'll need to update this block.
400439
401440 json_response = {
402441 'keys' : [jwk_json ]
403442 }
404- return json_response #utils.ok(result=metadata, msg='OAuth OIDC metadata retrieved successfully.')
443+ return jsonify ( json_response ) #utils.ok(result=metadata, msg='OAuth OIDC metadata retrieved successfully.')
405444
406445
407446# ---------------------------------
@@ -459,7 +498,6 @@ def check_client(use_session=False):
459498 logout ()
460499 raise errors .ResourceError ("Required query parameter client_id missing." )
461500 # make sure the client exists and the redirect_uri matches
462- logger .debug (f"checking for client with id: { client_id } in tenant { tenant_id } " )
463501 client = Client .query .filter_by (tenant_id = tenant_id , client_id = client_id ).first ()
464502 if not client :
465503 logout ()
@@ -1405,6 +1443,7 @@ def _handle_tokens_request(request, oidc=False):
14051443 client = Client .query .filter_by (tenant_id = tenant_id , client_id = client_id , client_key = client_key ).first ()
14061444 if not client :
14071445 # todo -- remove session
1446+ logger .debug (f'Client with id { client_id } and key { client_key } not found on tenant { tenant_id } .' )
14081447 raise errors .ResourceError (msg = f'Invalid client credentials: { client_id } , { client_key } . '
14091448 f'session: { session } ' )
14101449
@@ -1511,6 +1550,7 @@ def _handle_tokens_request(request, oidc=False):
15111550 content ['claims' ]['tapis/idp_id' ] = idp_id
15121551 if oidc :
15131552 if client_id :
1553+ # bookstack for example requires aud to match client id
15141554 content ['claims' ]['aud' ] = client_id
15151555 content ['claims' ]['iat' ] = int (time .time ())
15161556 content ['claims' ]['extravar' ] = username
0 commit comments