add revoke
This commit is contained in:
@@ -10,6 +10,7 @@ from aws_lambda_powertools.utilities.typing import LambdaContext
|
|||||||
from routes.authorize import router as authorize
|
from routes.authorize import router as authorize
|
||||||
from routes.jwks import router as jwks
|
from routes.jwks import router as jwks
|
||||||
from routes.openid_configuration import router as openid_configuration
|
from routes.openid_configuration import router as openid_configuration
|
||||||
|
from routes.revoke import router as revoke
|
||||||
from routes.session import router as session
|
from routes.session import router as session
|
||||||
from routes.token import router as token
|
from routes.token import router as token
|
||||||
from routes.userinfo import router as userinfo
|
from routes.userinfo import router as userinfo
|
||||||
@@ -22,6 +23,7 @@ app.include_router(authorize)
|
|||||||
app.include_router(jwks)
|
app.include_router(jwks)
|
||||||
app.include_router(token)
|
app.include_router(token)
|
||||||
app.include_router(userinfo)
|
app.include_router(userinfo)
|
||||||
|
app.include_router(revoke)
|
||||||
app.include_router(openid_configuration)
|
app.include_router(openid_configuration)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,15 +68,30 @@ class AuthorizationServer(oauth2.AuthorizationServer):
|
|||||||
raise ValueError('Missing request user')
|
raise ValueError('Missing request user')
|
||||||
|
|
||||||
now_ = now()
|
now_ = now()
|
||||||
client_id = request.payload.client_id
|
client_id = (
|
||||||
|
request.client.get_client_id()
|
||||||
|
if request.client
|
||||||
|
else request.payload.client_id
|
||||||
|
)
|
||||||
|
user_id = request.user.get('id')
|
||||||
access_token = token['access_token']
|
access_token = token['access_token']
|
||||||
refresh_token = token.get('refresh_token')
|
refresh_token = token.get('refresh_token')
|
||||||
token_type = token['token_type']
|
token_type = token['token_type']
|
||||||
scope = token['scope']
|
scope = token['scope']
|
||||||
expires_in = int(token['expires_in'])
|
expires_in = int(token['expires_in'])
|
||||||
issued_at = int(now_.timestamp())
|
issued_at = int(now_.timestamp())
|
||||||
|
access_token_ttl = ttl(start_dt=now_, seconds=expires_in)
|
||||||
|
refresh_token_ttl = ttl(start_dt=now_, seconds=OAUTH2_REFRESH_TOKEN_EXPIRES_IN)
|
||||||
|
|
||||||
with self._persistence_layer.transact_writer() as transact:
|
with self._persistence_layer.transact_writer() as transact:
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': user_id,
|
||||||
|
'sk': f'SESSION#ACCESS_TOKEN#{access_token}',
|
||||||
|
'ttl': access_token_ttl,
|
||||||
|
'created_at': now_,
|
||||||
|
}
|
||||||
|
)
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': 'OAUTH2#TOKEN',
|
'id': 'OAUTH2#TOKEN',
|
||||||
@@ -88,11 +103,19 @@ class AuthorizationServer(oauth2.AuthorizationServer):
|
|||||||
'user': request.user,
|
'user': request.user,
|
||||||
'expires_in': expires_in,
|
'expires_in': expires_in,
|
||||||
'issued_at': issued_at,
|
'issued_at': issued_at,
|
||||||
'ttl': ttl(start_dt=now_, seconds=expires_in),
|
'ttl': access_token_ttl,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if refresh_token:
|
if refresh_token:
|
||||||
|
transact.put(
|
||||||
|
item={
|
||||||
|
'id': user_id,
|
||||||
|
'sk': f'SESSION#REFRESH_TOKEN#{refresh_token}',
|
||||||
|
'ttl': access_token_ttl,
|
||||||
|
'created_at': now_,
|
||||||
|
}
|
||||||
|
)
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': 'OAUTH2#TOKEN',
|
'id': 'OAUTH2#TOKEN',
|
||||||
@@ -104,9 +127,7 @@ class AuthorizationServer(oauth2.AuthorizationServer):
|
|||||||
'user': request.user,
|
'user': request.user,
|
||||||
'expires_in': OAUTH2_REFRESH_TOKEN_EXPIRES_IN,
|
'expires_in': OAUTH2_REFRESH_TOKEN_EXPIRES_IN,
|
||||||
'issued_at': issued_at,
|
'issued_at': issued_at,
|
||||||
'ttl': ttl(
|
'ttl': refresh_token_ttl,
|
||||||
start_dt=now_, seconds=OAUTH2_REFRESH_TOKEN_EXPIRES_IN
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,7 +135,10 @@ class AuthorizationServer(oauth2.AuthorizationServer):
|
|||||||
|
|
||||||
def query_client(self, client_id: str):
|
def query_client(self, client_id: str):
|
||||||
client = self._persistence_layer.collection.get_item(
|
client = self._persistence_layer.collection.get_item(
|
||||||
KeyPair(pk='OAUTH2', sk=f'CLIENT_ID#{client_id}'),
|
KeyPair(
|
||||||
|
pk='OAUTH2',
|
||||||
|
sk=f'CLIENT_ID#{client_id}',
|
||||||
|
),
|
||||||
exc_cls=ClientNotFoundError,
|
exc_cls=ClientNotFoundError,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -125,9 +149,7 @@ class AuthorizationServer(oauth2.AuthorizationServer):
|
|||||||
redirect_uris=client['redirect_uris'],
|
redirect_uris=client['redirect_uris'],
|
||||||
response_types=client['response_types'],
|
response_types=client['response_types'],
|
||||||
grant_types=client['grant_types'],
|
grant_types=client['grant_types'],
|
||||||
token_endpoint_auth_method=client.get(
|
token_endpoint_auth_method=client.get('token_endpoint_auth_method', 'none'),
|
||||||
'token_endpoint_auth_method', 'client_secret_basic'
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_oauth2_request(
|
def create_oauth2_request(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from authlib.common.urls import add_params_to_uri
|
from authlib.common.urls import add_params_to_uri
|
||||||
from authlib.oauth2 import OAuth2Request, rfc9207
|
from authlib.oauth2 import OAuth2Request, rfc7009, rfc9207
|
||||||
from authlib.oauth2.rfc6749 import ClientMixin, TokenMixin, grants
|
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_
|
||||||
@@ -8,7 +8,12 @@ from aws_lambda_powertools import Logger
|
|||||||
from aws_lambda_powertools.event_handler.api_gateway import Response
|
from aws_lambda_powertools.event_handler.api_gateway import Response
|
||||||
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
|
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,
|
||||||
|
SortKey,
|
||||||
|
TransactKey,
|
||||||
|
)
|
||||||
from layercake.funcs import omit, pick
|
from layercake.funcs import omit, pick
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
@@ -22,7 +27,7 @@ from integrations.apigateway_oauth2.tokens import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
class OpenIDCode(OpenIDCode_):
|
class OpenIDCode(OpenIDCode_):
|
||||||
@@ -30,7 +35,7 @@ class OpenIDCode(OpenIDCode_):
|
|||||||
if not request.payload:
|
if not request.payload:
|
||||||
raise ValueError('Missing request payload')
|
raise ValueError('Missing request payload')
|
||||||
|
|
||||||
nonce_ = oauth2_layer.get_item(
|
nonce_ = dyn.get_item(
|
||||||
KeyPair(pk='OAUTH2#CODE', sk=f'NONCE#{nonce}'),
|
KeyPair(pk='OAUTH2#CODE', sk=f'NONCE#{nonce}'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,6 +70,7 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
|||||||
code: str,
|
code: str,
|
||||||
request: OAuth2Request,
|
request: OAuth2Request,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Save authorization_code for later use."""
|
||||||
if not request.payload:
|
if not request.payload:
|
||||||
raise ValueError('Missing request payload')
|
raise ValueError('Missing request payload')
|
||||||
|
|
||||||
@@ -81,7 +87,7 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
|||||||
now_ = now()
|
now_ = now()
|
||||||
ttl_ = ttl(start_dt=now_, minutes=10)
|
ttl_ = ttl(start_dt=now_, minutes=10)
|
||||||
|
|
||||||
with oauth2_layer.transact_writer() as transact:
|
with dyn.transact_writer() as transact:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': 'OAUTH2#CODE',
|
'id': 'OAUTH2#CODE',
|
||||||
@@ -116,7 +122,8 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
|||||||
code: str,
|
code: str,
|
||||||
client: ClientMixin,
|
client: ClientMixin,
|
||||||
) -> OAuth2AuthorizationCode:
|
) -> OAuth2AuthorizationCode:
|
||||||
auth_code = oauth2_layer.get_item(
|
"""Get authorization_code from previously savings."""
|
||||||
|
auth_code = dyn.get_item(
|
||||||
KeyPair(pk='OAUTH2#CODE', sk=f'CODE#{code}'),
|
KeyPair(pk='OAUTH2#CODE', sk=f'CODE#{code}'),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -129,16 +136,24 @@ class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
|||||||
self,
|
self,
|
||||||
authorization_code: OAuth2AuthorizationCode,
|
authorization_code: OAuth2AuthorizationCode,
|
||||||
) -> None:
|
) -> None:
|
||||||
oauth2_layer.delete_item(
|
"""Delete authorization code from database or cache."""
|
||||||
KeyPair(pk='OAUTH2#CODE', sk=f'CODE#{authorization_code.code}'),
|
dyn.delete_item(
|
||||||
|
KeyPair(
|
||||||
|
pk='OAUTH2#CODE',
|
||||||
|
sk=f'CODE#{authorization_code.code}',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def authenticate_user(
|
def authenticate_user(
|
||||||
self,
|
self,
|
||||||
authorization_code: OAuth2AuthorizationCode,
|
authorization_code: OAuth2AuthorizationCode,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
user = oauth2_layer.get_item(
|
"""Authenticate the user related to this authorization_code."""
|
||||||
KeyPair(pk=authorization_code.user_id, sk='0'),
|
user = dyn.get_item(
|
||||||
|
KeyPair(
|
||||||
|
pk=authorization_code.user_id,
|
||||||
|
sk='0',
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return pick(('id', 'name', 'email', 'email_verified'), user)
|
return pick(('id', 'name', 'email', 'email_verified'), user)
|
||||||
|
|
||||||
@@ -154,10 +169,13 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
|
|||||||
'client_secret_post',
|
'client_secret_post',
|
||||||
'none',
|
'none',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# The authorization server MAY issue a new refresh token
|
||||||
INCLUDE_NEW_REFRESH_TOKEN = True
|
INCLUDE_NEW_REFRESH_TOKEN = True
|
||||||
|
|
||||||
def authenticate_refresh_token(self, refresh_token: str, **kwargs) -> TokenMixin:
|
def authenticate_refresh_token(self, refresh_token: str, **kwargs) -> TokenMixin:
|
||||||
token = oauth2_layer.collection.get_item(
|
"""Get token information with refresh_token string."""
|
||||||
|
token = dyn.collection.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk='OAUTH2#TOKEN',
|
pk='OAUTH2#TOKEN',
|
||||||
sk=f'REFRESH_TOKEN#{refresh_token}',
|
sk=f'REFRESH_TOKEN#{refresh_token}',
|
||||||
@@ -175,16 +193,90 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def authenticate_user(self, refresh_token: TokenMixin):
|
def authenticate_user(self, refresh_token: TokenMixin):
|
||||||
|
"""Authenticate the user related to this credential."""
|
||||||
return refresh_token.get_user()
|
return refresh_token.get_user()
|
||||||
|
|
||||||
def revoke_old_credential(self, refresh_token: TokenMixin) -> None:
|
def revoke_old_credential(self, refresh_token: TokenMixin) -> None:
|
||||||
logger.info('Revoking old refresh token', refresh_token=refresh_token)
|
"""The authorization server MAY revoke the old refresh token after
|
||||||
token = getattr(refresh_token, 'refresh_token', None)
|
issuing a new refresh token to the client."""
|
||||||
|
|
||||||
if token:
|
logger.debug('Revoking old refresh token', refresh_token=refresh_token)
|
||||||
oauth2_layer.delete_item(
|
token = getattr(refresh_token, 'refresh_token', None)
|
||||||
KeyPair(pk='OAUTH2#TOKEN', sk=f'REFRESH_TOKEN#{token}')
|
user = refresh_token.get_user()
|
||||||
|
|
||||||
|
with dyn.transact_writer() as transact:
|
||||||
|
transact.delete(
|
||||||
|
key=KeyPair(
|
||||||
|
pk='OAUTH2#TOKEN',
|
||||||
|
sk=f'REFRESH_TOKEN#{token}',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
transact.delete(
|
||||||
|
key=KeyPair(
|
||||||
|
pk=user.get('id'),
|
||||||
|
sk=f'SESSION#REFRESH_TOKEN#{token}',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RevocationEndpoint(rfc7009.RevocationEndpoint):
|
||||||
|
def query_token( # type: ignore
|
||||||
|
self,
|
||||||
|
token_string: str,
|
||||||
|
token_type_hint: str | None = None,
|
||||||
|
):
|
||||||
|
result = dyn.collection.get_items(
|
||||||
|
TransactKey('OAUTH2#TOKEN')
|
||||||
|
+ SortKey(sk=f'REFRESH_TOKEN#{token_string}', rename_key='refresh_token')
|
||||||
|
+ SortKey(sk=f'ACCESS_TOKEN#{token_string}', rename_key='access_token'),
|
||||||
|
flatten_top=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.debug('Tokens retrieved', result=result)
|
||||||
|
|
||||||
|
if not token_type_hint:
|
||||||
|
token_type_hint = (
|
||||||
|
'refresh_token' if 'refresh_token' in result else 'access_token'
|
||||||
|
)
|
||||||
|
|
||||||
|
token = result[token_type_hint]
|
||||||
|
|
||||||
|
return OAuth2Token(
|
||||||
|
expires_in=int(token['expires_in']),
|
||||||
|
issued_at=int(token['issued_at']),
|
||||||
|
**{token_type_hint: token_string},
|
||||||
|
**omit(('expires_in', 'issued_at', 'refresh_token', 'access_token'), token),
|
||||||
|
)
|
||||||
|
|
||||||
|
def revoke_token(
|
||||||
|
self,
|
||||||
|
token: OAuth2Token,
|
||||||
|
request: OAuth2Request,
|
||||||
|
):
|
||||||
|
user_id = token.user['id']
|
||||||
|
r = dyn.collection.query(KeyPair(pk=user_id, sk='SESSION'))
|
||||||
|
|
||||||
|
with dyn.transact_writer() as transact:
|
||||||
|
# Revoke all sessions, access tokens, and refresh tokens
|
||||||
|
for x in r['items']:
|
||||||
|
pk, sk = x['id'], x['sk']
|
||||||
|
*_, kind, idx = sk.split('#')
|
||||||
|
|
||||||
|
transact.delete(key=KeyPair(pk, sk))
|
||||||
|
transact.delete(
|
||||||
|
key=KeyPair(
|
||||||
|
pk='SESSION',
|
||||||
|
sk=idx,
|
||||||
|
)
|
||||||
|
if kind == 'SESSION'
|
||||||
|
else KeyPair(
|
||||||
|
pk='OAUTH2#TOKEN',
|
||||||
|
sk=f'{kind}#{idx}',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IssuerParameter(rfc9207.IssuerParameter):
|
class IssuerParameter(rfc9207.IssuerParameter):
|
||||||
@@ -207,7 +299,7 @@ class IssuerParameter(rfc9207.IssuerParameter):
|
|||||||
return ISSUER
|
return ISSUER
|
||||||
|
|
||||||
|
|
||||||
server = AuthorizationServer(persistence_layer=oauth2_layer)
|
server = AuthorizationServer(persistence_layer=dyn)
|
||||||
server.register_grant(
|
server.register_grant(
|
||||||
AuthorizationCodeGrant,
|
AuthorizationCodeGrant,
|
||||||
[
|
[
|
||||||
@@ -216,4 +308,5 @@ server.register_grant(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
server.register_grant(RefreshTokenGrant)
|
server.register_grant(RefreshTokenGrant)
|
||||||
|
server.register_endpoint(RevocationEndpoint)
|
||||||
server.register_extension(IssuerParameter())
|
server.register_extension(IssuerParameter())
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ from authlib.oauth2.rfc6749 import errors
|
|||||||
from authlib.oauth2.rfc6749.util import scope_to_list
|
from authlib.oauth2.rfc6749.util import scope_to_list
|
||||||
from aws_lambda_powertools import Logger
|
from aws_lambda_powertools import Logger
|
||||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||||
from aws_lambda_powertools.event_handler.exceptions import BadRequestError, ServiceError
|
from aws_lambda_powertools.event_handler.exceptions import (
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
BadRequestError,
|
||||||
|
ServiceError,
|
||||||
|
UnauthorizedError,
|
||||||
|
)
|
||||||
|
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, JWT_ALGORITHM, JWT_SECRET, OAUTH2_TABLE
|
||||||
@@ -15,7 +19,7 @@ from oauth2 import server
|
|||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
logger = Logger(__name__)
|
logger = Logger(__name__)
|
||||||
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/authorize')
|
@router.get('/authorize')
|
||||||
@@ -36,9 +40,8 @@ def authorize():
|
|||||||
client_scopes = set(scope_to_list(grant.client.scope))
|
client_scopes = set(scope_to_list(grant.client.scope))
|
||||||
user_scopes = set(scope_to_list(session_scope)) if session_scope else set()
|
user_scopes = set(scope_to_list(session_scope)) if session_scope else set()
|
||||||
|
|
||||||
if not client_scopes.issubset(
|
# Deny authorization if user has no scopes matching the client request
|
||||||
user_scopes | {'openid', 'email', 'profile', 'offline_access'}
|
if not user_scopes & client_scopes:
|
||||||
):
|
|
||||||
raise errors.InvalidScopeError(status_code=HTTPStatus.UNAUTHORIZED)
|
raise errors.InvalidScopeError(status_code=HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
return server.create_authorization_response(
|
return server.create_authorization_response(
|
||||||
@@ -69,15 +72,27 @@ def verify_session(session_id: str) -> tuple[str, str | None]:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
oauth2_layer.collection.get_item(
|
user = dyn.collection.get_items(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk='SESSION',
|
pk='SESSION',
|
||||||
sk=payload['sid'],
|
sk=payload['sid'],
|
||||||
|
rename_key='session',
|
||||||
|
)
|
||||||
|
+ KeyPair(
|
||||||
|
pk=payload['sub'],
|
||||||
|
sk=SortKey(
|
||||||
|
sk='SCOPE',
|
||||||
|
path_spec='scope',
|
||||||
|
rename_key='scope',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
exc_cls=SessionRevokedError,
|
flatten_top=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
return payload['sub'], payload.get('scope')
|
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]:
|
def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
|
||||||
@@ -94,6 +109,4 @@ def _parse_cookies(cookies: list[str] | None) -> dict[str, str]:
|
|||||||
return parsed_cookies
|
return parsed_cookies
|
||||||
|
|
||||||
|
|
||||||
class SessionRevokedError(BadRequestError):
|
class SessionRevokedError(UnauthorizedError): ...
|
||||||
def __init__(self, *_):
|
|
||||||
super().__init__('Session revoked')
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ def openid_configuration():
|
|||||||
'issuer': ISSUER,
|
'issuer': ISSUER,
|
||||||
'authorization_endpoint': f'{ISSUER}/authorize',
|
'authorization_endpoint': f'{ISSUER}/authorize',
|
||||||
'token_endpoint': f'{ISSUER}/token',
|
'token_endpoint': f'{ISSUER}/token',
|
||||||
|
'revocation_endpoint': f'{ISSUER}/revoke',
|
||||||
'userinfo_endpoint': f'{ISSUER}/userinfo',
|
'userinfo_endpoint': f'{ISSUER}/userinfo',
|
||||||
'jwks_uri': f'{ISSUER}/jwks.json',
|
'jwks_uri': f'{ISSUER}/jwks.json',
|
||||||
'scopes_supported': OAUTH2_SCOPES_SUPPORTED.split(),
|
'scopes_supported': OAUTH2_SCOPES_SUPPORTED.split(),
|
||||||
|
|||||||
13
id.saladeaula.digital/app/routes/revoke.py
Normal file
13
id.saladeaula.digital/app/routes/revoke.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||||
|
|
||||||
|
from oauth2 import RevocationEndpoint, server
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post('/revoke')
|
||||||
|
def revoke():
|
||||||
|
return server.create_endpoint_response(
|
||||||
|
RevocationEndpoint.ENDPOINT_NAME,
|
||||||
|
router.current_event,
|
||||||
|
)
|
||||||
@@ -15,10 +15,16 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
|
|||||||
from passlib.hash import pbkdf2_sha256
|
from passlib.hash import pbkdf2_sha256
|
||||||
|
|
||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import ISSUER, JWT_ALGORITHM, JWT_EXP_SECONDS, JWT_SECRET, OAUTH2_TABLE
|
from config import (
|
||||||
|
ISSUER,
|
||||||
|
JWT_ALGORITHM,
|
||||||
|
JWT_SECRET,
|
||||||
|
OAUTH2_REFRESH_TOKEN_EXPIRES_IN,
|
||||||
|
OAUTH2_TABLE,
|
||||||
|
)
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
oauth2_layer = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(OAUTH2_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/session')
|
@router.post('/session')
|
||||||
@@ -26,11 +32,7 @@ def session(
|
|||||||
username: Annotated[str, Body()],
|
username: Annotated[str, Body()],
|
||||||
password: Annotated[str, Body()],
|
password: Annotated[str, Body()],
|
||||||
):
|
):
|
||||||
(
|
user_id, password_hash = _get_user(username)
|
||||||
user_id,
|
|
||||||
password_hash,
|
|
||||||
scope,
|
|
||||||
) = _get_user(username)
|
|
||||||
|
|
||||||
if not pbkdf2_sha256.verify(password, password_hash):
|
if not pbkdf2_sha256.verify(password, password_hash):
|
||||||
raise ForbiddenError('Invalid credentials')
|
raise ForbiddenError('Invalid credentials')
|
||||||
@@ -40,28 +42,27 @@ def session(
|
|||||||
cookies=[
|
cookies=[
|
||||||
Cookie(
|
Cookie(
|
||||||
name='session_id',
|
name='session_id',
|
||||||
value=new_session(user_id, scope),
|
value=new_session(user_id),
|
||||||
http_only=True,
|
http_only=True,
|
||||||
secure=True,
|
secure=True,
|
||||||
same_site=None,
|
same_site=None,
|
||||||
max_age=JWT_EXP_SECONDS,
|
max_age=OAUTH2_REFRESH_TOKEN_EXPIRES_IN,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_user(username: str) -> tuple[str, str, str | None]:
|
def _get_user(username: str) -> tuple[str, str]:
|
||||||
sk = SortKey(username, path_spec='user_id')
|
sk = SortKey(username, path_spec='user_id')
|
||||||
user = oauth2_layer.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)
|
||||||
+ KeyPair(pk='cpf', sk=sk, rename_key=sk.path_spec),
|
+ KeyPair(pk='cpf', sk=sk, rename_key=sk.path_spec),
|
||||||
flatten_top=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise UserNotFoundError()
|
raise UserNotFoundError()
|
||||||
|
|
||||||
userdata = oauth2_layer.collection.get_items(
|
password = dyn.collection.get_item(
|
||||||
KeyPair(
|
KeyPair(
|
||||||
pk=user['user_id'],
|
pk=user['user_id'],
|
||||||
sk=SortKey(
|
sk=SortKey(
|
||||||
@@ -69,46 +70,34 @@ def _get_user(username: str) -> tuple[str, str, str | None]:
|
|||||||
path_spec='hash',
|
path_spec='hash',
|
||||||
rename_key='password',
|
rename_key='password',
|
||||||
),
|
),
|
||||||
)
|
|
||||||
+ KeyPair(
|
|
||||||
pk=user['user_id'],
|
|
||||||
sk=SortKey(
|
|
||||||
sk='SCOPE',
|
|
||||||
path_spec='scope',
|
|
||||||
rename_key='scope',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
flatten_top=False,
|
exc_cls=UserNotFoundError,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not userdata:
|
return user['user_id'], password
|
||||||
raise UserNotFoundError()
|
|
||||||
|
|
||||||
return user['user_id'], userdata['password'], userdata.get('scope')
|
|
||||||
|
|
||||||
|
|
||||||
def new_session(sub: str, scope: str | None) -> str:
|
def new_session(sub: str) -> str:
|
||||||
|
session_id = str(uuid4())
|
||||||
now_ = now()
|
now_ = now()
|
||||||
sid = str(uuid4())
|
exp = ttl(start_dt=now_, seconds=OAUTH2_REFRESH_TOKEN_EXPIRES_IN)
|
||||||
exp = ttl(start_dt=now_, seconds=JWT_EXP_SECONDS)
|
|
||||||
token = jwt.encode(
|
token = jwt.encode(
|
||||||
{
|
{
|
||||||
'sid': sid,
|
'sid': session_id,
|
||||||
'sub': sub,
|
'sub': sub,
|
||||||
'iss': ISSUER,
|
'iss': ISSUER,
|
||||||
'iat': int(now_.timestamp()),
|
'iat': int(now_.timestamp()),
|
||||||
'exp': exp,
|
'exp': exp,
|
||||||
'scope': scope,
|
|
||||||
},
|
},
|
||||||
JWT_SECRET,
|
JWT_SECRET,
|
||||||
algorithm=JWT_ALGORITHM,
|
algorithm=JWT_ALGORITHM,
|
||||||
)
|
)
|
||||||
|
|
||||||
with oauth2_layer.transact_writer() as transact:
|
with dyn.transact_writer() as transact:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': 'SESSION',
|
'id': 'SESSION',
|
||||||
'sk': sid,
|
'sk': session_id,
|
||||||
'user_id': sub,
|
'user_id': sub,
|
||||||
'ttl': exp,
|
'ttl': exp,
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
@@ -117,7 +106,7 @@ def new_session(sub: str, scope: str | None) -> str:
|
|||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': sub,
|
'id': sub,
|
||||||
'sk': f'SESSION#{sid}',
|
'sk': f'SESSION#{session_id}',
|
||||||
'ttl': exp,
|
'ttl': exp,
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export const OK = 200
|
export const OK = 200
|
||||||
export const FOUND = 302
|
export const FOUND = 302
|
||||||
export const BAD_REQUEST = 400
|
export const BAD_REQUEST = 400
|
||||||
|
export const UNAUTHORIZED = 401
|
||||||
export const INTERNAL_SERVER = 500
|
export const INTERNAL_SERVER = 500
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
type RouteConfig,
|
|
||||||
index,
|
index,
|
||||||
layout,
|
layout,
|
||||||
route
|
route,
|
||||||
|
type RouteConfig
|
||||||
} from '@react-router/dev/routes'
|
} from '@react-router/dev/routes'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
layout('routes/layout.tsx', [index('routes/index.tsx')]),
|
layout('routes/layout.tsx', [index('routes/index.tsx')]),
|
||||||
route('/authorize', 'routes/authorize.ts'),
|
route('/authorize', 'routes/authorize.ts'),
|
||||||
route('/token', 'routes/token.ts')
|
route('/token', 'routes/token.ts'),
|
||||||
|
route('/revoke', 'routes/revoke.ts')
|
||||||
] satisfies RouteConfig
|
] satisfies RouteConfig
|
||||||
|
|||||||
@@ -30,15 +30,6 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
redirect: 'manual'
|
redirect: 'manual'
|
||||||
})
|
})
|
||||||
|
|
||||||
// if (r.status === httpStatus.BAD_REQUEST) {
|
|
||||||
// return new Response(null, {
|
|
||||||
// status: httpStatus.FOUND,
|
|
||||||
// headers: {
|
|
||||||
// Location: redirect.toString()
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (r.status === httpStatus.FOUND) {
|
if (r.status === httpStatus.FOUND) {
|
||||||
return new Response(await r.text(), {
|
return new Response(await r.text(), {
|
||||||
status: r.status,
|
status: r.status,
|
||||||
@@ -46,9 +37,17 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json(await r.json(), {
|
console.log('Issuer response', {
|
||||||
status: r.status,
|
json: await r.json(),
|
||||||
headers: r.headers
|
headers: r.headers,
|
||||||
|
status: r.status
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
status: httpStatus.FOUND,
|
||||||
|
headers: {
|
||||||
|
Location: redirect.toString()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return new Response(null, { status: httpStatus.INTERNAL_SERVER })
|
return new Response(null, { status: httpStatus.INTERNAL_SERVER })
|
||||||
|
|||||||
17
id.saladeaula.digital/client/app/routes/revoke.ts
Normal file
17
id.saladeaula.digital/client/app/routes/revoke.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Route } from './+types'
|
||||||
|
|
||||||
|
export async function action({ request, context }: Route.ActionArgs) {
|
||||||
|
const issuerUrl = new URL('/revoke', context.cloudflare.env.ISSUER_URL)
|
||||||
|
const r = await fetch(issuerUrl.toString(), {
|
||||||
|
method: request.method,
|
||||||
|
headers: request.headers,
|
||||||
|
body: await request.text()
|
||||||
|
})
|
||||||
|
|
||||||
|
// console.log(await r.text(), r)
|
||||||
|
|
||||||
|
return new Response(await r.text(), {
|
||||||
|
status: r.status,
|
||||||
|
headers: r.headers
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -82,6 +82,12 @@ Resources:
|
|||||||
Path: /token
|
Path: /token
|
||||||
Method: POST
|
Method: POST
|
||||||
ApiId: !Ref HttpApi
|
ApiId: !Ref HttpApi
|
||||||
|
Revoke:
|
||||||
|
Type: HttpApi
|
||||||
|
Properties:
|
||||||
|
Path: /revoke
|
||||||
|
Method: POST
|
||||||
|
ApiId: !Ref HttpApi
|
||||||
UserInfo:
|
UserInfo:
|
||||||
Type: HttpApi
|
Type: HttpApi
|
||||||
Properties:
|
Properties:
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ def pytest_configure():
|
|||||||
os.environ['OAUTH2_SCOPES_SUPPORTED'] = (
|
os.environ['OAUTH2_SCOPES_SUPPORTED'] = (
|
||||||
'openid profile email offline_access read:users'
|
'openid profile email offline_access read:users'
|
||||||
)
|
)
|
||||||
# os.environ['POWERTOOLS_LOGGER_LOG_EVENT'] = 'true'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from routes.session import new_session
|
|||||||
|
|
||||||
from ..conftest import HttpApiProxy, LambdaContext
|
from ..conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
CLIENT_ID = 'd72d4005-1fa7-4430-9754-80d5e2487bb6'
|
|
||||||
USER_ID = '357db1c5-7442-4075-98a3-fbe5c938a419'
|
USER_ID = '357db1c5-7442-4075-98a3-fbe5c938a419'
|
||||||
|
|
||||||
|
|
||||||
@@ -17,7 +16,7 @@ def test_authorize(
|
|||||||
http_api_proxy: HttpApiProxy,
|
http_api_proxy: HttpApiProxy,
|
||||||
lambda_context: LambdaContext,
|
lambda_context: LambdaContext,
|
||||||
):
|
):
|
||||||
session_id = new_session(USER_ID, 'read:users')
|
session_id = new_session(USER_ID)
|
||||||
|
|
||||||
r = app.lambda_handler(
|
r = app.lambda_handler(
|
||||||
http_api_proxy(
|
http_api_proxy(
|
||||||
@@ -25,7 +24,7 @@ def test_authorize(
|
|||||||
method=HTTPMethod.GET,
|
method=HTTPMethod.GET,
|
||||||
query_string_parameters={
|
query_string_parameters={
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'client_id': CLIENT_ID,
|
'client_id': 'd72d4005-1fa7-4430-9754-80d5e2487bb6',
|
||||||
'redirect_uri': 'https://localhost/callback',
|
'redirect_uri': 'https://localhost/callback',
|
||||||
'scope': 'openid offline_access read:users',
|
'scope': 'openid offline_access read:users',
|
||||||
'nonce': '123',
|
'nonce': '123',
|
||||||
@@ -61,7 +60,7 @@ def test_unauthorized(
|
|||||||
http_api_proxy: HttpApiProxy,
|
http_api_proxy: HttpApiProxy,
|
||||||
lambda_context: LambdaContext,
|
lambda_context: LambdaContext,
|
||||||
):
|
):
|
||||||
session_id = new_session(USER_ID, 'read:enrollments')
|
session_id = new_session(USER_ID)
|
||||||
|
|
||||||
r = app.lambda_handler(
|
r = app.lambda_handler(
|
||||||
http_api_proxy(
|
http_api_proxy(
|
||||||
@@ -69,7 +68,7 @@ def test_unauthorized(
|
|||||||
method=HTTPMethod.GET,
|
method=HTTPMethod.GET,
|
||||||
query_string_parameters={
|
query_string_parameters={
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'client_id': CLIENT_ID,
|
'client_id': '6ebe1709-0831-455c-84c0-d4c753bf33c6',
|
||||||
'redirect_uri': 'https://localhost/callback',
|
'redirect_uri': 'https://localhost/callback',
|
||||||
'scope': 'openid email offline_access',
|
'scope': 'openid email offline_access',
|
||||||
'nonce': '123',
|
'nonce': '123',
|
||||||
@@ -100,7 +99,7 @@ def test_authorize_revoked(
|
|||||||
method=HTTPMethod.GET,
|
method=HTTPMethod.GET,
|
||||||
query_string_parameters={
|
query_string_parameters={
|
||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'client_id': CLIENT_ID,
|
'client_id': 'd72d4005-1fa7-4430-9754-80d5e2487bb6',
|
||||||
'redirect_uri': 'https://localhost/callback',
|
'redirect_uri': 'https://localhost/callback',
|
||||||
'scope': 'openid offline_access',
|
'scope': 'openid offline_access',
|
||||||
'nonce': '123',
|
'nonce': '123',
|
||||||
|
|||||||
109
id.saladeaula.digital/tests/routes/test_revoke.py
Normal file
109
id.saladeaula.digital/tests/routes/test_revoke.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import json
|
||||||
|
import pprint
|
||||||
|
from base64 import b64encode
|
||||||
|
from http import HTTPMethod, HTTPStatus
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
|
|
||||||
|
from ..conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
|
CLIENT_ID = '1db63660-063d-4280-b2ea-388aca4a9459'
|
||||||
|
CLIENT_SECRET = '1nFD8alDbGHgc3g1RLY960xyRJVee0SlMoIB0MUlSuiJy28W'
|
||||||
|
AUTH = b64encode(f'{CLIENT_ID}:{CLIENT_SECRET}'.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def token(
|
||||||
|
app,
|
||||||
|
seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
http_api_proxy: HttpApiProxy,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
r = app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/token',
|
||||||
|
method=HTTPMethod.POST,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': f'Basic {AUTH}',
|
||||||
|
},
|
||||||
|
body=urlencode(
|
||||||
|
{
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'redirect_uri': 'https://localhost/callback',
|
||||||
|
'code': 'kyqp3oSuRFTfuBaCmq3XOgGWg67l42Kt3D6xPEj7Yd3MLdi9',
|
||||||
|
'code_verifier': '9072df2d3709425993e733f38fb27a825b8860e699364ce9abafdf51077c0bdb4e456ddb741147a4bec4eeda782d92cc',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
return json.loads(r['body'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_token(
|
||||||
|
app,
|
||||||
|
token,
|
||||||
|
seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
http_api_proxy: HttpApiProxy,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
access_token = token['access_token']
|
||||||
|
|
||||||
|
tokens = dynamodb_persistence_layer.query(
|
||||||
|
key_cond_expr='#pk = :pk',
|
||||||
|
expr_attr_name={
|
||||||
|
'#pk': 'id',
|
||||||
|
},
|
||||||
|
expr_attr_values={
|
||||||
|
':pk': 'OAUTH2#TOKEN',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(tokens['items']) == 2
|
||||||
|
|
||||||
|
r = app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/revoke',
|
||||||
|
method=HTTPMethod.POST,
|
||||||
|
headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': f'Basic {AUTH}',
|
||||||
|
},
|
||||||
|
body=urlencode(
|
||||||
|
{
|
||||||
|
'token': access_token,
|
||||||
|
# 'token_type_hint': 'access_token',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert r['statusCode'] == HTTPStatus.OK
|
||||||
|
|
||||||
|
tokens = dynamodb_persistence_layer.query(
|
||||||
|
key_cond_expr='#pk = :pk',
|
||||||
|
expr_attr_name={
|
||||||
|
'#pk': 'id',
|
||||||
|
},
|
||||||
|
expr_attr_values={
|
||||||
|
':pk': 'OAUTH2#TOKEN',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(tokens['items']) == 0
|
||||||
|
|
||||||
|
sessions = dynamodb_persistence_layer.query(
|
||||||
|
key_cond_expr='#pk = :pk',
|
||||||
|
expr_attr_name={
|
||||||
|
'#pk': 'id',
|
||||||
|
},
|
||||||
|
expr_attr_values={
|
||||||
|
':pk': 'SESSION',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(sessions['items']) == 0
|
||||||
@@ -27,4 +27,5 @@ def test_session(
|
|||||||
assert len(r['cookies']) == 1
|
assert len(r['cookies']) == 1
|
||||||
|
|
||||||
session = dynamodb_persistence_layer.collection.query(PartitionKey('SESSION'))
|
session = dynamodb_persistence_layer.collection.query(PartitionKey('SESSION'))
|
||||||
assert len(session['items']) == 1
|
# One seesion if created from seeds
|
||||||
|
assert len(session['items']) == 2
|
||||||
|
|||||||
@@ -35,50 +35,50 @@ def test_token(
|
|||||||
),
|
),
|
||||||
lambda_context,
|
lambda_context,
|
||||||
)
|
)
|
||||||
auth_token = json.loads(r['body'])
|
|
||||||
print(auth_token)
|
|
||||||
|
|
||||||
# assert r['statusCode'] == HTTPStatus.OK
|
# print(r)
|
||||||
# assert auth_token['expires_in'] == 600
|
|
||||||
|
|
||||||
# r = dynamodb_persistence_layer.query(
|
assert r['statusCode'] == HTTPStatus.OK
|
||||||
# key_cond_expr='#pk = :pk',
|
|
||||||
# expr_attr_name={
|
|
||||||
# '#pk': 'id',
|
|
||||||
# },
|
|
||||||
# expr_attr_values={
|
|
||||||
# ':pk': 'OAUTH2#TOKEN',
|
|
||||||
# },
|
|
||||||
# )
|
|
||||||
# assert len(r['items']) == 2
|
|
||||||
|
|
||||||
# r = app.lambda_handler(
|
r = json.loads(r['body'])
|
||||||
# http_api_proxy(
|
assert r['expires_in'] == 600
|
||||||
# raw_path='/token',
|
|
||||||
# method=HTTPMethod.POST,
|
|
||||||
# headers={
|
|
||||||
# 'Content-Type': 'application/x-www-form-urlencoded',
|
|
||||||
# },
|
|
||||||
# body=urlencode(
|
|
||||||
# {
|
|
||||||
# 'grant_type': 'refresh_token',
|
|
||||||
# 'refresh_token': auth_token['refresh_token'],
|
|
||||||
# 'client_id': client_id,
|
|
||||||
# }
|
|
||||||
# ),
|
|
||||||
# ),
|
|
||||||
# lambda_context,
|
|
||||||
# )
|
|
||||||
|
|
||||||
# assert r['statusCode'] == HTTPStatus.OK
|
tokens = dynamodb_persistence_layer.query(
|
||||||
|
key_cond_expr='#pk = :pk',
|
||||||
|
expr_attr_name={
|
||||||
|
'#pk': 'id',
|
||||||
|
},
|
||||||
|
expr_attr_values={
|
||||||
|
':pk': 'OAUTH2#TOKEN',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(tokens['items']) == 2
|
||||||
|
|
||||||
# r = dynamodb_persistence_layer.query(
|
r = app.lambda_handler(
|
||||||
# key_cond_expr='#pk = :pk',
|
http_api_proxy(
|
||||||
# expr_attr_name={
|
raw_path='/token',
|
||||||
# '#pk': 'id',
|
method=HTTPMethod.POST,
|
||||||
# },
|
headers={
|
||||||
# expr_attr_values={
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
# ':pk': 'OAUTH2#TOKEN',
|
},
|
||||||
# },
|
body=urlencode(
|
||||||
# )
|
{
|
||||||
# assert len(r['items']) == 3
|
'grant_type': 'refresh_token',
|
||||||
|
'refresh_token': r['refresh_token'],
|
||||||
|
'client_id': client_id,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
r = dynamodb_persistence_layer.query(
|
||||||
|
key_cond_expr='#pk = :pk',
|
||||||
|
expr_attr_name={
|
||||||
|
'#pk': 'id',
|
||||||
|
},
|
||||||
|
expr_attr_values={
|
||||||
|
':pk': 'OAUTH2#TOKEN',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert len(r['items']) == 3
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
// OAuth2
|
// OAuth2
|
||||||
{"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 offline_access read:users", "token_endpoint_auth_method": "none"}
|
{"id": "OAUTH2", "sk": "CLIENT_ID#d72d4005-1fa7-4430-9754-80d5e2487bb6", "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 read:users", "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", "response_type": "code", "code_challenge": "ejYEIGKQUgMnNh4eV0sftb0hXdLwkvKm6OHXRYvC--I", "code_challenge_method": "S256", "created_at": "2025-08-07T12:38:26.550431-03:00"}
|
{"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 read:users", "token_endpoint_auth_method": "client_secret_basic"}
|
||||||
|
{"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 read:users", "response_type": "code", "code_challenge": "ejYEIGKQUgMnNh4eV0sftb0hXdLwkvKm6OHXRYvC--I", "code_challenge_method": "S256", "created_at": "2025-08-07T12:38:26.550431-03:00"}
|
||||||
|
|
||||||
{"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"}
|
||||||
{"id": "cpf", "sk": "07879819908", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"}
|
{"id": "cpf", "sk": "07879819908", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"}
|
||||||
|
|
||||||
|
// Session
|
||||||
|
{"id": "SESSION", "sk": "36af142e-9f6d-49d3-bfe9-6a6bd6ab2712", "user_id": "357db1c5-7442-4075-98a3-fbe5c938a419"}
|
||||||
|
|
||||||
// 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": "read:users"}
|
{"id": "357db1c5-7442-4075-98a3-fbe5c938a419", "sk": "SCOPE", "scope": "read:users read:enrollments"}
|
||||||
|
{"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