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 os
|
||||||
import toml
|
import toml
|
||||||
|
from datetime import timedelta
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from fastapi_paseto_auth import AuthPASETO
|
||||||
|
|
||||||
config: dict = toml.load("config.toml")
|
config: dict = toml.load("config.toml")
|
||||||
|
|
||||||
class PasetoSettings(BaseModel):
|
class PasetoSettings(BaseModel):
|
||||||
authpaseto_secret_key: str = os.environ['SECRET_KEY']
|
authpaseto_secret_key: str = os.environ['SECRET_KEY']
|
||||||
authpaseto_access_token_expires: int | bool = config['auth']['access_token_expires']
|
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
|
banned: bool = False
|
||||||
|
|
||||||
try:
|
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)
|
await self.UserLogger.log("BAN_TOKEN", None, token)
|
||||||
banned = True
|
banned = True
|
||||||
except aioredis.RedisError as e:
|
except aioredis.RedisError as e:
|
||||||
@ -275,52 +281,25 @@ class Clients:
|
|||||||
|
|
||||||
return banned
|
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:
|
async def auth_checks(self, client_id: str, token: str) -> bool:
|
||||||
"""Check if a client exists, is active and the token isn't banned
|
"""Check if a client exists, is active and the token isn't banned
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
client_id (str): UUID of the client
|
client_id (str): UUID of the client
|
||||||
secret (str): Secret of the client
|
secret (str): Secret of the client
|
||||||
|
token (str): Token JTI
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if the client exists, is active
|
bool: True if the client exists, is active
|
||||||
and the token isn't banned, False otherwise
|
and the token isn't banned, False otherwise
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if await self.exists(client_id):
|
if await self.exists(client_id) and await self.is_active(client_id):
|
||||||
if await self.is_active(client_id):
|
return True
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
await self.ban_token(token)
|
if not await self.redis_tokens.exists(token):
|
||||||
return False
|
await self.ban_token(token)
|
||||||
|
return False
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
11
app/main.py
11
app/main.py
@ -3,6 +3,7 @@
|
|||||||
import os
|
import os
|
||||||
import toml
|
import toml
|
||||||
import binascii
|
import binascii
|
||||||
|
from redis import Redis
|
||||||
|
|
||||||
from fastapi import FastAPI, Request, status
|
from fastapi import FastAPI, Request, status
|
||||||
from fastapi.responses import JSONResponse, UJSONResponse
|
from fastapi.responses import JSONResponse, UJSONResponse
|
||||||
@ -21,6 +22,7 @@ from fastapi_paseto_auth.exceptions import AuthPASETOException
|
|||||||
|
|
||||||
import app.controllers.Auth as Auth
|
import app.controllers.Auth as Auth
|
||||||
from app.controllers.Clients import Clients
|
from app.controllers.Clients import Clients
|
||||||
|
|
||||||
from app.utils.RedisConnector import RedisConnector
|
from app.utils.RedisConnector import RedisConnector
|
||||||
|
|
||||||
import app.models.GeneralErrors as GeneralErrors
|
import app.models.GeneralErrors as GeneralErrors
|
||||||
@ -100,6 +102,15 @@ def get_config() -> Auth.PasetoSettings:
|
|||||||
"""
|
"""
|
||||||
return 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
|
# Setup custom error handlers
|
||||||
|
|
||||||
@app.exception_handler(AuthPASETOException)
|
@app.exception_handler(AuthPASETOException)
|
||||||
|
@ -21,4 +21,3 @@ class ClientAuthModel(BaseModel):
|
|||||||
|
|
||||||
id: str
|
id: str
|
||||||
secret: str
|
secret: str
|
||||||
|
|
||||||
|
@ -98,3 +98,12 @@ class ChangelogsResponseModel(BaseModel):
|
|||||||
repository: str
|
repository: str
|
||||||
path: str
|
path: str
|
||||||
commits: list[ ResponseFields.ChangelogsResponseFields ]
|
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.GeneralErrors as GeneralErrors
|
||||||
import app.models.ResponseModels as ResponseModels
|
import app.models.ResponseModels as ResponseModels
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter(
|
||||||
|
prefix="/auth",
|
||||||
|
tags=['Authentication']
|
||||||
|
)
|
||||||
clients = Clients()
|
clients = Clients()
|
||||||
config: dict = load_config()
|
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:
|
async def auth(request: Request, response: Response, client: ClientModels.ClientAuthModel, Authorize: AuthPASETO = Depends()) -> dict:
|
||||||
"""Authenticate a client and get an auth token.
|
"""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]
|
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)
|
authenticated: bool = await clients.authenticate(client.id, client.secret)
|
||||||
|
|
||||||
if not authenticated:
|
if not authenticated:
|
||||||
@ -46,3 +49,16 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
|
|||||||
"message": GeneralErrors.Unauthorized().message
|
"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:
|
async def client_status(request: Request, response: Response, client_id: str, active: bool, Authorize: AuthPASETO = Depends()) -> dict:
|
||||||
"""Activate or deactivate a client
|
"""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():
|
if 'admin' in Authorize.get_token_payload():
|
||||||
admin_claim = {"admin": Authorize.get_token_payload()['admin']}
|
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
|
if ( await clients.auth_checks(Authorize.get_subject(), Authorize.get_jti()) and
|
||||||
( admin_claim['admin'] == True or
|
( admin_claim['admin'] == True or
|
||||||
current_user == client_id ) ):
|
current_user == client_id ) ):
|
||||||
|
print("client exists")
|
||||||
if await clients.exists(client_id):
|
if await clients.exists(client_id):
|
||||||
if await clients.status(client_id, active):
|
if await clients.status(client_id, active):
|
||||||
return {"id": client_id, "active": active}
|
return {"id": client_id, "active": active}
|
||||||
@ -159,6 +159,7 @@ async def client_status(request: Request, response: Response, client_id: str, ac
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
print("Client does not exist")
|
||||||
raise HTTPException(status_code=404, detail={
|
raise HTTPException(status_code=404, detail={
|
||||||
"error": GeneralErrors.ClientNotFound().error,
|
"error": GeneralErrors.ClientNotFound().error,
|
||||||
"message": GeneralErrors.ClientNotFound().message
|
"message": GeneralErrors.ClientNotFound().message
|
||||||
|
@ -60,9 +60,9 @@ class AnnouncementsLogger:
|
|||||||
key (str): Key used in the operation
|
key (str): Key used in the operation
|
||||||
"""
|
"""
|
||||||
if type(result) is RedisError:
|
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:
|
else:
|
||||||
logger.info(f"[User] REDIS {operation} {key} - OK")
|
logger.info(f"[ANNOUNCEMENT] REDIS {operation} {key} - OK")
|
||||||
|
|
||||||
class MirrorsLogger:
|
class MirrorsLogger:
|
||||||
async def log(self, operation: str, result: RedisError | None = None, key: str = "") -> None:
|
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
|
key (str): Key used in the operation
|
||||||
"""
|
"""
|
||||||
if type(result) is RedisError:
|
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:
|
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
|
* Rate Limiting - 60 requests per minute
|
||||||
* Cache - 5 minutes
|
* Cache - 5 minutes
|
||||||
* Token duration - 1 hour
|
* Token duration - 1 year
|
||||||
* Token refresh - 30 days
|
|
||||||
|
|
||||||
### Additional Notes
|
### Additional Notes
|
||||||
|
|
||||||
1. Breaking changes are to be expected
|
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
|
3. Abuse of the API will result in IP blocks
|
||||||
|
|
||||||
"""
|
"""
|
||||||
version = "0.9 RC2"
|
version = "1.0.0"
|
||||||
|
|
||||||
[license]
|
[license]
|
||||||
|
|
||||||
|
4
mypy.ini
4
mypy.ini
@ -72,3 +72,7 @@ ignore_missing_imports = True
|
|||||||
[mypy-gunicorn.*]
|
[mypy-gunicorn.*]
|
||||||
# No stubs available
|
# No stubs available
|
||||||
ignore_missing_imports = True
|
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]]
|
[[package]]
|
||||||
name = "types-redis"
|
name = "types-redis"
|
||||||
version = "4.3.21.1"
|
version = "4.3.21.2"
|
||||||
description = "Typing stubs for redis"
|
description = "Typing stubs for redis"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@ -1478,8 +1478,8 @@ toolz = [
|
|||||||
{file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"},
|
{file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"},
|
||||||
]
|
]
|
||||||
types-redis = [
|
types-redis = [
|
||||||
{file = "types-redis-4.3.21.1.tar.gz", hash = "sha256:493814829643fc04a14595eda6ccd69bdc0606477541ccda54238ce3f60bc993"},
|
{file = "types-redis-4.3.21.2.tar.gz", hash = "sha256:ab542249a47d3903b94162e6395ae6be0cc0f562fd184ed51a0a34d8a7021a39"},
|
||||||
{file = "types_redis-4.3.21.1-py3-none-any.whl", hash = "sha256:65b8c842f406932218f8ce636f75e5a03cb6b382d3922cb3e5f87e127e6d434d"},
|
{file = "types_redis-4.3.21.2-py3-none-any.whl", hash = "sha256:eda5fd9e80f453143902db5547ca6506a3af44476ad080db7d18840da532ab19"},
|
||||||
]
|
]
|
||||||
types-toml = [
|
types-toml = [
|
||||||
{file = "types-toml-0.10.8.tar.gz", hash = "sha256:b7e7ea572308b1030dc86c3ba825c5210814c2825612ec679eb7814f8dd9295a"},
|
{file = "types-toml-0.10.8.tar.gz", hash = "sha256:b7e7ea572308b1030dc86c3ba825c5210814c2825612ec679eb7814f8dd9295a"},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user