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
0a9c2bae63
commit
a46c62a898
@ -6,4 +6,4 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
|
||||
|
||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
&& apt-get -y install --no-install-recommends python3-venv python-is-python3 micro \
|
||||
unzip zip build-essential python3-dev redis-tools
|
||||
unzip zip build-essential python3-dev redis-tools
|
||||
|
@ -19,4 +19,4 @@ docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:lat
|
||||
|
||||
printf "Installing dependencies...\n"
|
||||
|
||||
poetry install --all-extras
|
||||
poetry install --all-extras
|
||||
|
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -3,4 +3,4 @@ updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "daily"
|
||||
|
2
.github/workflows/mypy.yml
vendored
2
.github/workflows/mypy.yml
vendored
@ -23,4 +23,4 @@ jobs:
|
||||
with:
|
||||
checkName: 'mypy'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -153,4 +153,4 @@ cython_debug/
|
||||
|
||||
# PROJECT SPECIFIC
|
||||
setup_env.sh
|
||||
admin_info.json
|
||||
admin_info.json
|
||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"python.analysis.typeCheckingMode": "off"
|
||||
}
|
||||
}
|
||||
|
@ -20,4 +20,4 @@ RUN apt update && \
|
||||
apt-get install build-essential libffi-dev -y \
|
||||
&& pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
CMD [ "python3", "./run.py" ]
|
||||
CMD [ "python3", "./run.py" ]
|
||||
|
@ -8,4 +8,4 @@
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, please open an Issue in our issue tracker here on GitHub.
|
||||
To report a vulnerability, please open an Issue in our issue tracker here on GitHub.
|
||||
|
@ -99,4 +99,4 @@ class Announcements:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
@ -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
|
||||
|
||||
@ -348,4 +327,4 @@ class Clients:
|
||||
await self.UserLogger.log("CREATE_ADMIN", e)
|
||||
raise e
|
||||
|
||||
return created
|
||||
return created
|
||||
|
@ -186,4 +186,4 @@ class Releases:
|
||||
|
||||
return payload
|
||||
else:
|
||||
raise Exception("Invalid organization.")
|
||||
raise Exception("Invalid organization.")
|
||||
|
13
app/main.py
13
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)
|
||||
@ -155,4 +166,4 @@ async def startup() -> None:
|
||||
FastAPICache.init(RedisBackend(RedisConnector.connect(config['cache']['database'])),
|
||||
prefix="fastapi-cache")
|
||||
|
||||
return None
|
||||
return None
|
||||
|
@ -43,4 +43,4 @@ class AnnouncementDeleted(BaseModel):
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
|
||||
deleted: bool
|
||||
deleted: bool
|
||||
|
@ -21,4 +21,3 @@ class ClientAuthModel(BaseModel):
|
||||
|
||||
id: str
|
||||
secret: str
|
||||
|
||||
|
@ -68,4 +68,4 @@ class MirrorAlreadyExistsError(BaseModel):
|
||||
"""
|
||||
|
||||
error: str = "Conflict"
|
||||
message: str = "A mirror already exists for the organization, repository, and version provided. Please use the PUT method to update the mirror."
|
||||
message: str = "A mirror already exists for the organization, repository, and version provided. Please use the PUT method to update the mirror."
|
||||
|
@ -49,4 +49,4 @@ class MirrorDeletedResponseModel(BaseModel):
|
||||
BaseModel (pydantic.BaseModel): BaseModel from pydantic
|
||||
"""
|
||||
deleted: bool
|
||||
key: str
|
||||
key: str
|
||||
|
@ -74,4 +74,4 @@ class ChangelogsResponseFields(BaseModel):
|
||||
sha: str
|
||||
author: str
|
||||
message: str
|
||||
html_url: str
|
||||
html_url: str
|
||||
|
@ -97,4 +97,13 @@ class ChangelogsResponseModel(BaseModel):
|
||||
|
||||
repository: 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.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:
|
||||
@ -45,4 +48,17 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"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
|
||||
@ -169,4 +170,4 @@ async def client_status(request: Request, response: Response, client_id: str, ac
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -18,4 +18,4 @@ async def contributors(request: Request, response: Response) -> dict:
|
||||
Returns:
|
||||
json: list of contributors
|
||||
"""
|
||||
return await releases.get_contributors(config['app']['repositories'])
|
||||
return await releases.get_contributors(config['app']['repositories'])
|
||||
|
@ -136,4 +136,4 @@ async def delete_mirror(request: Request, response: Response, org: str, repo: st
|
||||
"error": GeneralErrors.Unauthorized().error,
|
||||
"message": GeneralErrors.Unauthorized().message
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -9,4 +9,4 @@ async def ping(request: Request, response: Response) -> None:
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
return None
|
||||
return None
|
||||
|
@ -27,4 +27,4 @@ class Generators:
|
||||
Returns:
|
||||
int: A timestamp
|
||||
"""
|
||||
return int(time.time())
|
||||
return int(time.time())
|
||||
|
@ -29,4 +29,4 @@ class HTTPXClient:
|
||||
}
|
||||
)
|
||||
|
||||
return httpx_client
|
||||
return httpx_client
|
||||
|
@ -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")
|
||||
|
@ -20,4 +20,4 @@ class RedisConnector:
|
||||
def connect(database: str) -> aioredis.Redis:
|
||||
"""Connect to Redis"""
|
||||
redis_url = f"{redis_config['url']}:{redis_config['port']}/{database}"
|
||||
return aioredis.from_url(redis_url, encoding="utf-8", decode_responses=True)
|
||||
return aioredis.from_url(redis_url, encoding="utf-8", decode_responses=True)
|
||||
|
@ -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]
|
||||
|
||||
|
@ -30,4 +30,4 @@ services:
|
||||
|
||||
networks:
|
||||
infra:
|
||||
external: true
|
||||
external: true
|
||||
|
@ -31,4 +31,4 @@ services:
|
||||
|
||||
networks:
|
||||
infra:
|
||||
external: true
|
||||
external: true
|
||||
|
4
mypy.ini
4
mypy.ini
@ -71,4 +71,8 @@ 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