update session

This commit is contained in:
2025-10-10 12:52:41 -03:00
parent 2de2d4dc0e
commit c9438d49fb
15 changed files with 116 additions and 112 deletions

View File

@@ -106,7 +106,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
pk=enrollment_id,
sk='0',
),
update_expr='SET issued_cert = :issued_cert, uploaded_at = :now',
update_expr='SET issued_cert = :issued_cert, updated_at = :now',
expr_attr_values={
':now': now_,
':issued_cert': {

View File

@@ -3,8 +3,20 @@ import os
ISSUER: str = os.getenv('ISSUER') # type: ignore
OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore
OAUTH2_SCOPES_SUPPORTED: str = os.getenv('OAUTH2_SCOPES_SUPPORTED', '')
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days
OAUTH2_SCOPES_SUPPORTED: list[str] = [
'openid',
'profile',
'email',
'offline_access',
'read:users',
'write:users',
'read:enrollments',
'write:enrollments',
'read:orders',
'write:orders',
'read:courses',
'write:courses',
]
SESSION_SECRET: str = os.environ.get('SESSION_SECRET') # type: ignore
SESSION_EXPIRES_IN = 86400 * 30 # 30 days

View File

@@ -1,4 +1,3 @@
import os
from dataclasses import asdict
import authlib.oauth2 as oauth2
@@ -17,24 +16,19 @@ from config import OAUTH2_REFRESH_TOKEN_EXPIRES_IN
from .client import OAuth2Client
from .requests import APIGatewayJsonRequest, APIGatewayOAuth2Request
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
logger = Logger(__name__)
class AuthorizationServer(oauth2.AuthorizationServer):
def __init__(
self,
scopes_supported: list[str],
*,
persistence_layer: DynamoDBPersistenceLayer,
) -> None:
super().__init__(scopes_supported=scopes_supported)
self._persistence_layer = persistence_layer
if OAUTH2_SCOPES_SUPPORTED:
super().__init__(
scopes_supported=set(OAUTH2_SCOPES_SUPPORTED.split()),
)
def save_token(
self,
token: dict,

View File

@@ -49,7 +49,7 @@ class APIGatewayOAuth2Request(requests.OAuth2Request):
super().__init__(
request.request_context.http.method,
uri,
request.headers,
headers=request.headers,
)
self._request = request
self.payload = APIGatewayOAuth2Payload(request)

View File

@@ -3,6 +3,7 @@ from authlib.common.urls import add_params_to_uri
from authlib.jose import JsonWebKey
from authlib.oauth2 import OAuth2Request, rfc7009, rfc9207
from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants
from authlib.oauth2.rfc6749.hooks import hooked
from authlib.oauth2.rfc6750 import BearerTokenGenerator
from authlib.oauth2.rfc7636 import CodeChallenge
from authlib.oauth2.rfc9068 import JWTBearerTokenGenerator as JWTBearerTokenGenerator_
@@ -21,7 +22,7 @@ from layercake.dynamodb import (
from layercake.funcs import omit, pick
from boto3clients import dynamodb_client
from config import ISSUER, OAUTH2_TABLE
from config import ISSUER, OAUTH2_SCOPES_SUPPORTED, OAUTH2_TABLE
from integrations.apigateway_oauth2.authorization_server import (
AuthorizationServer,
)
@@ -64,6 +65,8 @@ class OpenIDCode(OpenIDCode_):
}
def generate_user_info(self, user: User, scope: str) -> UserInfo:
print(scope)
print('--' * 100)
return UserInfo(
sub=user.id,
name=user.name,
@@ -173,6 +176,20 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
return User(**pick(('id', 'name', 'email', 'email_verified'), user))
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 RefreshTokenNotFoundError(NotFoundError):
def __init__(self, *_):
super().__init__('Refresh token not found')
@@ -337,7 +354,10 @@ class JWTBearerTokenGenerator(JWTBearerTokenGenerator_):
}
server = AuthorizationServer(persistence_layer=dyn)
server = AuthorizationServer(
persistence_layer=dyn,
scopes_supported=OAUTH2_SCOPES_SUPPORTED,
)
server.register_grant(
AuthorizationCodeGrant,
[
@@ -353,6 +373,7 @@ server.register_token_generator(
expires_generator=expires_in,
),
)
server.register_grant(TokenExchangeGrant)
server.register_grant(RefreshTokenGrant)
server.register_endpoint(RevocationEndpoint)
server.register_extension(IssuerParameter())

View File

@@ -1,6 +1,5 @@
from http.cookies import SimpleCookie
import jwt
from authlib.oauth2.rfc6749 import errors
from authlib.oauth2.rfc6749.util import scope_to_list
from aws_lambda_powertools import Logger
@@ -9,12 +8,12 @@ from aws_lambda_powertools.event_handler.exceptions import (
BadRequestError,
ForbiddenError,
ServiceError,
UnauthorizedError,
)
from joserfc.errors import JoseError
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from boto3clients import dynamodb_client
from config import ISSUER, OAUTH2_TABLE, SESSION_SECRET
from config import OAUTH2_TABLE
from oauth2 import server
router = Router()
@@ -26,19 +25,24 @@ dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
def authorize():
current_event = router.current_event
cookies = _parse_cookies(current_event.get('cookies', []))
session_id = cookies.get('session_id')
session = cookies.get('__session')
if not session_id:
raise BadRequestError('Missing session_id')
if not session:
raise BadRequestError('Missing session')
try:
sub, session_scope = verify_session(session_id)
sid, sub = session.split(':')
# Raise if session is not found
dyn.collection.get_item(
KeyPair('SESSION', sid),
exc_cls=InvalidSession,
)
grant = server.get_consent_grant(
request=router.current_event,
end_user=sub,
)
user_scopes = _user_scopes(sub)
client_scopes = set(scope_to_list(grant.client.scope))
user_scopes = set(scope_to_list(session_scope)) if session_scope else set()
# Deny authorization if user lacks scopes requested by client
if not client_scopes.issubset(user_scopes):
@@ -49,7 +53,7 @@ def authorize():
grant_user=sub,
grant=grant,
)
except jwt.exceptions.InvalidTokenError as err:
except JoseError as err:
logger.exception(err)
raise BadRequestError(str(err))
except errors.OAuth2Error as err:
@@ -60,38 +64,18 @@ def authorize():
)
def verify_session(session_id: str) -> tuple[str, str | None]:
payload = jwt.decode(
session_id,
SESSION_SECRET,
algorithms=['HS256'],
issuer=ISSUER,
options={
'require': ['exp', 'sub', 'iss', 'sid'],
},
)
user = dyn.collection.get_items(
def _user_scopes(sub: str) -> set:
return set(
scope_to_list(
dyn.collection.get_item(
KeyPair(
pk='SESSION',
sk=payload['sid'],
rename_key='session',
)
+ KeyPair(
pk=payload['sub'],
sk=SortKey(
sk='SCOPE',
path_spec='scope',
rename_key='scope',
pk=sub,
sk=SortKey(sk='SCOPE', path_spec='scope'),
),
),
flatten_top=False,
exc_cls=BadRequestError,
)
)
)
if 'session' not in user:
raise SessionRevokedError('Session revoked')
return payload['sub'], user.get('scope')
def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
@@ -108,4 +92,4 @@ def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
return parsed_cookies
class SessionRevokedError(UnauthorizedError): ...
class InvalidSession(BadRequestError): ...

View File

@@ -3,7 +3,6 @@ from typing import Annotated
from uuid import uuid4
import boto3
import jwt
from aws_lambda_powertools.event_handler import (
Response,
)
@@ -17,10 +16,8 @@ from passlib.hash import pbkdf2_sha256
from boto3clients import dynamodb_client
from config import (
ISSUER,
OAUTH2_TABLE,
SESSION_EXPIRES_IN,
SESSION_SECRET,
)
router = Router()
@@ -45,7 +42,7 @@ def session(
status_code=HTTPStatus.OK,
cookies=[
Cookie(
name='session_id',
name='__session',
value=new_session(user_id),
http_only=True,
secure=True,
@@ -127,26 +124,15 @@ def _get_idp_user(
def new_session(sub: str) -> str:
session_id = str(uuid4())
sid = str(uuid4())
now_ = now()
exp = ttl(start_dt=now_, seconds=SESSION_EXPIRES_IN)
token = jwt.encode(
{
'sid': session_id,
'sub': sub,
'iss': ISSUER,
'iat': int(now_.timestamp()),
'exp': exp,
},
SESSION_SECRET,
algorithm='HS256',
)
with dyn.transact_writer() as transact:
transact.put(
item={
'id': 'SESSION',
'sk': session_id,
'sk': sid,
'user_id': sub,
'ttl': exp,
'created_at': now_,
@@ -155,13 +141,13 @@ def new_session(sub: str) -> str:
transact.put(
item={
'id': sub,
'sk': f'SESSION#{session_id}',
'sk': f'SESSION#{sid}',
'ttl': exp,
'created_at': now_,
}
)
return token
return f'{sid}:{sub}'
class UserNotFoundError(NotFoundError):

View File

@@ -11,7 +11,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
issuerUrl.search = url.search
redirect.search = url.search
if (!cookies.session_id) {
if (!cookies?.__session) {
return new Response(null, {
status: httpStatus.FOUND,
headers: {

View File

@@ -14,7 +14,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:98
Environment:
Variables:
TZ: America/Sao_Paulo
@@ -25,8 +25,6 @@ Globals:
DYNAMODB_SORT_KEY: sk
OAUTH2_TABLE: !Ref OAuth2Table
ISSUER: https://id.saladeaula.digital
SESSION_SECRET: 7DUTFB1iLeSpiXvmxbOZim1yPVmQbmBpAzgscob0RDzrL2wVwRi1ti2ZSry7jJAf
OAUTH2_SCOPES_SUPPORTED: openid profile email offline_access read:users read:enrollments read:orders read:courses write:courses
Resources:
HttpLog:

View File

@@ -21,9 +21,6 @@ def pytest_configure():
os.environ['DYNAMODB_PARTITION_KEY'] = PK
os.environ['DYNAMODB_SORT_KEY'] = SK
os.environ['ISSUER'] = 'http://localhost'
os.environ['OAUTH2_SCOPES_SUPPORTED'] = (
'openid profile email offline_access read:users'
)
@dataclass

View File

@@ -16,7 +16,7 @@ def test_authorize(
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
session_id = new_session(USER_ID)
session = new_session(USER_ID)
r = app.lambda_handler(
http_api_proxy(
@@ -31,7 +31,7 @@ def test_authorize(
'state': '456',
},
cookies=[
f'session_id={session_id}; HttpOnly; Secure',
f'__session={session}; HttpOnly; Secure',
],
),
lambda_context,
@@ -60,7 +60,7 @@ def test_forbidden(
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
session_id = new_session('fd5914ec-fd37-458b-b6b9-8aeab38b666b')
session = new_session('fd5914ec-fd37-458b-b6b9-8aeab38b666b')
r = app.lambda_handler(
http_api_proxy(
@@ -75,7 +75,7 @@ def test_forbidden(
'state': '456',
},
cookies=[
f'session_id={session_id}; HttpOnly; Secure',
f'__session={session}; HttpOnly; Secure',
],
),
lambda_context,
@@ -84,15 +84,13 @@ def test_forbidden(
assert r['statusCode'] == HTTPStatus.FORBIDDEN
def test_authorize_revoked(
def test_invalid_session(
app,
seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
invalid_session_id = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWQiOiIwNTgzNTBhYi02NGU1LTQ0MzEtYmQyNy01MGVhOWIxNmQxZGYiLCJzdWIiOiIzNTdkYjFjNS03NDQyLTQwNzUtOThhMy1mYmU1YzkzOGE0MTkiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0IiwiaWF0IjoxNzU1Mzk3Nzk5LCJleHAiOjE3NTUzOTg2OTl9.dDbiHYReVERbkNH2df4sXK2VIwT7G1KjNC5UrBuN6IQ'
r = app.lambda_handler(
http_api_proxy(
raw_path='/authorize',
@@ -106,7 +104,7 @@ def test_authorize_revoked(
'state': '456',
},
cookies=[
f'session_id={invalid_session_id}; HttpOnly; Secure',
'__session=10:10; HttpOnly; Secure',
],
),
lambda_context,

View File

@@ -16,9 +16,9 @@
// User data
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SCOPE", "scope": "openid profile email offline_access read:users read:courses"}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SCOPE", "scope": ["openid", "profile", "email", "offline_access", "read:users", "read:courses", "impersonate:users"]}
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SESSION#36af142e-9f6d-49d3-bfe9-6a6bd6ab2712", "created_at": "2025-09-17T13:44:34.544491-03:00", "ttl": 1760719474}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "0", "name": "Johnny Cash", "email": "johnny@johnnycash.com"}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "PASSWORD", "hash": "$pbkdf2-sha256$29000$IuTcm7M2BiAEgPB.b.3dGw$d8xVCbx8zxg7MeQBrOvCOgniiilsIHEMHzoH/OXftLQ"}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "SCOPE", "scope": "openid"}
{"id": "fd5914ec-fd37-458b-b6b9-8aeab38b666b", "sk": "SCOPE", "scope": ["openid"]}

View File

@@ -35,14 +35,14 @@ wheels = [
[[package]]
name = "authlib"
version = "1.6.1"
version = "1.6.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" },
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
]
[[package]]
@@ -469,6 +469,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" },
]
[[package]]
name = "joserfc"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/26/a0/4b8dfecc8ec3c15aa1f2ff7d5b947344378b5b595ce37c8a8fe6e25c1400/joserfc-1.4.0.tar.gz", hash = "sha256:e8c2f327bf10a937d284d57e9f8aec385381e5e5850469b50a7dade1aba59759", size = 196339, upload-time = "2025-10-09T07:47:00.835Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/05/342459b7629c6fcb5f99a646886ee2904491955b8cce6b26b0b9a498f67c/joserfc-1.4.0-py3-none-any.whl", hash = "sha256:46917e6b53f1ec0c7e20d34d6f3e6c27da0fa43d0d4ebfb89aada7c86582933a", size = 66390, upload-time = "2025-10-09T07:46:59.591Z" },
]
[[package]]
name = "jsonlines"
version = "4.0.0"
@@ -495,7 +507,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.9.14"
version = "0.10.1"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },
@@ -504,6 +516,7 @@ dependencies = [
{ name = "dictdiffer" },
{ name = "ftfy" },
{ name = "glom" },
{ name = "joserfc" },
{ name = "meilisearch" },
{ name = "orjson" },
{ name = "passlib" },
@@ -511,7 +524,7 @@ dependencies = [
{ name = "pycpfcnpj" },
{ name = "pydantic", extra = ["email"] },
{ name = "pydantic-extra-types" },
{ name = "pyjwt" },
{ name = "python-multipart" },
{ name = "pytz" },
{ name = "requests" },
{ name = "smart-open", extra = ["s3"] },
@@ -522,11 +535,12 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" },
{ name = "authlib", specifier = ">=1.6.1" },
{ name = "authlib", specifier = ">=1.6.5" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" },
{ name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" },
{ name = "glom", specifier = ">=24.11.0" },
{ name = "joserfc", specifier = ">=1.2.2" },
{ name = "meilisearch", specifier = ">=0.34.0" },
{ name = "orjson", specifier = ">=3.10.15" },
{ name = "passlib", specifier = ">=1.7.4" },
@@ -534,7 +548,7 @@ requires-dist = [
{ name = "pycpfcnpj", specifier = ">=1.8" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
{ name = "pyjwt", specifier = ">=2.10.1" },
{ name = "python-multipart", specifier = ">=0.0.20" },
{ name = "pytz", specifier = ">=2025.1" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" },
@@ -825,15 +839,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyjwt"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
[[package]]
name = "pytest"
version = "8.4.1"
@@ -885,6 +890,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
[[package]]
name = "pytz"
version = "2025.2"

View File

@@ -1,6 +1,6 @@
[project]
name = "layercake"
version = "0.10.0"
version = "0.10.1"
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
readme = "README.md"
authors = [
@@ -23,11 +23,11 @@ dependencies = [
"sqlite-utils>=3.38",
"dictdiffer>=0.9.0",
"unidecode>=1.4.0",
"authlib>=1.6.1",
"passlib>=1.7.4",
"psycopg[binary]>=3.2.9",
"joserfc>=1.2.2",
"python-multipart>=0.0.20",
"authlib>=1.6.5",
]
[dependency-groups]

8
layercake/uv.lock generated
View File

@@ -44,14 +44,14 @@ wheels = [
[[package]]
name = "authlib"
version = "1.6.1"
version = "1.6.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/a1/d8d1c6f8bc922c0b87ae0d933a8ed57be1bef6970894ed79c2852a153cd3/authlib-1.6.1.tar.gz", hash = "sha256:4dffdbb1460ba6ec8c17981a4c67af7d8af131231b5a36a88a1e8c80c111cdfd", size = 159988, upload-time = "2025-07-20T07:38:42.834Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/58/cc6a08053f822f98f334d38a27687b69c6655fb05cd74a7a5e70a2aeed95/authlib-1.6.1-py2.py3-none-any.whl", hash = "sha256:e9d2031c34c6309373ab845afc24168fe9e93dc52d252631f52642f21f5ed06e", size = 239299, upload-time = "2025-07-20T07:38:39.259Z" },
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
]
[[package]]
@@ -714,7 +714,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "arnparse", specifier = ">=0.0.2" },
{ name = "authlib", specifier = ">=1.6.1" },
{ name = "authlib", specifier = ">=1.6.5" },
{ name = "aws-lambda-powertools", extras = ["all"], specifier = ">=3.18.0" },
{ name = "dictdiffer", specifier = ">=0.9.0" },
{ name = "ftfy", specifier = ">=6.3.1" },