This commit is contained in:
2025-11-26 15:14:29 -03:00
parent 0d3d9ac7d3
commit d3ccfb4775
32 changed files with 496 additions and 474 deletions

View File

@@ -7,24 +7,26 @@ from aws_lambda_powertools.event_handler.api_gateway import (
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext
from routes.authentication import router as authentication
from routes.authorize import router as authorize
from routes.jwks import router as jwks
from routes.openid_configuration import router as openid_configuration
from routes.register import router as register
from routes.revoke import router as revoke
from routes.session import router as session
from routes.token import router as token
from routes.userinfo import router as userinfo
logger = Logger(__name__)
tracer = Tracer()
app = APIGatewayHttpResolver(enable_validation=True)
app.include_router(session)
app.include_router(authentication)
app.include_router(authorize)
app.include_router(jwks)
app.include_router(openid_configuration)
app.include_router(register)
app.include_router(revoke)
app.include_router(token)
app.include_router(userinfo)
app.include_router(revoke)
app.include_router(openid_configuration)
@app.get('/health')

View File

@@ -198,23 +198,6 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
)
class TokenExchangeGrant(grants.BaseGrant):
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic',
'client_secret_post',
]
@hooked
def validate_token_request(self):
raise NotImplementedError()
@hooked
def create_token_response(self):
raise NotImplementedError()
class RefreshTokenGrant(grants.RefreshTokenGrant):
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic',
@@ -280,6 +263,23 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
)
class TokenExchangeGrant(grants.BaseGrant):
GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic',
'client_secret_post',
]
@hooked
def validate_token_request(self):
raise NotImplementedError()
@hooked
def create_token_response(self):
raise NotImplementedError()
class RevocationEndpoint(rfc7009.RevocationEndpoint):
def query_token( # type: ignore
self,

View File

@@ -24,7 +24,7 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
def authorize():
current_event = router.current_event
cookies = parse_cookies(current_event.get('cookies', []))
session = cookies.get('__session')
session = cookies.get('SID')
if not session:
raise BadRequestError('Missing session')

View File

@@ -1,158 +0,0 @@
from http import HTTPStatus
from typing import Annotated
from uuid import uuid4
import boto3
from aws_lambda_powertools.event_handler import (
Response,
)
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import ForbiddenError, NotFoundError
from aws_lambda_powertools.event_handler.openapi.params import Body
from aws_lambda_powertools.shared.cookies import Cookie
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from passlib.hash import pbkdf2_sha256
from boto3clients import dynamodb_client
from config import (
OAUTH2_TABLE,
SESSION_EXPIRES_IN,
)
router = Router()
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
idp = boto3.client('cognito-idp')
@router.post('/session')
def session(
username: Annotated[str, Body()],
password: Annotated[str, Body()],
):
user_id, password_hash = _get_user(username)
if not password_hash:
_get_idp_user(user_id, username, password)
else:
if not pbkdf2_sha256.verify(password, password_hash):
raise ForbiddenError('Invalid credentials')
return Response(
status_code=HTTPStatus.OK,
cookies=[
Cookie(
name='__session',
value=new_session(user_id),
http_only=True,
secure=True,
same_site=None,
max_age=SESSION_EXPIRES_IN,
)
],
)
def _get_user(username: str) -> tuple[str, str | None]:
sk = SortKey(username, path_spec='user_id')
user = dyn.collection.get_items(
KeyPair(pk='email', sk=sk, rename_key=sk.path_spec)
+ KeyPair(pk='cpf', sk=sk, rename_key=sk.path_spec),
)
if not user:
raise UserNotFoundError()
password = dyn.collection.get_item(
KeyPair(
pk=user['user_id'],
sk=SortKey(
sk='PASSWORD',
path_spec='hash',
rename_key='password',
),
),
raise_on_error=False,
default=None,
# Uncomment the following line when removing support for Cognito
# exc_cls=UserNotFoundError,
)
return user['user_id'], password
def _get_idp_user(
user_id: str,
username: str,
password: str,
) -> bool:
import base64
import hashlib
import hmac
# That should be removed when completing the migration
# to our own OAuth2 implementation.
client_id = '3ijacqc7r2jc9l4oli2b41f7te'
client_secret = 'amktf9l40g1mlqdo9fjlcfvpn2cp3mvh4pt97hu55sfelccos58'
dig = hmac.new(
client_secret.encode('utf-8'),
msg=(username + client_id).encode('utf-8'),
digestmod=hashlib.sha256,
).digest()
try:
idp.initiate_auth(
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': username,
'PASSWORD': password,
'SECRET_HASH': base64.b64encode(dig).decode(),
},
ClientId=client_id,
)
dyn.put_item(
item={
'id': user_id,
'sk': 'PASSWORD',
'hash': pbkdf2_sha256.hash(password),
'created_at': now(),
}
)
except Exception:
raise ForbiddenError('Invalid credentials')
return True
def new_session(sub: str) -> str:
sid = str(uuid4())
now_ = now()
exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN)
with dyn.transact_writer() as transact:
transact.put(
item={
'id': 'SESSION',
'sk': sid,
'user_id': sub,
'ttl': exp,
'created_at': now_,
}
)
transact.put(
item={
'id': sub,
'sk': f'SESSION#{sid}',
'ttl': exp,
'created_at': now_,
}
)
return f'{sid}:{sub}'
class UserNotFoundError(NotFoundError):
def __init__(self, *_):
super().__init__('User not found')