This commit is contained in:
2025-08-10 02:33:00 -03:00
parent b7143ea634
commit a77cab45c1
10 changed files with 35 additions and 14 deletions

View File

@@ -22,8 +22,8 @@ OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
GRANT_TYPES_EXPIRES_IN = { GRANT_TYPES_EXPIRES_IN = {
'refresh_token': 900, 'refresh_token': 600,
'authorization_code': 900, 'authorization_code': 600,
} }

View File

@@ -0,0 +1,4 @@
from authlib.oauth2 import ResourceProtector as _ResourceProtector
class ResourceProtector(_ResourceProtector): ...

View File

@@ -1,8 +1,12 @@
import re
from authlib.oauth2 import OAuth2Request from authlib.oauth2 import OAuth2Request
from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants
from authlib.oauth2.rfc7636 import CodeChallenge from authlib.oauth2.rfc7636 import CodeChallenge
from authlib.oidc.core import OpenIDCode as OpenIDCode_ from authlib.oidc.core import OpenIDCode as OpenIDCode_
from authlib.oidc.core import UserInfo from authlib.oidc.core import UserInfo
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from layercake.dateutils import now, ttl from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.funcs import omit, pick from layercake.funcs import omit, pick
@@ -17,6 +21,7 @@ from integrations.apigateway_oauth2.tokens import (
OAuth2Token, OAuth2Token,
) )
logger = Logger(__name__)
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client) oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
@@ -137,21 +142,29 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
return pick(('id', 'name', 'email', 'email_verified'), user) return pick(('id', 'name', 'email', 'email_verified'), user)
class RefreshTokenNotFoundError(NotFoundError):
def __init__(self, *_):
super().__init__('Refresh token not found')
class RefreshTokenGrant(grants.RefreshTokenGrant): class RefreshTokenGrant(grants.RefreshTokenGrant):
INCLUDE_NEW_REFRESH_TOKEN = True
TOKEN_ENDPOINT_AUTH_METHODS = [ TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic', 'client_secret_basic',
'client_secret_post', 'client_secret_post',
'none', 'none',
] ]
INCLUDE_NEW_REFRESH_TOKEN = True
def authenticate_refresh_token(self, refresh_token: str, **kwargs) -> TokenMixin: def authenticate_refresh_token(self, refresh_token: str, **kwargs) -> TokenMixin:
token = oauth2_layer.get_item( token = oauth2_layer.collection.get_item(
KeyPair( KeyPair(
pk='OAUTH2#TOKEN', pk='OAUTH2#TOKEN',
sk=f'REFRESH_TOKEN#{refresh_token}', sk=f'REFRESH_TOKEN#{refresh_token}',
),
exc_cls=RefreshTokenNotFoundError,
) )
)
logger.info('Refresh token retrieved', token=token)
return OAuth2Token( return OAuth2Token(
expires_in=int(token['expires_in']), expires_in=int(token['expires_in']),
@@ -164,7 +177,10 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
return refresh_token.get_user() return refresh_token.get_user()
def revoke_old_credential(self, refresh_token: TokenMixin) -> None: def revoke_old_credential(self, refresh_token: TokenMixin) -> None:
if token := getattr(refresh_token, 'refresh_token', None): logger.info('Revoking old refresh token', refresh_token=refresh_token)
token = getattr(refresh_token, 'refresh_token', None)
if token:
oauth2_layer.delete_item( oauth2_layer.delete_item(
KeyPair(pk='OAUTH2#TOKEN', sk=f'REFRESH_TOKEN#{token}') KeyPair(pk='OAUTH2#TOKEN', sk=f'REFRESH_TOKEN#{token}')
) )

View File

@@ -3,6 +3,6 @@ from aws_lambda_powertools.event_handler.api_gateway import Router
router = Router() router = Router()
@router.get('/jwks.json') @router.get('/.well-known/jwks.json')
def jwks(): def jwks():
return {} return {}

View File

@@ -5,4 +5,4 @@ router = Router()
@router.get('/userinfo') @router.get('/userinfo')
def userinfo(): def userinfo():
return {} return {'name': 'test'}

View File

@@ -26,7 +26,7 @@ Globals:
OAUTH2_TABLE: !Ref OAuth2Table OAUTH2_TABLE: !Ref OAuth2Table
ISSUER: https://id.saladeaula.digital ISSUER: https://id.saladeaula.digital
JWT_SECRET: 7DUTFB1iLeSpiXvmxbOZim1yPVmQbmBpAzgscob0RDzrL2wVwRi1ti2ZSry7jJAf JWT_SECRET: 7DUTFB1iLeSpiXvmxbOZim1yPVmQbmBpAzgscob0RDzrL2wVwRi1ti2ZSry7jJAf
OAUTH2_SCOPES_SUPPORTED: openid profile email OAUTH2_SCOPES_SUPPORTED: openid profile email offline_access
Resources: Resources:
HttpLog: HttpLog:
@@ -79,7 +79,7 @@ Resources:
Jwks: Jwks:
Type: HttpApi Type: HttpApi
Properties: Properties:
Path: /jwks.json Path: /.well-known/jwks.json
Method: GET Method: GET
ApiId: !Ref HttpApi ApiId: !Ref HttpApi
Token: Token:

View File

@@ -20,7 +20,7 @@ def pytest_configure():
os.environ['JWT_SECRET'] = 'secret' os.environ['JWT_SECRET'] = 'secret'
os.environ['DYNAMODB_PARTITION_KEY'] = PK os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['OAUTH2_SCOPES_SUPPORTED'] = 'openid profile email' os.environ['OAUTH2_SCOPES_SUPPORTED'] = 'openid profile email offline_access'
# os.environ['POWERTOOLS_LOGGER_LOG_EVENT'] = 'true' # os.environ['POWERTOOLS_LOGGER_LOG_EVENT'] = 'true'

View File

@@ -29,8 +29,9 @@ def test_authorize(
'response_type': 'code', 'response_type': 'code',
'client_id': client_id, 'client_id': client_id,
'redirect_uri': 'https://localhost/callback', 'redirect_uri': 'https://localhost/callback',
'scope': 'openid', 'scope': 'openid offline_access',
'nonce': '123', 'nonce': '123',
'state': '456',
}, },
cookies=[ cookies=[
f'id_token={id_token}; HttpOnly; Secure', f'id_token={id_token}; HttpOnly; Secure',

View File

@@ -38,7 +38,7 @@ def test_token(
auth_token = json.loads(r['body']) auth_token = json.loads(r['body'])
assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.OK
assert auth_token['expires_in'] == 900 assert auth_token['expires_in'] == 600
r = dynamodb_persistence_layer.query( r = dynamodb_persistence_layer.query(
key_cond_expr='#pk = :pk', key_cond_expr='#pk = :pk',

View File

@@ -1,5 +1,5 @@
// OAuth2 // OAuth2
{"id": "OAUTH2", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email", "token_endpoint_auth_method": "none"} {"id": "OAUTH2", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access", "token_endpoint_auth_method": "none"}
{"id": "OAUTH2#CODE", "sk": "CODE#kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9", "client_id": "d72d4005-1fa7-4430-9754-80d5e2487bb6", "redirect_uri": "https://localhost/callback", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419", "nonce": null, "scope": "openid profile email", "response_type": "code", "code_challenge": "ejYEIGKQUgMnNh4eV0sftb0hXdLwkvKm6OHXRYvC--I", "code_challenge_method": "S256", "created_at": "2025-08-07T12:38:26.550431-03:00"} {"id": "OAUTH2#CODE", "sk": "CODE#kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9", "client_id": "d72d4005-1fa7-4430-9754-80d5e2487bb6", "redirect_uri": "https://localhost/callback", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419", "nonce": null, "scope": "openid profile email", "response_type": "code", "code_challenge": "ejYEIGKQUgMnNh4eV0sftb0hXdLwkvKm6OHXRYvC--I", "code_challenge_method": "S256", "created_at": "2025-08-07T12:38:26.550431-03:00"}
// Post-migration: uncomment the following line // Post-migration: uncomment the following line