55from requests .auth import HTTPBasicAuth
66import json
77import time
8- from flask import g , request , Response , render_template , redirect , make_response , send_from_directory , session , url_for
8+ from flask import g , request , Response , render_template , redirect , make_response , send_from_directory , session , url_for , jsonify
99from flask_restful import Resource
1010from openapi_core import openapi_request_validator
1111from openapi_core .contrib .flask import FlaskOpenAPIRequest
12+ from jwcrypto import jwk
1213import sqlalchemy
1314import secrets
1415import random
@@ -44,6 +45,7 @@ class OAuthMetadataResource(Resource):
4445 See https://datatracker.ietf.org/doc/html/rfc8414
4546 """
4647 def get (self ):
48+ logger .info ("top of GET /v3/oauth2/.well-known/oauth-authorization-server" )
4749 tenant_id = g .request_tenant_id
4850 config = tenant_configs_cache .get_config (tenant_id )
4951 allowable_grant_types = json .loads (config .allowable_grant_types )
@@ -203,7 +205,7 @@ def get(self):
203205
204206class UserInfoResource (Resource ):
205207 def get (self ):
206- logger .debug (f'top of GET /userinfo' )
208+ logger .debug (f'top of GET /v3/oauth2/ userinfo' )
207209 tenant_id = g .request_tenant_id
208210 # note that the user info endpoint is more limited for custom oauth idp extensions in general because the
209211 # custom OAuth server may not provider a profile endpoint.
@@ -218,7 +220,7 @@ def get(self):
218220
219221class ProfileResource (Resource ):
220222 def get (self , username ):
221- logger .debug (f'top of GET /profiles/{ username } ' )
223+ logger .debug (f'top of GET /v3/ profiles/{ username } ' )
222224 tenant_id = g .request_tenant_id
223225 # note that the user info endpoint is more limited for custom oauth idp extensions in general because the
224226 # custom OAuth server may not provider a profile endpoint.
@@ -352,6 +354,56 @@ def put(self):
352354 return utils .ok (result = config .serialize , msg = "Tenant config object updated successfully." )
353355
354356
357+ # ---------------------------------
358+ # OIDC endpoints
359+ # ---------------------------------
360+
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+
384+ class OIDCjwksResource (Resource ):
385+ """
386+ Provides the OIDC jwks endpoint.
387+ """
388+ def get (self ):
389+ logger .info ("top of GET /v3/oauth2/jwks" )
390+ tenant_id = g .request_tenant_id
391+ config = tenant_configs_cache .get_config (tenant_id )
392+ allowable_grant_types = json .loads (config .allowable_grant_types )
393+ tenant = t .tenant_cache .get_tenant_config (tenant_id = tenant_id )
394+ base_url = tenant .base_url
395+
396+ # unpack jwks info from tenant public key
397+ pem_key = tenant .public_key
398+ key = jwk .JWK .from_pem (pem_key .encode ('utf-8' ))
399+ jwk_json = key .export (as_dict = True )
400+
401+ json_response = {
402+ 'keys' : [jwk_json ]
403+ }
404+ return json_response #utils.ok(result=metadata, msg='OAuth OIDC metadata retrieved successfully.')
405+
406+
355407# ---------------------------------
356408# Authorization Server controllers
357409# ---------------------------------
@@ -363,7 +415,7 @@ def check_client(use_session=False):
363415 and returns the associated objects.
364416
365417 If use_session is True, this function will check for the client credentials out of the session. This is
366- used when the tenant is configured with a 3rd-party OAuth2 sever that does not pass back the original
418+ used when the tenant is configured with a 3rd-party OAuth2 server that does not pass back the original
367419 client credentials.
368420 """
369421 # tenant_id should be determined by the request URL -
@@ -424,6 +476,8 @@ def check_client(use_session=False):
424476 raise errors .ResourceError ("Required query parameter redirect_uri missing." )
425477 if not client .callback_url == client_redirect_uri :
426478 logout ()
479+ # cgarcia - I'm not sure if the uris should be exact or if only domain should match. But I'll leave this as is.
480+ logger .debug (f"redirect_uri query parameter does not match registered callback_url for the client. redirect_uri: { client_redirect_uri } ; callback_url: { client .callback_url } " )
427481 raise errors .ResourceError (
428482 "redirect_uri query parameter does not match the registered callback_url for the client." )
429483 return client_id , client_redirect_uri , client_state , client , response_type
@@ -867,7 +921,7 @@ class AuthorizeResource(Resource):
867921 """
868922
869923 def get (self ):
870- logger .info ("top of GET /oauth2/authorize" )
924+ logger .info ("top of GET /v3/ oauth2/authorize" )
871925 is_device_flow = True if 'device_login' in session else False
872926 # if we are using the multi_idp custom oa2 extension type it is possible we are being redirected here, not by the
873927 # original web client, but by our select_idp page, in which case we need to get the client out of the session.
@@ -1000,7 +1054,7 @@ def get(self):
10001054 return make_response (render_template ('authorize.html' , ** context ), 200 , headers )
10011055
10021056 def post (self ):
1003- logger .debug ("top of POST /oauth2/authorize" )
1057+ logger .info ("top of POST /v3 /oauth2/authorize" )
10041058 # selecting a tenant id is required before logging in -
10051059 tenant_id = g .request_tenant_id
10061060 if not tenant_id :
@@ -1210,7 +1264,7 @@ class OAuth2ProviderExtCallback(Resource):
12101264 GET /v3/oauth2/extensions/oa2/callback -- receive the authorization code and exchange it for a token.
12111265 """
12121266 def get (self ):
1213- logger .debug ("top of GET /oauth2/extensions/oa2/callback" )
1267+ logger .info ("top of GET /v3 /oauth2/extensions/oa2/callback" )
12141268 # use tenant id to create the tenant oa2 extension config
12151269 tenant_id = g .request_tenant_id
12161270 session ['tenant_id' ] = tenant_id
@@ -1259,17 +1313,16 @@ def get(self):
12591313 response_type = 'code' ))
12601314
12611315
1262- class TokensResource ( Resource ):
1316+ def _handle_tokens_request ( request , oidc = False ):
12631317 """
12641318 Implements the oauth2/tokens endpoint for generating tokens for the following grant types:
12651319 * password
12661320 * authorization_code
12671321 * refresh_token
12681322 * device_code
12691323 """
1270-
1271- def post (self ):
1272- logger .debug ("top of POST /oauth2/tokens" )
1324+ if oidc or not oidc :
1325+ logger .info ("top of POST /v3/oauth2/tokens" )
12731326 # support content-type www-form by setting the body on the request equal to the JSON
12741327 if request .content_type .startswith ('application/x-www-form-urlencoded' ):
12751328 logger .debug (f"handling x-www-form data" )
@@ -1456,6 +1509,12 @@ def post(self):
14561509 }
14571510 if idp_id :
14581511 content ['claims' ]['tapis/idp_id' ] = idp_id
1512+ if oidc :
1513+ if client_id :
1514+ content ['claims' ]['aud' ] = client_id
1515+ content ['claims' ]['iat' ] = int (time .time ())
1516+ content ['claims' ]['extravar' ] = username
1517+ content ['claims' ]['email' ] = username
14591518
14601519 # only generate a refresh token when OAuth client is passed
14611520 if client_id and client_key :
@@ -1561,10 +1620,31 @@ def post(self):
15611620 f"Contact system administrator. (Debug data: { e } )"
15621621 logger .error (msg )
15631622 raise errors .ResourceError (f"{ msg } " )
1564-
1623+
1624+ if oidc :
1625+ logger .info ("Token endpoint with OIDC flag set." )
1626+ response_json = {
1627+ 'access_token' : result ['access_token' ]['access_token' ],
1628+ 'expires_in' : result ['access_token' ]['expires_in' ],
1629+ 'token_type' : 'Bearer' ,
1630+ 'id_token' : result ['access_token' ]['id_token' ]}
1631+ logger .info (f"OIDC response: { response_json } " )
1632+ # oidc endpoints aren't expecting our tapis 5 stanza response.
1633+ return jsonify (response_json )
1634+
15651635 return utils .ok (result = result , msg = "Token created successfully." )
15661636
15671637
1638+ class TokensResource (Resource ):
1639+ def post (self ):
1640+ return _handle_tokens_request (request , oidc = False )
1641+
1642+
1643+ class OIDCTokensResource (Resource ):
1644+ def post (self ):
1645+ return _handle_tokens_request (request , oidc = True )
1646+
1647+
15681648class V2TokenResource (Resource ):
15691649 def post (self ):
15701650 logger .debug ("Top of v2 Token Resource" )
0 commit comments