add ft to migrate from cognito
This commit is contained in:
@@ -34,7 +34,7 @@ tracer = Tracer()
|
|||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
cors = CORSConfig(
|
cors = CORSConfig(
|
||||||
allow_origin='*',
|
allow_origin='*',
|
||||||
allow_headers=['Content-Type', 'X-Requested-With', 'Authorization', 'X-Tenant'],
|
allow_headers=['Content-Type', 'X-Requested-With', 'Authorization'],
|
||||||
max_age=600,
|
max_age=600,
|
||||||
allow_credentials=False,
|
allow_credentials=False,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore
|
||||||
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
|
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
|
||||||
|
|||||||
@@ -80,6 +80,6 @@ Outputs:
|
|||||||
Value:
|
Value:
|
||||||
Fn::Sub: "https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}"
|
Fn::Sub: "https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}"
|
||||||
HttpApiId:
|
HttpApiId:
|
||||||
Description: Api id of HttpApi
|
Description: Api ID of HttpApi
|
||||||
Value:
|
Value:
|
||||||
Ref: HttpApi
|
Ref: HttpApi
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class Cert(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class Course(BaseModel):
|
class Course(BaseModel):
|
||||||
id: UUID4 = Field(default_factory=uuid4)
|
id: UUID4 | str = Field(default_factory=uuid4)
|
||||||
name: str
|
name: str
|
||||||
cert: Cert | None = None
|
cert: Cert | None = None
|
||||||
access_period: int = 90 # 3 months
|
access_period: int = 90 # 3 months
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ def get_enrollment(id: str):
|
|||||||
+ SortKey('0')
|
+ SortKey('0')
|
||||||
# + SortKey('STARTED', rename_key='started_at', path_spec='started_at')
|
# + SortKey('STARTED', rename_key='started_at', path_spec='started_at')
|
||||||
# + SortKey('COMPLETED', rename_key='completed_at', path_spec='completed_at')
|
# + SortKey('COMPLETED', rename_key='completed_at', path_spec='completed_at')
|
||||||
+ SortKey('FAILED', rename_key='failed_at', path_spec='failed_at')
|
# + SortKey('FAILED', rename_key='failed_at', path_spec='failed_at')
|
||||||
+ SortKey('CANCELED', rename_key='canceled')
|
+ SortKey('CANCELED', rename_key='canceled')
|
||||||
+ SortKey('ARCHIVED', rename_key='archived_at', path_spec='archived_at')
|
+ SortKey('ARCHIVED', rename_key='archived_at', path_spec='archived_at')
|
||||||
+ SortKey('CANCEL_POLICY', rename_key='cancel_policy')
|
+ SortKey('CANCEL_POLICY', rename_key='cancel_policy')
|
||||||
|
|||||||
@@ -6,7 +6,5 @@ OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore
|
|||||||
OAUTH2_SCOPES_SUPPORTED: str = os.getenv('OAUTH2_SCOPES_SUPPORTED', '')
|
OAUTH2_SCOPES_SUPPORTED: str = os.getenv('OAUTH2_SCOPES_SUPPORTED', '')
|
||||||
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days
|
OAUTH2_REFRESH_TOKEN_EXPIRES_IN = 86_400 * 7 # 7 days
|
||||||
|
|
||||||
JWT_SECRET: str = os.environ.get('JWT_SECRET') # type: ignore
|
SESSION_SECRET: str = os.environ.get('SESSION_SECRET') # type: ignore
|
||||||
JWT_ALGORITHM = 'HS256'
|
|
||||||
|
|
||||||
SESSION_EXPIRES_IN = 86400 * 30 # 30 days
|
SESSION_EXPIRES_IN = 86400 * 30 # 30 days
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from aws_lambda_powertools.event_handler.exceptions import (
|
|||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import ISSUER, JWT_ALGORITHM, JWT_SECRET, OAUTH2_TABLE
|
from config import ISSUER, OAUTH2_TABLE, SESSION_SECRET
|
||||||
from oauth2 import server
|
from oauth2 import server
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
@@ -63,12 +63,11 @@ def authorize():
|
|||||||
def verify_session(session_id: str) -> tuple[str, str | None]:
|
def verify_session(session_id: str) -> tuple[str, str | None]:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
session_id,
|
session_id,
|
||||||
JWT_SECRET,
|
SESSION_SECRET,
|
||||||
algorithms=[JWT_ALGORITHM],
|
algorithms=['HS256'],
|
||||||
issuer=ISSUER,
|
issuer=ISSUER,
|
||||||
options={
|
options={
|
||||||
'require': ['exp', 'sub', 'iss', 'sid'],
|
'require': ['exp', 'sub', 'iss', 'sid'],
|
||||||
'leeway': 60,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from http import HTTPStatus
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import boto3
|
||||||
import jwt
|
import jwt
|
||||||
from aws_lambda_powertools.event_handler import (
|
from aws_lambda_powertools.event_handler import (
|
||||||
Response,
|
Response,
|
||||||
@@ -17,14 +18,14 @@ from passlib.hash import pbkdf2_sha256
|
|||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import (
|
from config import (
|
||||||
ISSUER,
|
ISSUER,
|
||||||
JWT_ALGORITHM,
|
|
||||||
JWT_SECRET,
|
|
||||||
OAUTH2_TABLE,
|
OAUTH2_TABLE,
|
||||||
SESSION_EXPIRES_IN,
|
SESSION_EXPIRES_IN,
|
||||||
|
SESSION_SECRET,
|
||||||
)
|
)
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||||
|
idp = boto3.client('cognito-idp')
|
||||||
|
|
||||||
|
|
||||||
@router.post('/session')
|
@router.post('/session')
|
||||||
@@ -34,8 +35,11 @@ def session(
|
|||||||
):
|
):
|
||||||
user_id, password_hash = _get_user(username)
|
user_id, password_hash = _get_user(username)
|
||||||
|
|
||||||
if not pbkdf2_sha256.verify(password, password_hash):
|
if not password_hash:
|
||||||
raise ForbiddenError('Invalid credentials')
|
_get_idp_user(user_id, username, password)
|
||||||
|
else:
|
||||||
|
if not pbkdf2_sha256.verify(password, password_hash):
|
||||||
|
raise ForbiddenError('Invalid credentials')
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
status_code=HTTPStatus.OK,
|
status_code=HTTPStatus.OK,
|
||||||
@@ -52,7 +56,7 @@ def session(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_user(username: str) -> tuple[str, str]:
|
def _get_user(username: str) -> tuple[str, str | None]:
|
||||||
sk = SortKey(username, path_spec='user_id')
|
sk = SortKey(username, path_spec='user_id')
|
||||||
user = dyn.collection.get_items(
|
user = dyn.collection.get_items(
|
||||||
KeyPair(pk='email', sk=sk, rename_key=sk.path_spec)
|
KeyPair(pk='email', sk=sk, rename_key=sk.path_spec)
|
||||||
@@ -71,12 +75,57 @@ def _get_user(username: str) -> tuple[str, str]:
|
|||||||
rename_key='password',
|
rename_key='password',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
exc_cls=UserNotFoundError,
|
raise_on_error=False,
|
||||||
|
default=None,
|
||||||
|
# Uncomment the following line when removing support for Cognito
|
||||||
|
# exc_cls=UserNotFoundError,
|
||||||
)
|
)
|
||||||
|
|
||||||
return user['user_id'], password
|
return user['user_id'], password
|
||||||
|
|
||||||
|
|
||||||
|
def _get_idp_user(
|
||||||
|
user_id: str,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
) -> bool:
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise ForbiddenError('Invalid credentials')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def new_session(sub: str) -> str:
|
def new_session(sub: str) -> str:
|
||||||
session_id = str(uuid4())
|
session_id = str(uuid4())
|
||||||
now_ = now()
|
now_ = now()
|
||||||
@@ -89,8 +138,8 @@ def new_session(sub: str) -> str:
|
|||||||
'iat': int(now_.timestamp()),
|
'iat': int(now_.timestamp()),
|
||||||
'exp': exp,
|
'exp': exp,
|
||||||
},
|
},
|
||||||
JWT_SECRET,
|
SESSION_SECRET,
|
||||||
algorithm=JWT_ALGORITHM,
|
algorithm='HS256',
|
||||||
)
|
)
|
||||||
|
|
||||||
with dyn.transact_writer() as transact:
|
with dyn.transact_writer() as transact:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Globals:
|
|||||||
DYNAMODB_SORT_KEY: sk
|
DYNAMODB_SORT_KEY: sk
|
||||||
OAUTH2_TABLE: !Ref OAuth2Table
|
OAUTH2_TABLE: !Ref OAuth2Table
|
||||||
ISSUER: https://id.saladeaula.digital
|
ISSUER: https://id.saladeaula.digital
|
||||||
JWT_SECRET: 7DUTFB1iLeSpiXvmxbOZim1yPVmQbmBpAzgscob0RDzrL2wVwRi1ti2ZSry7jJAf
|
SESSION_SECRET: 7DUTFB1iLeSpiXvmxbOZim1yPVmQbmBpAzgscob0RDzrL2wVwRi1ti2ZSry7jJAf
|
||||||
OAUTH2_SCOPES_SUPPORTED: openid profile email offline_access read:users read:enrollments read:orders read:courses write:courses
|
OAUTH2_SCOPES_SUPPORTED: openid profile email offline_access read:users read:enrollments read:orders read:courses write:courses
|
||||||
|
|
||||||
Resources:
|
Resources:
|
||||||
@@ -51,6 +51,12 @@ Resources:
|
|||||||
Policies:
|
Policies:
|
||||||
- DynamoDBCrudPolicy:
|
- DynamoDBCrudPolicy:
|
||||||
TableName: !Ref OAuth2Table
|
TableName: !Ref OAuth2Table
|
||||||
|
- Version: 2012-10-17
|
||||||
|
Statement:
|
||||||
|
- Effect: Allow
|
||||||
|
Action:
|
||||||
|
- cognito-idp:InitiateAuth
|
||||||
|
Resource: !Sub arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/*
|
||||||
Events:
|
Events:
|
||||||
Session:
|
Session:
|
||||||
Type: HttpApi
|
Type: HttpApi
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ SK = 'sk'
|
|||||||
def pytest_configure():
|
def pytest_configure():
|
||||||
os.environ['TZ'] = 'America/Sao_Paulo'
|
os.environ['TZ'] = 'America/Sao_Paulo'
|
||||||
os.environ['OAUTH2_TABLE'] = PYTEST_TABLE_NAME
|
os.environ['OAUTH2_TABLE'] = PYTEST_TABLE_NAME
|
||||||
os.environ['JWT_SECRET'] = 'secret'
|
os.environ['SESSION_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['ISSUER'] = 'http://localhost'
|
os.environ['ISSUER'] = 'http://localhost'
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ def test_token(
|
|||||||
http_api_proxy: HttpApiProxy,
|
http_api_proxy: HttpApiProxy,
|
||||||
lambda_context: LambdaContext,
|
lambda_context: LambdaContext,
|
||||||
):
|
):
|
||||||
access_token = token['access_token']
|
|
||||||
|
|
||||||
tokens = dynamodb_persistence_layer.query(
|
tokens = dynamodb_persistence_layer.query(
|
||||||
key_cond_expr='#pk = :pk',
|
key_cond_expr='#pk = :pk',
|
||||||
expr_attr_name={
|
expr_attr_name={
|
||||||
@@ -76,7 +74,7 @@ def test_token(
|
|||||||
},
|
},
|
||||||
body=urlencode(
|
body=urlencode(
|
||||||
{
|
{
|
||||||
'token': access_token,
|
'token': token['access_token'],
|
||||||
# 'token_type_hint': 'access_token',
|
# 'token_type_hint': 'access_token',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
// User data
|
// 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": "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": "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"}
|
||||||
{"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": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SESSION#36af142e-9f6d-49d3-bfe9-6a6bd6ab2712", "created_at": "2025-09-17T13:44:34.544491-03:00", "ttl": 1760719474}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user