wip
This commit is contained in:
@@ -3,12 +3,11 @@ import os
|
|||||||
ISSUER: str = os.getenv('ISSUER') # type: ignore
|
ISSUER: str = os.getenv('ISSUER') # type: ignore
|
||||||
|
|
||||||
OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore
|
OAUTH2_TABLE: str = os.getenv('OAUTH2_TABLE') # type: ignore
|
||||||
DYNAMODB_SORT_KEY = os.getenv('DYNAMODB_SORT_KEY')
|
OAUTH2_SCOPES_SUPPORTED: str = os.getenv('OAUTH2_SCOPES_SUPPORTED', '')
|
||||||
|
|
||||||
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
|
|
||||||
|
|
||||||
JWT_SECRET: str = os.environ.get('JWT_SECRET') # type: ignore
|
JWT_SECRET: str = os.environ.get('JWT_SECRET') # type: ignore
|
||||||
JWT_ALGORITHM = 'HS256'
|
JWT_ALGORITHM = 'HS256'
|
||||||
JWT_EXP_SECONDS = 900 # 15 minutes
|
JWT_EXP_SECONDS = 900 # 15 minutes
|
||||||
|
|
||||||
REFRESH_TOKEN_EXP_SECONDS = 7 * 86400 # 7 days
|
ACCESS_TOKEN_EXP_SECONDS = 3600 # 1 hour
|
||||||
|
REFRESH_TOKEN_EXP_SECONDS = 14 * 86400 # 14 days
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
import os
|
|
||||||
import secrets
|
|
||||||
from collections import defaultdict
|
|
||||||
from urllib.parse import parse_qs
|
|
||||||
|
|
||||||
from authlib.oauth2 import AuthorizationServer as _AuthorizationServer
|
|
||||||
from authlib.oauth2.rfc6749 import AuthorizationCodeMixin, ClientMixin, TokenMixin
|
|
||||||
from authlib.oauth2.rfc6749.requests import JsonRequest as _JsonRequest
|
|
||||||
from authlib.oauth2.rfc6749.requests import OAuth2Payload as _OAuth2Payload
|
|
||||||
from authlib.oauth2.rfc6749.requests import OAuth2Request as _OAuth2Request
|
|
||||||
from aws_lambda_powertools.event_handler.api_gateway import Response
|
|
||||||
from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import (
|
|
||||||
APIGatewayProxyEventV2,
|
|
||||||
)
|
|
||||||
|
|
||||||
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
|
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Payload(_OAuth2Payload):
|
|
||||||
def __init__(self, request: APIGatewayProxyEventV2):
|
|
||||||
self._request = request
|
|
||||||
|
|
||||||
@property
|
|
||||||
def decoded_body(self):
|
|
||||||
# TODO
|
|
||||||
body = parse_qs(self._request.decoded_body, keep_blank_values=True)
|
|
||||||
return {k: v[0] if len(v) == 1 else v for k, v in body.items()}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def data(self):
|
|
||||||
"""Combines query string parameters and the request body"""
|
|
||||||
return self._request.query_string_parameters | self.decoded_body
|
|
||||||
|
|
||||||
@property
|
|
||||||
def datalist(self) -> dict[str, list]:
|
|
||||||
values = defaultdict(list)
|
|
||||||
|
|
||||||
for k, v in self.data.items():
|
|
||||||
values[k].extend([v])
|
|
||||||
return values
|
|
||||||
|
|
||||||
|
|
||||||
class JsonRequest(_JsonRequest):
|
|
||||||
def __init__(self, request: APIGatewayProxyEventV2):
|
|
||||||
uri = f'https://{request.request_context.domain_name}'
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
request.request_context.http.method,
|
|
||||||
uri,
|
|
||||||
request.headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Request(_OAuth2Request):
|
|
||||||
def __init__(self, request: APIGatewayProxyEventV2):
|
|
||||||
uri = f'https://{request.request_context.domain_name}'
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
request.request_context.http.method,
|
|
||||||
uri,
|
|
||||||
request.headers,
|
|
||||||
)
|
|
||||||
self._request = request
|
|
||||||
self.payload = OAuth2Payload(request)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def args(self):
|
|
||||||
return self._request.query_string_parameters
|
|
||||||
|
|
||||||
@property
|
|
||||||
def form(self) -> dict[str, str]:
|
|
||||||
return self.payload.decoded_body
|
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Client(ClientMixin):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
client_id: str,
|
|
||||||
client_secret: str,
|
|
||||||
redirect_uris: list,
|
|
||||||
response_types: list,
|
|
||||||
grant_types: list,
|
|
||||||
token_endpoint_auth_method: str = 'client_secret_basic',
|
|
||||||
) -> None:
|
|
||||||
self.client_id = client_id
|
|
||||||
self.client_secret = client_secret
|
|
||||||
self.redirect_uris = redirect_uris
|
|
||||||
self.response_types = response_types
|
|
||||||
self.grant_types = grant_types
|
|
||||||
self.token_endpoint_auth_method = token_endpoint_auth_method
|
|
||||||
|
|
||||||
def get_client_id(self):
|
|
||||||
return self.client_id
|
|
||||||
|
|
||||||
def get_default_redirect_uri(self) -> str: # type: ignore
|
|
||||||
if self.redirect_uris:
|
|
||||||
return self.redirect_uris[0]
|
|
||||||
|
|
||||||
def check_response_type(self, response_type):
|
|
||||||
return response_type in self.response_types
|
|
||||||
|
|
||||||
def check_redirect_uri(self, redirect_uri):
|
|
||||||
return redirect_uri in self.redirect_uris
|
|
||||||
|
|
||||||
def check_endpoint_auth_method(self, method, endpoint):
|
|
||||||
if endpoint == 'token':
|
|
||||||
return self.token_endpoint_auth_method == method
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def check_grant_type(self, grant_type):
|
|
||||||
return grant_type in self.grant_types
|
|
||||||
|
|
||||||
def check_client_secret(self, client_secret):
|
|
||||||
return secrets.compare_digest(self.client_secret, client_secret)
|
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Token(TokenMixin): ...
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationCode(AuthorizationCodeMixin):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
user_id: str,
|
|
||||||
code: str,
|
|
||||||
client_id: str,
|
|
||||||
redirect_uri: str,
|
|
||||||
response_type: str,
|
|
||||||
scope: str,
|
|
||||||
code_challenge: str | None = None,
|
|
||||||
code_challenge_method: str | None = None,
|
|
||||||
nonce: str | None = None,
|
|
||||||
) -> None:
|
|
||||||
self.user_id = user_id
|
|
||||||
self.code = code
|
|
||||||
self.client_id = client_id
|
|
||||||
self.redirect_uri = redirect_uri
|
|
||||||
self.response_type = response_type
|
|
||||||
self.scope = scope
|
|
||||||
self.code_challenge = code_challenge
|
|
||||||
self.code_challenge_method = code_challenge_method
|
|
||||||
self.nonce = nonce
|
|
||||||
|
|
||||||
def get_redirect_uri(self):
|
|
||||||
return self.redirect_uri
|
|
||||||
|
|
||||||
def get_scope(self):
|
|
||||||
return self.scope
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationServer(_AuthorizationServer):
|
|
||||||
def __init__(self, query_client, save_token) -> None:
|
|
||||||
super().__init__(
|
|
||||||
scopes_supported=OAUTH2_SCOPES_SUPPORTED,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._query_client = query_client
|
|
||||||
self._save_token = save_token
|
|
||||||
|
|
||||||
def save_token(self, token, request):
|
|
||||||
return self._save_token(token, request)
|
|
||||||
|
|
||||||
def query_client(self, client_id: str):
|
|
||||||
return self._query_client(client_id)
|
|
||||||
|
|
||||||
def create_oauth2_request(self, request: APIGatewayProxyEventV2) -> OAuth2Request:
|
|
||||||
return OAuth2Request(request)
|
|
||||||
|
|
||||||
def create_json_request(self, request: APIGatewayProxyEventV2) -> JsonRequest:
|
|
||||||
return JsonRequest(request)
|
|
||||||
|
|
||||||
def handle_response(self, status, body, headers):
|
|
||||||
return Response(
|
|
||||||
status_code=status,
|
|
||||||
body=body,
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_signal(self, name, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import authlib.oauth2 as oauth2
|
||||||
|
import authlib.oauth2.rfc6749.requests as requests
|
||||||
|
from authlib.common.security import generate_token
|
||||||
|
from authlib.oauth2.rfc6750 import BearerTokenGenerator
|
||||||
|
from aws_lambda_powertools.event_handler.api_gateway import Response
|
||||||
|
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
|
||||||
|
from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import (
|
||||||
|
APIGatewayProxyEventV2,
|
||||||
|
)
|
||||||
|
from layercake.dateutils import now, ttl
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
|
||||||
|
from config import ACCESS_TOKEN_EXP_SECONDS, REFRESH_TOKEN_EXP_SECONDS
|
||||||
|
|
||||||
|
from .client import OAuth2Client
|
||||||
|
from .requests import APIGatewayJsonRequest, APIGatewayOAuth2Request
|
||||||
|
|
||||||
|
DYNAMODB_SORT_KEY = os.getenv('DYNAMODB_SORT_KEY')
|
||||||
|
OAUTH2_SCOPES_SUPPORTED = os.getenv('OAUTH2_SCOPES_SUPPORTED')
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationServer(oauth2.AuthorizationServer):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
) -> None:
|
||||||
|
self._persistence_layer = persistence_layer
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
scopes_supported=(
|
||||||
|
set(OAUTH2_SCOPES_SUPPORTED.split()) if OAUTH2_SCOPES_SUPPORTED else []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.register_token_generator(
|
||||||
|
'default',
|
||||||
|
BearerTokenGenerator(
|
||||||
|
access_token_generator=create_token_generator(42),
|
||||||
|
refresh_token_generator=create_token_generator(48),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_token(
|
||||||
|
self,
|
||||||
|
token: dict,
|
||||||
|
request: requests.OAuth2Request,
|
||||||
|
) -> None:
|
||||||
|
if not request.payload:
|
||||||
|
raise ValueError('Missing request payload')
|
||||||
|
|
||||||
|
if not request.user:
|
||||||
|
raise ValueError('Missing request user')
|
||||||
|
|
||||||
|
now_ = now()
|
||||||
|
client_id = request.payload.client_id
|
||||||
|
access_token = token['access_token']
|
||||||
|
refresh_token = token.get('refresh_token')
|
||||||
|
token_type = token['token_type']
|
||||||
|
scope = token['scope']
|
||||||
|
issued_at = int(now_.timestamp())
|
||||||
|
|
||||||
|
with self._persistence_layer.transact_writer() as transact:
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': 'OAUTH2#TOKEN',
|
||||||
|
'sk': f'ACCESS_TOKEN#{access_token}',
|
||||||
|
'client_id': client_id,
|
||||||
|
'token_type': token_type,
|
||||||
|
'refresh_token': refresh_token,
|
||||||
|
'scope': scope,
|
||||||
|
'user': request.user,
|
||||||
|
'expires_in': ACCESS_TOKEN_EXP_SECONDS,
|
||||||
|
'issued_at': issued_at,
|
||||||
|
'ttl': ttl(start_dt=now_, seconds=ACCESS_TOKEN_EXP_SECONDS),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if refresh_token:
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': 'OAUTH2#TOKEN',
|
||||||
|
'sk': f'REFRESH_TOKEN#{refresh_token}',
|
||||||
|
'client_id': client_id,
|
||||||
|
'token_type': token_type,
|
||||||
|
'access_token': access_token,
|
||||||
|
'scope': scope,
|
||||||
|
'user': request.user,
|
||||||
|
'expires_in': REFRESH_TOKEN_EXP_SECONDS,
|
||||||
|
'issued_at': issued_at,
|
||||||
|
'ttl': ttl(start_dt=now_, seconds=REFRESH_TOKEN_EXP_SECONDS),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def query_client(self, client_id: str):
|
||||||
|
client = self._persistence_layer.collection.get_item(
|
||||||
|
KeyPair(pk='OAUTH2', sk=f'CLIENT_ID#{client_id}'),
|
||||||
|
exc_cls=ClientNotFoundError,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, client_id = client.get(DYNAMODB_SORT_KEY, '').split('#')
|
||||||
|
|
||||||
|
return OAuth2Client(
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client['client_secret'],
|
||||||
|
scope=client['scope'],
|
||||||
|
redirect_uris=client['redirect_uris'],
|
||||||
|
response_types=client['response_types'],
|
||||||
|
grant_types=client['grant_types'],
|
||||||
|
token_endpoint_auth_method=client.get(
|
||||||
|
'token_endpoint_auth_method', 'client_secret_basic'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_oauth2_request(
|
||||||
|
self, request: APIGatewayProxyEventV2
|
||||||
|
) -> APIGatewayOAuth2Request:
|
||||||
|
return APIGatewayOAuth2Request(request)
|
||||||
|
|
||||||
|
def create_json_request(
|
||||||
|
self, request: APIGatewayProxyEventV2
|
||||||
|
) -> APIGatewayJsonRequest:
|
||||||
|
return APIGatewayJsonRequest(request)
|
||||||
|
|
||||||
|
def handle_response(self, status: int, body, headers):
|
||||||
|
return Response(
|
||||||
|
status_code=status,
|
||||||
|
body=body,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_signal(self, name: str, *args, **kwargs) -> None:
|
||||||
|
# after_authenticate_client
|
||||||
|
# when client is authenticated
|
||||||
|
|
||||||
|
# after_revoke_token
|
||||||
|
# when token is revoked
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class ClientNotFoundError(NotFoundError):
|
||||||
|
def __init__(self, *_):
|
||||||
|
super().__init__('Client not found')
|
||||||
|
|
||||||
|
|
||||||
|
def create_token_generator(length: int = 42):
|
||||||
|
def token_generator(*args, **kwargs):
|
||||||
|
return generate_token(length)
|
||||||
|
|
||||||
|
return token_generator
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import secrets
|
||||||
|
|
||||||
|
from authlib.oauth2.rfc6749 import (
|
||||||
|
ClientMixin,
|
||||||
|
list_to_scope,
|
||||||
|
scope_to_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Client(ClientMixin):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client_id: str,
|
||||||
|
client_secret: str,
|
||||||
|
scope: str,
|
||||||
|
redirect_uris: list,
|
||||||
|
response_types: list,
|
||||||
|
grant_types: list,
|
||||||
|
token_endpoint_auth_method: str = 'client_secret_basic',
|
||||||
|
) -> None:
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.scope = scope
|
||||||
|
self.redirect_uris = redirect_uris
|
||||||
|
self.response_types = response_types
|
||||||
|
self.grant_types = grant_types
|
||||||
|
self.token_endpoint_auth_method = token_endpoint_auth_method
|
||||||
|
|
||||||
|
def get_client_id(self):
|
||||||
|
return self.client_id
|
||||||
|
|
||||||
|
def get_allowed_scope(self, scope) -> str:
|
||||||
|
if not scope:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
allowed = set(self.scope.split())
|
||||||
|
scopes = scope_to_list(scope)
|
||||||
|
return list_to_scope([s for s in scopes if s in allowed])
|
||||||
|
|
||||||
|
def get_default_redirect_uri(self) -> str: # type: ignore
|
||||||
|
if self.redirect_uris:
|
||||||
|
return self.redirect_uris[0]
|
||||||
|
|
||||||
|
def check_response_type(self, response_type):
|
||||||
|
return response_type in self.response_types
|
||||||
|
|
||||||
|
def check_redirect_uri(self, redirect_uri):
|
||||||
|
return redirect_uri in self.redirect_uris
|
||||||
|
|
||||||
|
def check_endpoint_auth_method(self, method, endpoint):
|
||||||
|
if endpoint == 'token':
|
||||||
|
return self.token_endpoint_auth_method == method
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_grant_type(self, grant_type):
|
||||||
|
return grant_type in self.grant_types
|
||||||
|
|
||||||
|
def check_client_secret(self, client_secret):
|
||||||
|
return secrets.compare_digest(self.client_secret, client_secret)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
|
import authlib.oauth2.rfc6749.requests as requests
|
||||||
|
from aws_lambda_powertools.utilities.data_classes.api_gateway_proxy_event import (
|
||||||
|
APIGatewayProxyEventV2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIGatewayOAuth2Payload(requests.OAuth2Payload):
|
||||||
|
def __init__(self, request: APIGatewayProxyEventV2):
|
||||||
|
self._request = request
|
||||||
|
|
||||||
|
@property
|
||||||
|
def decoded_body(self):
|
||||||
|
# TODO
|
||||||
|
body = parse_qs(self._request.decoded_body, keep_blank_values=True)
|
||||||
|
return {k: v[0] if len(v) == 1 else v for k, v in body.items()}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
"""Combines query string parameters and the request body"""
|
||||||
|
return self._request.query_string_parameters | self.decoded_body
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datalist(self) -> dict[str, list]:
|
||||||
|
values = defaultdict(list)
|
||||||
|
|
||||||
|
for k, v in self.data.items():
|
||||||
|
values[k].extend([v])
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class APIGatewayJsonRequest(requests.JsonRequest):
|
||||||
|
def __init__(self, request: APIGatewayProxyEventV2):
|
||||||
|
uri = f'https://{request.request_context.domain_name}'
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
request.request_context.http.method,
|
||||||
|
uri,
|
||||||
|
request.headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIGatewayOAuth2Request(requests.OAuth2Request):
|
||||||
|
def __init__(self, request: APIGatewayProxyEventV2):
|
||||||
|
uri = f'https://{request.request_context.domain_name}'
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
request.request_context.http.method,
|
||||||
|
uri,
|
||||||
|
request.headers,
|
||||||
|
)
|
||||||
|
self._request = request
|
||||||
|
self.payload = APIGatewayOAuth2Payload(request)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def args(self):
|
||||||
|
# @TODO
|
||||||
|
return self._request.query_string_parameters
|
||||||
|
|
||||||
|
@property
|
||||||
|
def form(self) -> dict[str, str]:
|
||||||
|
# @TODO
|
||||||
|
return self.payload.decoded_body
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
from authlib.oauth2.rfc6749 import (
|
||||||
|
AuthorizationCodeMixin,
|
||||||
|
ClientMixin,
|
||||||
|
TokenMixin,
|
||||||
|
)
|
||||||
|
from layercake.dateutils import fromisoformat
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2AuthorizationCode(AuthorizationCodeMixin):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
code: str,
|
||||||
|
client_id: str,
|
||||||
|
redirect_uri: str,
|
||||||
|
response_type: str,
|
||||||
|
scope: str,
|
||||||
|
code_challenge: str | None = None,
|
||||||
|
code_challenge_method: str | None = None,
|
||||||
|
nonce: str | None = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> None:
|
||||||
|
self.user_id = user_id
|
||||||
|
self.code = code
|
||||||
|
self.client_id = client_id
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
self.response_type = response_type
|
||||||
|
self.scope = scope
|
||||||
|
self.code_challenge = code_challenge
|
||||||
|
self.code_challenge_method = code_challenge_method
|
||||||
|
self.nonce = nonce
|
||||||
|
|
||||||
|
auth_time = fromisoformat(kwargs.get('created_at', '')) or now()
|
||||||
|
self.auth_time = int(auth_time.timestamp())
|
||||||
|
|
||||||
|
def get_redirect_uri(self):
|
||||||
|
return self.redirect_uri
|
||||||
|
|
||||||
|
def get_scope(self):
|
||||||
|
return self.scope
|
||||||
|
|
||||||
|
def get_nonce(self):
|
||||||
|
return self.nonce
|
||||||
|
|
||||||
|
def get_auth_time(self):
|
||||||
|
return self.auth_time
|
||||||
|
|
||||||
|
def get_acr(self):
|
||||||
|
return '0'
|
||||||
|
|
||||||
|
def get_amr(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2Token(TokenMixin):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
user: dict,
|
||||||
|
client_id: str,
|
||||||
|
scope: str,
|
||||||
|
expires_in: int,
|
||||||
|
issued_at: int,
|
||||||
|
access_token: str | None = None,
|
||||||
|
refresh_token: str | None = None,
|
||||||
|
**_,
|
||||||
|
) -> None:
|
||||||
|
self.user = user
|
||||||
|
self.client_id = client_id
|
||||||
|
self.scope = scope
|
||||||
|
self.expires_in = expires_in
|
||||||
|
self.issued_at = issued_at
|
||||||
|
self.access_token = access_token
|
||||||
|
self.refresh_token = refresh_token
|
||||||
|
|
||||||
|
def get_user(self) -> dict:
|
||||||
|
return self.user
|
||||||
|
|
||||||
|
def check_client(self, client: ClientMixin):
|
||||||
|
return self.client_id == client.get_client_id()
|
||||||
|
|
||||||
|
def get_scope(self) -> str:
|
||||||
|
return self.scope
|
||||||
|
|
||||||
|
def get_expires_in(self) -> int:
|
||||||
|
return self.expires_in
|
||||||
|
|
||||||
|
def is_revoked(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
if not self.expires_in:
|
||||||
|
return False
|
||||||
|
|
||||||
|
expires_at = self.issued_at + self.expires_in
|
||||||
|
return expires_at < time.time()
|
||||||
@@ -1,110 +1,89 @@
|
|||||||
from authlib.oauth2.rfc6749 import TokenMixin, grants
|
from authlib.oauth2 import OAuth2Request
|
||||||
|
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.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 pick
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import DYNAMODB_SORT_KEY, OAUTH2_TABLE
|
from config import ISSUER, JWT_ALGORITHM, OAUTH2_TABLE
|
||||||
from integrations.apigateway_oauth2 import (
|
from integrations.apigateway_oauth2.authorization_server import (
|
||||||
AuthorizationCode,
|
|
||||||
AuthorizationServer,
|
AuthorizationServer,
|
||||||
OAuth2Client,
|
)
|
||||||
|
from integrations.apigateway_oauth2.tokens import (
|
||||||
|
OAuth2AuthorizationCode,
|
||||||
OAuth2Token,
|
OAuth2Token,
|
||||||
)
|
)
|
||||||
|
|
||||||
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
DUMMY_JWT_CONFIG = {
|
|
||||||
'key': 'secret-key',
|
|
||||||
'alg': 'HS256',
|
|
||||||
'iss': 'https://authlib.org',
|
|
||||||
'exp': 3600,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def create_save_token_func(persistence_layer: DynamoDBPersistenceLayer):
|
|
||||||
def save_token(token, request) -> OAuth2Token:
|
|
||||||
print('save_token')
|
|
||||||
return OAuth2Token()
|
|
||||||
|
|
||||||
return save_token
|
|
||||||
|
|
||||||
|
|
||||||
def create_query_client_func(persistence_layer: DynamoDBPersistenceLayer):
|
|
||||||
class ClientNotFoundError(NotFoundError):
|
|
||||||
def __init__(self, *_):
|
|
||||||
super().__init__('Client not found')
|
|
||||||
|
|
||||||
def query_client(client_id) -> OAuth2Client:
|
|
||||||
client = persistence_layer.collection.get_item(
|
|
||||||
KeyPair(
|
|
||||||
pk='OAUTH2_CLIENT',
|
|
||||||
sk=f'CLIENT_ID#{client_id}',
|
|
||||||
),
|
|
||||||
exc_cls=ClientNotFoundError,
|
|
||||||
)
|
|
||||||
|
|
||||||
_, client_id = client.get(DYNAMODB_SORT_KEY, '').split('#')
|
|
||||||
|
|
||||||
return OAuth2Client(
|
|
||||||
client_id=client_id,
|
|
||||||
client_secret=client['secret'],
|
|
||||||
redirect_uris=client['redirect_uris'],
|
|
||||||
response_types=client['response_types'],
|
|
||||||
grant_types=client['grant_types'],
|
|
||||||
token_endpoint_auth_method=client['token_endpoint_auth_method'],
|
|
||||||
)
|
|
||||||
|
|
||||||
return query_client
|
|
||||||
|
|
||||||
|
|
||||||
class OpenIDCode(OpenIDCode_):
|
class OpenIDCode(OpenIDCode_):
|
||||||
def exists_nonce(self, nonce, request):
|
def exists_nonce(self, nonce: str, request: OAuth2Request) -> bool:
|
||||||
|
if not request.payload:
|
||||||
|
raise ValueError('Missing request payload')
|
||||||
|
|
||||||
nonce_ = oauth2_layer.get_item(
|
nonce_ = oauth2_layer.get_item(
|
||||||
KeyPair(
|
KeyPair(pk='OAUTH2#CODE', sk=f'NONCE#{nonce}'),
|
||||||
f'OAUTH2_CODE#CLIENT_ID#{request.payload.client_id}', # type:ignore
|
|
||||||
f'NONCE#{nonce}',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return bool(nonce_)
|
return bool(nonce_)
|
||||||
|
|
||||||
def get_jwt_config(self, grant):
|
def get_jwt_config(self, grant):
|
||||||
return DUMMY_JWT_CONFIG
|
return {
|
||||||
|
'key': 'secret-key',
|
||||||
|
'alg': JWT_ALGORITHM,
|
||||||
|
'iss': ISSUER,
|
||||||
|
'exp': 3600,
|
||||||
|
}
|
||||||
|
|
||||||
def generate_user_info(self, user, scope):
|
def generate_user_info(self, user: dict, scope: str) -> UserInfo:
|
||||||
return UserInfo(
|
return UserInfo(
|
||||||
sub=user.id,
|
sub=user['id'],
|
||||||
name=user.name,
|
name=user['name'],
|
||||||
email=user.email,
|
email=user['email'],
|
||||||
|
email_verified=user.get('email_verified', False),
|
||||||
).filter(scope)
|
).filter(scope)
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
||||||
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none']
|
TOKEN_ENDPOINT_AUTH_METHODS = [
|
||||||
|
'client_secret_basic',
|
||||||
|
'client_secret_post',
|
||||||
|
'none',
|
||||||
|
]
|
||||||
|
|
||||||
def save_authorization_code(self, code: str, request):
|
def save_authorization_code(
|
||||||
client_id: str = request.payload.client_id # type: ignore
|
self,
|
||||||
data: dict = request.payload.data # type: ignore
|
code: str,
|
||||||
user: dict = request.user # type: ignore
|
request: OAuth2Request,
|
||||||
|
) -> None:
|
||||||
|
if not request.payload:
|
||||||
|
raise ValueError('Missing request payload')
|
||||||
|
|
||||||
|
if not request.user:
|
||||||
|
raise ValueError('Missing request user')
|
||||||
|
|
||||||
|
client_id: str = request.payload.client_id
|
||||||
|
data: dict = request.payload.data
|
||||||
|
user: dict = request.user
|
||||||
nonce: str | None = data.get('nonce')
|
nonce: str | None = data.get('nonce')
|
||||||
code_challenge: str | None = data.get('code_challenge')
|
code_challenge: str | None = data.get('code_challenge')
|
||||||
code_challenge_method: str | None = data.get('code_challenge_method')
|
code_challenge_method: str | None = data.get('code_challenge_method')
|
||||||
now_ = now()
|
now_ = now()
|
||||||
ttl_ = ttl(start_dt=now_, minutes=15)
|
ttl_ = ttl(start_dt=now_, minutes=10)
|
||||||
|
|
||||||
with oauth2_layer.transact_writer() as transact:
|
with oauth2_layer.transact_writer() as transact:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': f'OAUTH2_CODE#CLIENT_ID#{client_id}',
|
'id': 'OAUTH2#CODE',
|
||||||
'sk': f'CODE#{code}',
|
'sk': f'CODE#{code}',
|
||||||
'redirect_uri': request.payload.redirect_uri, # type: ignore
|
'redirect_uri': request.payload.redirect_uri,
|
||||||
'scope': request.payload.scope, # type: ignore
|
'response_type': request.payload.response_type,
|
||||||
|
'scope': request.payload.scope,
|
||||||
|
'client_id': client_id,
|
||||||
'user_id': user['id'],
|
'user_id': user['id'],
|
||||||
'nonce': nonce,
|
'nonce': nonce,
|
||||||
'code_challenge': code_challenge,
|
'code_challenge': code_challenge,
|
||||||
@@ -117,56 +96,90 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
|||||||
if nonce:
|
if nonce:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': f'OAUTH2_CODE#CLIENT_ID#{client_id}',
|
'id': 'OAUTH2#CODE',
|
||||||
'sk': f'NONCE#{nonce}',
|
'sk': f'NONCE#{nonce}',
|
||||||
|
'client_id': client_id,
|
||||||
'code': code,
|
'code': code,
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
'ttl': ttl_,
|
'ttl': ttl_,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def query_authorization_code(self, code, client):
|
def query_authorization_code(
|
||||||
client_id = client.get_client_id()
|
self,
|
||||||
|
code: str,
|
||||||
|
client: ClientMixin,
|
||||||
|
) -> OAuth2AuthorizationCode:
|
||||||
auth_code = oauth2_layer.get_item(
|
auth_code = oauth2_layer.get_item(
|
||||||
|
KeyPair(pk='OAUTH2#CODE', sk=f'CODE#{code}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return OAuth2AuthorizationCode(
|
||||||
|
code=code,
|
||||||
|
**auth_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_authorization_code(
|
||||||
|
self,
|
||||||
|
authorization_code: OAuth2AuthorizationCode,
|
||||||
|
) -> None:
|
||||||
|
oauth2_layer.delete_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk=f'OAUTH2_CODE#CLIENT_ID#{client_id}',
|
pk='OAUTH2#CODE',
|
||||||
sk=f'CODE#{code}',
|
sk=f'CODE#{authorization_code.code}',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return AuthorizationCode(
|
def authenticate_user(
|
||||||
client_id=client_id,
|
self,
|
||||||
code=code,
|
authorization_code: OAuth2AuthorizationCode,
|
||||||
**omit(('id', 'sk'), auth_code),
|
) -> dict:
|
||||||
)
|
|
||||||
|
|
||||||
def delete_authorization_code(self, authorization_code):
|
|
||||||
print('authorization_code')
|
|
||||||
|
|
||||||
def authenticate_user(self, authorization_code):
|
|
||||||
user = oauth2_layer.get_item(
|
user = oauth2_layer.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk=authorization_code.user_id,
|
pk=authorization_code.user_id,
|
||||||
sk='0',
|
sk='0',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return pick(('id', 'name', 'email'), user)
|
return pick(('id', 'name', 'email', 'email_verified'), user)
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenGrant(grants.RefreshTokenGrant):
|
class RefreshTokenGrant(grants.RefreshTokenGrant):
|
||||||
|
TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_basic', 'client_secret_post', 'none']
|
||||||
INCLUDE_NEW_REFRESH_TOKEN = True
|
INCLUDE_NEW_REFRESH_TOKEN = True
|
||||||
|
|
||||||
def authenticate_refresh_token(self, refresh_token: str) -> TokenMixin: ...
|
def authenticate_refresh_token(self, refresh_token: str, **kwargs) -> TokenMixin:
|
||||||
|
token = oauth2_layer.get_item(
|
||||||
|
KeyPair(
|
||||||
|
pk='OAUTH2#TOKEN',
|
||||||
|
sk=f'REFRESH_TOKEN#{refresh_token}',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def authenticate_user(self, refresh_token): ...
|
return OAuth2Token(
|
||||||
|
client_id=token['client_id'],
|
||||||
|
scope=token['scope'],
|
||||||
|
expires_in=int(token['expires_in']),
|
||||||
|
issued_at=int(token['issued_at']),
|
||||||
|
user=token['user'],
|
||||||
|
refresh_token=refresh_token,
|
||||||
|
)
|
||||||
|
|
||||||
def revoke_old_credential(self, refresh_token: TokenMixin) -> None: ...
|
def authenticate_user(self, refresh_token: TokenMixin):
|
||||||
|
return refresh_token.get_user()
|
||||||
|
|
||||||
|
def revoke_old_credential(self, refresh_token: TokenMixin) -> None:
|
||||||
|
refresh_token_ = getattr(refresh_token, 'refresh_token')
|
||||||
|
|
||||||
|
if refresh_token_:
|
||||||
|
oauth2_layer.delete_item(
|
||||||
|
KeyPair(
|
||||||
|
pk='OAUTH2#TOKEN',
|
||||||
|
sk=f'REFRESH_TOKEN#{refresh_token_}',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
server = AuthorizationServer(
|
server = AuthorizationServer(persistence_layer=oauth2_layer)
|
||||||
query_client=create_query_client_func(oauth2_layer),
|
|
||||||
save_token=create_save_token_func(oauth2_layer),
|
|
||||||
)
|
|
||||||
server.register_grant(
|
server.register_grant(
|
||||||
AuthorizationCodeGrant,
|
AuthorizationCodeGrant,
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||||
|
|
||||||
from config import ISSUER, JWT_ALGORITHM
|
from config import ISSUER, JWT_ALGORITHM, OAUTH2_SCOPES_SUPPORTED
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ def openid_configuration():
|
|||||||
'token_endpoint': f'{ISSUER}/token',
|
'token_endpoint': f'{ISSUER}/token',
|
||||||
'userinfo_endpoint': f'{ISSUER}/userinfo',
|
'userinfo_endpoint': f'{ISSUER}/userinfo',
|
||||||
'jwks_uri': f'{ISSUER}/jwks.json',
|
'jwks_uri': f'{ISSUER}/jwks.json',
|
||||||
'scopes_supported': ['openid', 'profile', 'email'],
|
'scopes_supported': OAUTH2_SCOPES_SUPPORTED.split(),
|
||||||
'response_types_supported': ['code'],
|
'response_types_supported': ['code'],
|
||||||
'grant_types_supported': ['authorization_code', 'refresh_token'],
|
'grant_types_supported': ['authorization_code', 'refresh_token'],
|
||||||
'subject_types_supported': ['public'],
|
'subject_types_supported': ['public'],
|
||||||
@@ -21,5 +21,6 @@ def openid_configuration():
|
|||||||
'token_endpoint_auth_methods_supported': [
|
'token_endpoint_auth_methods_supported': [
|
||||||
'client_secret_basic',
|
'client_secret_basic',
|
||||||
'client_secret_post',
|
'client_secret_post',
|
||||||
|
'none',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import secrets
|
|
||||||
|
|
||||||
SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
||||||
|
|
||||||
|
|
||||||
def gen_salt(length: int) -> str:
|
|
||||||
"""Generate a random string of SALT_CHARS with specified ``length``."""
|
|
||||||
if length <= 0:
|
|
||||||
raise ValueError('Salt length must be at least 1.')
|
|
||||||
|
|
||||||
return ''.join(secrets.choice(SALT_CHARS) for _ in range(length))
|
|
||||||
@@ -26,6 +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
|
||||||
|
|
||||||
Resources:
|
Resources:
|
||||||
HttpLog:
|
HttpLog:
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ 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['POWERTOOLS_LOGGER_LOG_EVENT'] = 'true'
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -42,16 +42,14 @@ def test_authorize(
|
|||||||
assert 'Location' in r['headers']
|
assert 'Location' in r['headers']
|
||||||
|
|
||||||
r = dynamodb_persistence_layer.query(
|
r = dynamodb_persistence_layer.query(
|
||||||
key_cond_expr='#pk = :pk AND begins_with(#sk, :sk)',
|
key_cond_expr='#pk = :pk',
|
||||||
expr_attr_name={
|
expr_attr_name={
|
||||||
'#pk': 'id',
|
'#pk': 'id',
|
||||||
'#sk': 'sk',
|
|
||||||
},
|
},
|
||||||
expr_attr_values={
|
expr_attr_values={
|
||||||
':pk': f'OAUTH2_CODE#CLIENT_ID#{client_id}',
|
':pk': 'OAUTH2#CODE',
|
||||||
':sk': 'CODE',
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# One item was added from seeds
|
# One item was added from seeds
|
||||||
assert len(r['items']) == 2
|
assert len(r['items']) == 3
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from http import HTTPMethod
|
from http import HTTPMethod
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from ..conftest import HttpApiProxy, LambdaContext
|
from ..conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ def test_html(
|
|||||||
lambda_context,
|
lambda_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(r)
|
# print(r)
|
||||||
|
|
||||||
|
|
||||||
def test_login(
|
def test_login(
|
||||||
@@ -34,9 +35,15 @@ def test_login(
|
|||||||
headers={
|
headers={
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body='username=sergio@somosbeta.com.br&password=pytest@123&continue=https://localhost',
|
body=urlencode(
|
||||||
|
{
|
||||||
|
'username': 'sergio@somosbeta.com.br',
|
||||||
|
'password': 'pytest@123',
|
||||||
|
'continue': 'http://localhost',
|
||||||
|
}
|
||||||
|
),
|
||||||
),
|
),
|
||||||
lambda_context,
|
lambda_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(r)
|
# print(r)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from http import HTTPMethod
|
import json
|
||||||
|
import pprint
|
||||||
|
from http import HTTPMethod, HTTPStatus
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
@@ -29,11 +31,43 @@ def test_token(
|
|||||||
'code': 'kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9',
|
'code': 'kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9',
|
||||||
'client_id': client_id,
|
'client_id': client_id,
|
||||||
'code_verifier': '9072df2d3709425993e733f38fb27a825b8860e699364ce9abafdf51077c0bdb4e456ddb741147a4bec4eeda782d92cc',
|
'code_verifier': '9072df2d3709425993e733f38fb27a825b8860e699364ce9abafdf51077c0bdb4e456ddb741147a4bec4eeda782d92cc',
|
||||||
# 'client_secret': '1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W',
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
assert r['statusCode'] == HTTPStatus.OK
|
||||||
|
data = json.loads(r['body'])
|
||||||
|
|
||||||
|
# print(data)
|
||||||
|
r = dynamodb_persistence_layer.query(
|
||||||
|
key_cond_expr='#pk = :pk',
|
||||||
|
expr_attr_name={
|
||||||
|
'#pk': 'id',
|
||||||
|
},
|
||||||
|
expr_attr_values={
|
||||||
|
':pk': 'OAUTH2#TOKEN',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# pprint.pp(r['items'])
|
||||||
|
|
||||||
|
r = app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/token',
|
||||||
|
method=HTTPMethod.POST,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body=urlencode(
|
||||||
|
{
|
||||||
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': data['refresh_token'],
|
||||||
|
'client_id': client_id,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
lambda_context,
|
lambda_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(r)
|
assert r['statusCode'] == HTTPStatus.OK
|
||||||
|
# print(r['body'])
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// OAuth2
|
// OAuth2
|
||||||
{"id": "OAUTH2_CLIENT", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "secret": "1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W", "name": "pytest", "scope": "openid profile", "redirect_uris": ["https://localhost/callback"], "response_types": ["code"], "grant_types": ["authorization_code", "refresh_token"], "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", "token_endpoint_auth_method": "none"}
|
||||||
{"id": "OAUTH2_CODE#CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "sk": "CODE#kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9", "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"}
|
{"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
|
||||||
// {"id": "EMAIL", "sk": "sergio@somosbeta.com.br", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"}
|
// {"id": "EMAIL", "sk": "sergio@somosbeta.com.br", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"}
|
||||||
|
|||||||
13
id.saladeaula.digital/uv.lock
generated
13
id.saladeaula.digital/uv.lock
generated
@@ -481,7 +481,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.9.7"
|
version = "0.9.8"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
@@ -497,6 +497,7 @@ dependencies = [
|
|||||||
{ name = "pycpfcnpj" },
|
{ name = "pycpfcnpj" },
|
||||||
{ name = "pydantic", extra = ["email"] },
|
{ name = "pydantic", extra = ["email"] },
|
||||||
{ name = "pydantic-extra-types" },
|
{ name = "pydantic-extra-types" },
|
||||||
|
{ name = "pyjwt" },
|
||||||
{ name = "python-jose", extra = ["cryptography"] },
|
{ name = "python-jose", extra = ["cryptography"] },
|
||||||
{ name = "pytz" },
|
{ name = "pytz" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
@@ -520,6 +521,7 @@ requires-dist = [
|
|||||||
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
{ name = "pycpfcnpj", specifier = ">=1.8" },
|
||||||
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6" },
|
||||||
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
{ name = "pydantic-extra-types", specifier = ">=2.10.3" },
|
||||||
|
{ name = "pyjwt", specifier = ">=2.10.1" },
|
||||||
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
|
{ name = "python-jose", extras = ["cryptography"], specifier = ">=3.5.0" },
|
||||||
{ name = "pytz", specifier = ">=2025.1" },
|
{ name = "pytz", specifier = ">=2025.1" },
|
||||||
{ name = "requests", specifier = ">=2.32.3" },
|
{ name = "requests", specifier = ">=2.32.3" },
|
||||||
@@ -750,6 +752,15 @@ 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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "8.4.1"
|
version = "8.4.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user