fix: fix token revogation

This commit is contained in:
Alexandre Teles 2022-10-17 16:57:29 -03:00
parent 07800c4d62
commit 2d3e62addf
33 changed files with 101 additions and 80 deletions

View File

@ -1,10 +1,13 @@
from datetime import timedelta
import os
import toml
from datetime import timedelta
from pydantic import BaseModel
from fastapi_paseto_auth import AuthPASETO
config: dict = toml.load("config.toml")
class PasetoSettings(BaseModel):
authpaseto_secret_key: str = os.environ['SECRET_KEY']
authpaseto_access_token_expires: int | bool = config['auth']['access_token_expires']
authpaseto_denylist_enabled: bool = True

View File

@ -266,7 +266,13 @@ class Clients:
banned: bool = False
try:
await self.redis_tokens.set(token, '')
if type(config['auth']['access_token_expires']) is bool:
await self.redis_tokens.set(name=token, value="", nx=True)
else:
await self.redis_tokens.set(name=token,
value="",
nx=True,
ex=config['auth']['access_token_expires'])
await self.UserLogger.log("BAN_TOKEN", None, token)
banned = True
except aioredis.RedisError as e:
@ -275,52 +281,25 @@ class Clients:
return banned
async def is_token_banned(self, token: str) -> bool:
"""Check if a token is banned
Args:
token (str): Token to check
Returns:
bool: True if the token is banned, False otherwise
"""
banned: bool = True
try:
banned = await self.redis_tokens.exists(token)
await self.UserLogger.log("CHECK_TOKEN", None, token)
except aioredis.RedisError as e:
await self.UserLogger.log("CHECK_TOKEN", e)
raise e
return banned
async def auth_checks(self, client_id: str, token: str) -> bool:
"""Check if a client exists, is active and the token isn't banned
Args:
client_id (str): UUID of the client
secret (str): Secret of the client
token (str): Token JTI
Returns:
bool: True if the client exists, is active
and the token isn't banned, False otherwise
"""
if await self.exists(client_id):
if await self.is_active(client_id):
if not await self.is_token_banned(token):
return True
else:
return False
else:
if not await self.is_token_banned(token):
await self.ban_token(token)
return False
if await self.exists(client_id) and await self.is_active(client_id):
return True
else:
await self.ban_token(token)
return False
if not await self.redis_tokens.exists(token):
await self.ban_token(token)
return False
return False

View File

@ -3,6 +3,7 @@
import os
import toml
import binascii
from redis import Redis
from fastapi import FastAPI, Request, status
from fastapi.responses import JSONResponse, UJSONResponse
@ -21,6 +22,7 @@ from fastapi_paseto_auth.exceptions import AuthPASETOException
import app.controllers.Auth as Auth
from app.controllers.Clients import Clients
from app.utils.RedisConnector import RedisConnector
import app.models.GeneralErrors as GeneralErrors
@ -100,6 +102,15 @@ def get_config() -> Auth.PasetoSettings:
"""
return Auth.PasetoSettings()
@AuthPASETO.token_in_denylist_loader
def check_if_token_in_denylist(decrypted_token):
redis = Redis(host=os.environ['REDIS_URL'],
port=os.environ['REDIS_PORT'],
db=config['tokens']['database'],
decode_responses=True)
return redis.exists(decrypted_token["jti"])
# Setup custom error handlers
@app.exception_handler(AuthPASETOException)

View File

@ -21,4 +21,3 @@ class ClientAuthModel(BaseModel):
id: str
secret: str

View File

@ -98,3 +98,12 @@ class ChangelogsResponseModel(BaseModel):
repository: str
path: str
commits: list[ ResponseFields.ChangelogsResponseFields ]
class RevokedTokenResponse(BaseModel):
"""Implements the response fields for token invalidation.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
revoked: bool

View File

@ -6,11 +6,14 @@ import app.models.ClientModels as ClientModels
import app.models.GeneralErrors as GeneralErrors
import app.models.ResponseModels as ResponseModels
router = APIRouter()
router = APIRouter(
prefix="/auth",
tags=['Authentication']
)
clients = Clients()
config: dict = load_config()
@router.post('/auth', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK, tags=['Authentication'])
@router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
async def auth(request: Request, response: Response, client: ClientModels.ClientAuthModel, Authorize: AuthPASETO = Depends()) -> dict:
"""Authenticate a client and get an auth token.
@ -20,7 +23,7 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
admin_claim: dict[str, bool]
if await clients.exists(client.id):
if await clients.exists(client.id) and await clients.is_active(client.id):
authenticated: bool = await clients.authenticate(client.id, client.secret)
if not authenticated:
@ -46,3 +49,16 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
"message": GeneralErrors.Unauthorized().message
}
)
@router.delete("/revoke", response_model=ResponseModels.RevokedTokenResponse, status_code=status.HTTP_200_OK)
async def revoke_token(request: Request, response: Response, Authorize: AuthPASETO = Depends()):
Authorize.paseto_required()
if await clients.ban_token(Authorize.get_jti()):
return {"revoked": True}
else:
raise HTTPException(status_code=500, detail={
"error": GeneralErrors.InternalServerError().error,
"message": GeneralErrors.InternalServerError().message
}
)

View File

@ -128,7 +128,7 @@ async def update_client(request: Request, response: Response, client_id: str, Au
}
)
@router.patch('/client/{client_id}/status', response_model=ResponseModels.ClientStatusResponse, status_code=status.HTTP_200_OK)
@router.patch('/{client_id}/status', response_model=ResponseModels.ClientStatusResponse, status_code=status.HTTP_200_OK)
async def client_status(request: Request, response: Response, client_id: str, active: bool, Authorize: AuthPASETO = Depends()) -> dict:
"""Activate or deactivate a client
@ -144,11 +144,11 @@ async def client_status(request: Request, response: Response, client_id: str, ac
if 'admin' in Authorize.get_token_payload():
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
print("admin claim: ", admin_claim)
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
( admin_claim['admin'] == True or
current_user == client_id ) ):
print("client exists")
if await clients.exists(client_id):
if await clients.status(client_id, active):
return {"id": client_id, "active": active}
@ -159,6 +159,7 @@ async def client_status(request: Request, response: Response, client_id: str, ac
}
)
else:
print("Client does not exist")
raise HTTPException(status_code=404, detail={
"error": GeneralErrors.ClientNotFound().error,
"message": GeneralErrors.ClientNotFound().message

View File

@ -60,9 +60,9 @@ class AnnouncementsLogger:
key (str): Key used in the operation
"""
if type(result) is RedisError:
logger.error(f"[User] REDIS {operation} - Failed with error: {result}")
logger.error(f"[ANNOUNCEMENT] REDIS {operation} - Failed with error: {result}")
else:
logger.info(f"[User] REDIS {operation} {key} - OK")
logger.info(f"[ANNOUNCEMENT] REDIS {operation} {key} - OK")
class MirrorsLogger:
async def log(self, operation: str, result: RedisError | None = None, key: str = "") -> None:
@ -73,6 +73,6 @@ class MirrorsLogger:
key (str): Key used in the operation
"""
if type(result) is RedisError:
logger.error(f"[User] REDIS {operation} - Failed with error: {result}")
logger.error(f"[MIRRORS] REDIS {operation} - Failed with error: {result}")
else:
logger.info(f"[User] REDIS {operation} {key} - OK")
logger.info(f"[MIRRORS] REDIS {operation} {key} - OK")

View File

@ -13,17 +13,16 @@ description = """
* Rate Limiting - 60 requests per minute
* Cache - 5 minutes
* Token duration - 1 hour
* Token refresh - 30 days
* Token duration - 1 year
### Additional Notes
1. Breaking changes are to be expected
2. Client side caching is adviced to avoid unnecessary requests
2. Client side caching is advised to avoid unnecessary requests
3. Abuse of the API will result in IP blocks
"""
version = "0.9 RC2"
version = "1.0.0"
[license]

View File

@ -72,3 +72,7 @@ ignore_missing_imports = True
[mypy-gunicorn.*]
# No stubs available
ignore_missing_imports = True
[mypy-asgiref.*]
# No stubs available
ignore_missing_imports = True

6
poetry.lock generated
View File

@ -723,7 +723,7 @@ python-versions = ">=3.5"
[[package]]
name = "types-redis"
version = "4.3.21.1"
version = "4.3.21.2"
description = "Typing stubs for redis"
category = "dev"
optional = false
@ -1478,8 +1478,8 @@ toolz = [
{file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"},
]
types-redis = [
{file = "types-redis-4.3.21.1.tar.gz", hash = "sha256:493814829643fc04a14595eda6ccd69bdc0606477541ccda54238ce3f60bc993"},
{file = "types_redis-4.3.21.1-py3-none-any.whl", hash = "sha256:65b8c842f406932218f8ce636f75e5a03cb6b382d3922cb3e5f87e127e6d434d"},
{file = "types-redis-4.3.21.2.tar.gz", hash = "sha256:ab542249a47d3903b94162e6395ae6be0cc0f562fd184ed51a0a34d8a7021a39"},
{file = "types_redis-4.3.21.2-py3-none-any.whl", hash = "sha256:eda5fd9e80f453143902db5547ca6506a3af44476ad080db7d18840da532ab19"},
]
types-toml = [
{file = "types-toml-0.10.8.tar.gz", hash = "sha256:b7e7ea572308b1030dc86c3ba825c5210814c2825612ec679eb7814f8dd9295a"},