mirror of
https://github.com/revanced/revanced-releases-api.git
synced 2025-04-29 22:14:28 +02:00
fix: fix token revogation
This commit is contained in:
parent
07800c4d62
commit
2d3e62addf
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
11
app/main.py
11
app/main.py
@ -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)
|
||||
|
@ -21,4 +21,3 @@ class ClientAuthModel(BaseModel):
|
||||
|
||||
id: str
|
||||
secret: str
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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]
|
||||
|
||||
|
4
mypy.ini
4
mypy.ini
@ -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
6
poetry.lock
generated
@ -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"},
|
||||
|
Loading…
x
Reference in New Issue
Block a user