This commit is contained in:
2025-12-03 01:24:52 -03:00
parent 3a49b13cb9
commit 38c49ff370
21 changed files with 133 additions and 73 deletions

View File

@@ -13,5 +13,11 @@ OAUTH2_SCOPES_SUPPORTED: list[str] = [
'apps:studio',
'apps:insights',
]
OAUTH2_DEFAULT_SCOPES = {
'email',
'offline_access',
'openid',
'profile',
}
SESSION_EXPIRES_IN = 86_400 * 30 # 30 days

View File

@@ -21,7 +21,7 @@ from layercake.dynamodb import (
from layercake.funcs import omit, pick
from boto3clients import dynamodb_client
from config import ISSUER, OAUTH2_SCOPES_SUPPORTED, OAUTH2_TABLE
from config import ISSUER, OAUTH2_DEFAULT_SCOPES, OAUTH2_SCOPES_SUPPORTED, OAUTH2_TABLE
from integrations.apigateway_oauth2.authorization_server import (
AuthorizationServer,
)
@@ -191,10 +191,11 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
rename_key='scope',
),
)
scope = set(user.get('scope', [])) | OAUTH2_DEFAULT_SCOPES
return User(
**pick(('id', 'name', 'email', 'email_verified'), user),
scope=' '.join(user['scope']) if 'scope' in user else None,
scope=' '.join(scope),
)

View File

@@ -5,13 +5,14 @@ from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError,
ForbiddenError,
NotFoundError,
ServiceError,
)
from joserfc.errors import JoseError
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from boto3clients import dynamodb_client
from config import OAUTH2_TABLE
from config import OAUTH2_DEFAULT_SCOPES, OAUTH2_TABLE
from oauth2 import server
from util import parse_cookies
@@ -20,6 +21,9 @@ logger = Logger(__name__)
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
class SessionNotFoundError(NotFoundError): ...
@router.get('/authorize')
def authorize():
current_event = router.current_event
@@ -27,20 +31,20 @@ def authorize():
session = cookies.get('SID')
if not session:
raise BadRequestError('Missing session')
raise BadRequestError('Session cookie (SID) is required')
try:
sid, sub = session.split(':')
session_id, user_id = session.split(':')
# Raise if session is not found
dyn.collection.get_item(
KeyPair('SESSION', sid),
exc_cls=InvalidSession,
KeyPair('SESSION', session_id),
exc_cls=SessionNotFoundError,
)
grant = server.get_consent_grant(
request=router.current_event,
end_user=sub,
end_user=user_id,
)
user_scopes = _user_scopes(sub)
user_scopes = _user_scopes(user_id)
client_scopes = set(scope_to_list(grant.client.scope))
# Deny authorization if user lacks scopes requested by client
@@ -49,7 +53,7 @@ def authorize():
response = server.create_authorization_response(
request=router.current_event,
grant_user=sub,
grant_user=user_id,
grant=grant,
)
except JoseError as err:
@@ -65,18 +69,16 @@ def authorize():
return response
def _user_scopes(sub: str) -> set:
return set(
def _user_scopes(user_id: str) -> set:
return OAUTH2_DEFAULT_SCOPES | set(
scope_to_list(
dyn.collection.get_item(
KeyPair(
pk=sub,
pk=user_id,
sk=SortKey(sk='SCOPE', path_spec='scope'),
),
exc_cls=BadRequestError,
raise_on_error=False,
default='',
)
)
)
class InvalidSession(BadRequestError): ...

View File

@@ -88,7 +88,9 @@ def _create_user(*, user: User, password: str):
item={
'sk': '0',
'email_verified': False,
'created_at': now_,
'createdDate': now_,
# Post-migration (users): uncomment the folloing line
# 'created_at': now_,
}
| asdict(user),
)
@@ -116,6 +118,7 @@ def _create_user(*, user: User, password: str):
# Post-migration (users): rename `cpf` to `CPF`
'id': 'cpf',
'sk': user.cpf,
'user_id': user.id,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
@@ -126,6 +129,7 @@ def _create_user(*, user: User, password: str):
# Post-migration (users): rename `email` to `EMAIL`
'id': 'email',
'sk': user.email,
'user_id': user.id,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',

View File

@@ -68,9 +68,9 @@ def test_forbidden(
method=HTTPMethod.GET,
query_string_parameters={
'response_type': 'code',
'client_id': '6ebe1709-0831-455c-84c0-d4c753bf33c6',
'client_id': '5e90c38f-f058-4e16-91fa-952554a290c5',
'redirect_uri': 'https://localhost/callback',
'scope': 'openid email offline_access',
'scope': 'apps:admin',
'nonce': '123',
'state': '456',
},
@@ -110,4 +110,4 @@ def test_invalid_session(
lambda_context,
)
assert r['statusCode'] == HTTPStatus.BAD_REQUEST
assert r['statusCode'] == HTTPStatus.NOT_FOUND

View File

@@ -3,6 +3,7 @@
{"id": "OAUTH2", "sk": "CLIENT_ID#8c5e92b0-9ed4-451e-8935-66084d8544b1", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 1", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access apps:admin", "token_endpoint_auth_method": "none"}
{"id": "OAUTH2", "sk": "CLIENT_ID#6ebe1709-0831-455c-84c0-d4c753bf33c6", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 2", "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", "sk": "CLIENT_ID#1db63660-063d-4280-b2ea-388aca4a9459", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 3", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "openid profile email offline_access apps:admin", "token_endpoint_auth_method": "client_secret_basic"}
{"id": "OAUTH2", "sk": "CLIENT_ID#5e90c38f-f058-4e16-91fa-952554a290c5", "client_secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest 2", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "scope": "apps:studio", "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 apps:admins", "response_type": "code", "code_challenge": "ejYEIGKQUgMnNh4eV0sftb0hXdLwkvKm6OHXRYvC--I", "code_challenge_method": "S256", "created_at": "2025-08-07T12:38:26.550431-03:00"}
{"id": "OAUTH2#TOKEN", "sk": "REFRESH_TOKEN#CyF3Ik3b9hMIo3REVv27gZAHd7dvwZq6QrkhWr7qHEen4UVy", "client_id": "d72d4005-1fa7-4430-9754-80d5e2487bb6", "token_type": "Bearer", "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IlRjT0VuV3JGSUFEYlZJNjJlY1pzU28ydEI1eW5mbkZZNTZ0Uy05b0stNW8ifQ.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwiZXhwIjoxNzU5NTg2NzgzLCJjbGllbnRfaWQiOiJkNzJkNDAwNS0xZmE3LTQ0MzAtOTc1NC04MGQ1ZTI0ODdiYjYiLCJpYXQiOjE3NTg5ODE5ODMsImp0aSI6Ik9uVzRIZm1FdFl2a21CbE4iLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIHJlYWQ6dXNlcnMiLCJzdWIiOiIzNTdkYjFjNS03NDQyLTQwNzUtOThhMy1mYmU1YzkzOGE0MTkiLCJhdWQiOiJkNzJkNDAwNS0xZmE3LTQ0MzAtOTc1NC04MGQ1ZTI0ODdiYjYifQ.i0NVgvPuf5jvl8JcYNsVCzjVUTDLihgQO4LmLeNijx9Ed3p_EgtVtcHFWFvEebe_LwTuDDtIJveH22Piyp4zresNSc_YNumnuvoY1aNd0ic2RIEtXaklRroq0xHwL_IVT-Dt6P9xL5Hyygx47Pvmci4U3wWK32a6Sb1Mm7ZZgXA00xWI1bJ_zwxFLvDkHDp9nrAa_vEWN6zRBcWc7JYNsgiaPMC0DoL8it0k48_g44zfsjGAZLcWFMoPlYt3wIcQQDeCKMsSJI0VPnqKK0pq4OOVs-pjkMyAU5aEMPvVOwdAL3VZY16RXt3eTzsmMH1XoRdCMP6UAx4ZS10RLGUPeA", "scope": "openid profile email read:users", "user": {"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "name": "S\u00e9rgio R Siqueira", "email": "sergio@somosbeta.com.br", "email_verified": false}, "expires_in": 180, "issued_at": 1758981984, "ttl": 1759586784}