update
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
from os import rename
|
||||
|
||||
from authlib.common.security import generate_token
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
from authlib.jose import JsonWebKey
|
||||
@@ -13,7 +11,6 @@ from authlib.oidc.core import OpenIDCode as OpenIDCode_
|
||||
from authlib.oidc.core import UserInfo
|
||||
from aws_lambda_powertools import Logger
|
||||
from aws_lambda_powertools.event_handler.api_gateway import Response
|
||||
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
|
||||
from layercake.dateutils import now, ttl
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
@@ -42,9 +39,11 @@ private_key = read_file_path('private.pem')
|
||||
private_jwk = JsonWebKey.import_key(private_key)
|
||||
|
||||
|
||||
# https://docs.authlib.org/en/v0.12/specs/rfc6750.html#authlib.oauth2.rfc6750.BearerToken.GRANT_TYPES_EXPIRES_IN
|
||||
GRANT_TYPES_EXPIRES_IN = {
|
||||
'authorization_code': 60 * 3, # 3 minutes
|
||||
'refresh_token': 3600, # 1 hour
|
||||
'refresh_token': 60 * 3, # 3 minutes
|
||||
# 'refresh_token': 3600, # 1 hour
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +82,17 @@ class OpenIDCode(OpenIDCode_):
|
||||
|
||||
|
||||
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
||||
"""The authorization code grant type is used to obtain both access tokens and
|
||||
refresh tokens and is optimized for confidential clients.
|
||||
|
||||
Since this is a redirection-based flow, the client must be capable of
|
||||
interacting with the resource owner’s user-agent (typically a web browser)
|
||||
and capable of receiving incoming requests (via redirection) from the
|
||||
authorization server:
|
||||
|
||||
- https://docs.authlib.org/en/v0.12/specs/rfc6749.html#authlib.oauth2.rfc6749.grants.AuthorizationCodeGrant
|
||||
"""
|
||||
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = [
|
||||
'client_secret_basic',
|
||||
'client_secret_post',
|
||||
@@ -206,11 +216,6 @@ class TokenExchangeGrant(grants.BaseGrant):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class RefreshTokenNotFoundError(NotFoundError):
|
||||
def __init__(self, *_):
|
||||
super().__init__('Refresh token not found')
|
||||
|
||||
|
||||
class RefreshTokenGrant(grants.RefreshTokenGrant):
|
||||
TOKEN_ENDPOINT_AUTH_METHODS = [
|
||||
'client_secret_basic',
|
||||
@@ -221,17 +226,22 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
|
||||
# The authorization server MAY issue a new refresh token
|
||||
INCLUDE_NEW_REFRESH_TOKEN = True
|
||||
|
||||
def authenticate_refresh_token(self, refresh_token: str, **kwargs) -> TokenMixin:
|
||||
def authenticate_refresh_token(self, refresh_token: str) -> TokenMixin:
|
||||
"""Get token information with refresh_token string."""
|
||||
token = dyn.collection.get_item(
|
||||
KeyPair(
|
||||
pk='OAUTH2#TOKEN',
|
||||
sk=f'REFRESH_TOKEN#{refresh_token}',
|
||||
),
|
||||
exc_cls=RefreshTokenNotFoundError,
|
||||
raise_on_error=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
logger.info('Refresh token retrieved', token=token)
|
||||
if not token:
|
||||
logger.info('Refresh token not found ', refresh_token=token)
|
||||
return None # type: ignore
|
||||
|
||||
logger.info('Refresh token retrieved', refresh_token=token)
|
||||
|
||||
return OAuth2Token(
|
||||
expires_in=int(token['expires_in']),
|
||||
@@ -249,13 +259,14 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
|
||||
issuing a new refresh token to the client."""
|
||||
|
||||
token = getattr(refresh_token, 'refresh_token', None)
|
||||
logger.debug('Revoking old refresh token', refresh_token=token)
|
||||
user = refresh_token.get_user()
|
||||
|
||||
if not token:
|
||||
return None
|
||||
|
||||
with dyn.transact_writer() as transact:
|
||||
logger.debug('Revoking old refresh token', refresh_token=token)
|
||||
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk='OAUTH2#TOKEN',
|
||||
@@ -276,24 +287,24 @@ class RevocationEndpoint(rfc7009.RevocationEndpoint):
|
||||
token_string: str,
|
||||
token_type_hint: str | None = None,
|
||||
):
|
||||
t = dyn.collection.get_items(
|
||||
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 t:
|
||||
if not result:
|
||||
return None
|
||||
|
||||
logger.debug('Tokens retrieved', result=t)
|
||||
logger.debug('Tokens retrieved', result=result)
|
||||
|
||||
if not token_type_hint:
|
||||
token_type_hint = (
|
||||
'refresh_token' if 'refresh_token' in t else 'access_token'
|
||||
'refresh_token' if 'refresh_token' in result else 'access_token'
|
||||
)
|
||||
|
||||
token = t[token_type_hint]
|
||||
token = result[token_type_hint]
|
||||
|
||||
return OAuth2Token(
|
||||
expires_in=int(token['expires_in']),
|
||||
@@ -319,24 +330,25 @@ class RevocationEndpoint(rfc7009.RevocationEndpoint):
|
||||
- https://docs.authlib.org/en/latest/specs/rfc7009.html#authlib.oauth2.rfc7009.RevocationEndpoint.revoke_token
|
||||
"""
|
||||
user_id = token.user['id']
|
||||
r = dyn.collection.query(KeyPair(pk=user_id, sk='SESSION'))
|
||||
result = 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('#')
|
||||
for item in result['items']:
|
||||
pk, sk = item['id'], item['sk']
|
||||
# Key pattern: SESSION#{}#{}
|
||||
*_, kind, token_ = sk.split('#')
|
||||
|
||||
transact.delete(key=KeyPair(pk, sk))
|
||||
transact.delete(
|
||||
key=KeyPair(
|
||||
pk='SESSION',
|
||||
sk=idx,
|
||||
sk=token_,
|
||||
)
|
||||
if kind == 'SESSION'
|
||||
else KeyPair(
|
||||
pk='OAUTH2#TOKEN',
|
||||
sk=f'{kind}#{idx}',
|
||||
sk=f'{kind}#{token_}',
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,4 @@ router = Router()
|
||||
|
||||
@router.post('/token')
|
||||
def issue_token():
|
||||
response = server.create_token_response(router.current_event)
|
||||
logger.debug(getattr(response, 'body', None))
|
||||
return response
|
||||
return server.create_token_response(router.current_event)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import pprint
|
||||
from base64 import b64encode
|
||||
from http import HTTPMethod, HTTPStatus
|
||||
from urllib.parse import urlencode
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import pprint
|
||||
from base64 import b64encode
|
||||
from http import HTTPMethod, HTTPStatus
|
||||
from urllib.parse import urlencode
|
||||
@@ -125,3 +126,32 @@ def test_refresh_token(
|
||||
)
|
||||
|
||||
assert len(r['items']) == 2
|
||||
|
||||
|
||||
def test_refresh_token_invalid_grant(
|
||||
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={
|
||||
'Authorization': f'Basic {AUTH}',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body=urlencode(
|
||||
{
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': '1234',
|
||||
'client_id': CLIENT_ID,
|
||||
}
|
||||
),
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
|
||||
assert r['statusCode'] == 400
|
||||
|
||||
2
id.saladeaula.digital/uv.lock
generated
2
id.saladeaula.digital/uv.lock
generated
@@ -507,7 +507,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = { directory = "../layercake" }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
|
||||
Reference in New Issue
Block a user