From 07bab9e668faffaa1cfb34765d88ff09e306d906 Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Mon, 14 Apr 2025 21:35:14 +0100 Subject: [PATCH 1/4] auth middleware for header authencation --- .env.example | 6 +- src/api/middlewares/GateWayMiddleware.py | 131 +++++++++++++++++++++++ src/api/routes/User.py | 13 +-- src/api/routes/__init__.py | 64 +++++++++-- src/config/apps.py | 1 + src/config/settings.py | 5 +- src/config/templates.py | 9 +- src/env.py | 11 +- uv.lock | 2 +- 9 files changed, 217 insertions(+), 25 deletions(-) create mode 100644 src/api/middlewares/GateWayMiddleware.py diff --git a/.env.example b/.env.example index 36ca934..f97a6d5 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ SECRET_KEY=your_secret_key DEBUG=True +ENVIRONMENT=xxxx ALLOWED_HOSTS=* LOG_LEVEL=debug @@ -26,4 +27,7 @@ JWT_SECRET=xxx JWT_ISSUER=xxx # time in minutes -OTP_LIFETIME=10 \ No newline at end of file +OTP_LIFETIME=10 + +API_GATEWAY_PUBLIC_KEY=xxxxxxxxxxxxxxxxxxxxxxxx +API_KEY_EXPIRES_AT=2 \ No newline at end of file diff --git a/src/api/middlewares/GateWayMiddleware.py b/src/api/middlewares/GateWayMiddleware.py new file mode 100644 index 0000000..f6fd175 --- /dev/null +++ b/src/api/middlewares/GateWayMiddleware.py @@ -0,0 +1,131 @@ +import base64 +import hashlib +import hmac +from datetime import datetime +from http import HTTPStatus + +from django.http import HttpRequest +from ninja.errors import HttpError +from ninja.openapi.schema import OpenAPISchema +from ninja.security import APIKeyHeader +from src.env import api_gateway +from src.utils.logger import Logger + + +class GateWayAuth(APIKeyHeader): + def __init__(self, logger: Logger) -> None: + self.logger = logger + # self.param_name = 'X-API-GATEWAY-KEY' + super().__init__() + + def authenticate(self, request: HttpRequest, api_key: str): + api_key = request.headers.get('X-API-GATEWAY-KEY') + api_timestamp = request.headers.get('X-API-GATEWAY-TIMESTAMP') + api_signature = request.headers.get('X-API-GATEWAY-SIGNATURE') + user_id = request.headers.get('X-USER-ID') + user_email = request.headers.get('X-USER-EMAIL') + + if not all([api_key, api_timestamp, api_signature, user_id, user_email]): + message = "Missing required headers!" + self.logger.error( + { + "activity_type": "Authenticate User", + "message": message, + "metadata": {"headers": request.headers}, + } + ) + raise HttpError(HTTPStatus.UNAUTHORIZED, message) + + valid_api_key = api_gateway["key"] + if api_key != valid_api_key: + message = "Invalid API key!" + self.logger.error( + { + "activity_type": "Authenticate User", + "message": message, + "metadata": {"headers": request.headers}, + } + ) + raise HttpError(HTTPStatus.UNAUTHORIZED, message) + + self._verify_signature(valid_api_key, valid_api_key, api_timestamp) + + self.logger.debug( + { + "activity_type": "Authenticate User", + "message": "Successfully authenticated user", + "metadata": { + "user_id": user_id, + "user_email": user_email, + }, + } + ) + setattr(request, "auth_email", user_email) + setattr(request, "auth_id", user_id) + + return api_signature + + + + + def _verify_signature(self, valid_api_key: str, signature: str, timestamp: str) -> bool: + + valid_signature = self.generate_signature(valid_api_key, timestamp) + is_valid = hmac.compare_digest(valid_signature, signature) + + if not is_valid: + message = "Invalid signature!" + self.logger.error( + { + "activity_type": "Authenticate User", + "message": message, + "metadata": {"signature": signature}, + } + ) + raise HttpError(HTTPStatus.UNAUTHORIZED, message) + + + timestamp = int(timestamp) + current_time = datetime.now().timestamp() * 1000 + lifespan = api_gateway['expires_at'] * 60 * 60 * 1000; + + if abs(current_time - timestamp) > lifespan: + message = "Signature expired!" + self.logger.error( + { + "activity_type": "Authenticate User", + "message": message, + "metadata": {"timestamp": timestamp}, + } + ) + raise HttpError(HTTPStatus.UNAUTHORIZED, message) + + return True + + + def generate_signature(self, api_key: str, timestamp: str) -> str: + signature = hmac.new( + key=api_key.encode(), + msg=timestamp.encode(), + digestmod=hashlib.sha256 + ).digest() + return base64.b64encode(signature).decode('utf-8') + + +def get_authentication() -> GateWayAuth: + gateway_auth = GateWayAuth(Logger("Authentication")) + return gateway_auth + +def add_global_headers(schema: OpenAPISchema): + for path in schema["paths"]: + for method in schema["paths"][path]: + operation = schema["paths"][path][method] + if operation.get("security"): + operation["security"] = schema["security"] + return schema + + +authentication = get_authentication() + + + diff --git a/src/api/routes/User.py b/src/api/routes/User.py index f71d1be..99c43e0 100644 --- a/src/api/routes/User.py +++ b/src/api/routes/User.py @@ -1,18 +1,15 @@ from http import HTTPStatus -from ninja import Router from django.http import HttpRequest - -from src.utils.svcs import ADepends +from ninja import Router from src.api.controllers.UserController import UserController from src.api.models.payload.requests.Pin import Pin -from src.api.models.payload.responses.User import UserResponse +from src.api.models.payload.requests.UpdateUserRequest import UpdateUserRequest from src.api.models.payload.responses.ErrorResponse import ( - ErrorResponse, - ServerErrorResponse, -) + ErrorResponse, ServerErrorResponse) from src.api.models.payload.responses.SuccessResponse import SuccessResponse -from src.api.models.payload.requests.UpdateUserRequest import UpdateUserRequest +from src.api.models.payload.responses.User import UserResponse +from src.utils.svcs import ADepends router = Router() diff --git a/src/api/routes/__init__.py b/src/api/routes/__init__.py index 9347391..bc604b2 100644 --- a/src/api/routes/__init__.py +++ b/src/api/routes/__init__.py @@ -1,38 +1,86 @@ -from ninja import NinjaAPI from django.http import HttpRequest - +from ninja import NinjaAPI +# from src.api.middlewares.AppMiddleware import authentication +from src.api.middlewares.GateWayMiddleware import (add_global_headers, + authentication) from src.env import app -from src.api.middlewares.AppMiddleware import authentication api: NinjaAPI = NinjaAPI( version=app["version"], title=app["display_name"], description=app["description"], + auth=authentication, ) +original_get_openapi_schema = api.get_openapi_schema + +def custom_openapi_schema(path_params=None): + schema = original_get_openapi_schema() + + schema['components']['securitySchemes'] = { + "Gateway Key": { + "type": "apiKey", + "in": "header", + "name": "X-API-GATEWAY-KEY", + }, + "API Timestamp": { + "type": "apiKey", + "in": "header", + "name": "X-API-GATEWAY-TIMESTAMP", + }, + "API Signature": { + "type": "apiKey", + "in": "header", + "name": "X-API-GATEWAY-SIGNATURE", + }, + "User ID": { + "type": "apiKey", + "in": "header", + "name": "X-USER-ID", + }, + "User Email": { + "type": "apiKey", + "in": "header", + "name": "X-USER-EMAIL", + } + } + + schema["security"] = [ + { + "Gateway Key": [], + "API Timestamp": [], + "API Signature": [], + "User ID": [], + "User Email": [] + } + ] + + schema = add_global_headers(schema) + return schema + +setattr(api, "get_openapi_schema", custom_openapi_schema) + from src.api.utils import error_handlers # noqa: E402, F401 -@api.get("/") +@api.get("/", auth=None) async def home(request: HttpRequest) -> dict: return {"message": "Hello, World!"} api.add_router( - "/users", "src.api.routes.User.router", auth=authentication, tags=["User"] + "/users", "src.api.routes.User.router", tags=["User"] ) api.add_router( - "/kyc", "src.api.routes.UserKYC.router", auth=authentication, tags=["User KYC"] + "/kyc", "src.api.routes.UserKYC.router", tags=["User KYC"] ) api.add_router( "/next-of-kin", "src.api.routes.UserNOK.router", - auth=authentication, tags=["User NOK"], ) api.add_router( "/withdrawal-accounts", "src.api.routes.WithdrawalAccount.router", - auth=authentication, tags=["User"], ) diff --git a/src/config/apps.py b/src/config/apps.py index 963d990..77bccf2 100644 --- a/src/config/apps.py +++ b/src/config/apps.py @@ -16,6 +16,7 @@ ] INSTALLED_APPS = [ + *CONTRIB_APPS, *PROJECT_APPS, *THIRD_PARTY_APPS, ] diff --git a/src/config/settings.py b/src/config/settings.py index fb8cb6e..a6a2d0a 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -2,10 +2,9 @@ from .apps import INSTALLED_APPS as INSTALLED_APPS from .caches import CACHES as CACHES -from .logger import LOGGING as LOGGING -from .databases import DATABASES as DATABASES from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS -from .templates import TEMPLATES as TEMPLATES +from .databases import DATABASES as DATABASES +from .logger import LOGGING as LOGGING from .middleware import MIDDLEWARE as MIDDLEWARE SECRET_KEY = app["secret_key"] diff --git a/src/config/templates.py b/src/config/templates.py index c6ec87f..4f9e46e 100644 --- a/src/config/templates.py +++ b/src/config/templates.py @@ -1,3 +1,8 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).parent.parent + CONTRIB_CT_PROCESSORS = [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", @@ -6,12 +11,14 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], + "DIRS": [os.path.join(BASE_DIR, "templates")], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', ], }, }, diff --git a/src/env.py b/src/env.py index 4dbdd1f..2bf8db2 100644 --- a/src/env.py +++ b/src/env.py @@ -1,7 +1,7 @@ from typing import TypedDict -from src import __name__, __version__, __description__, __display_name__ -from src.utils.env import get_env_int, get_env_str, get_env_list +from src import __description__, __display_name__, __name__, __version__ +from src.utils.env import get_env_int, get_env_list, get_env_str class Env: @@ -98,6 +98,11 @@ class OTP(TypedDict): "issuer": get_env_str("JWT_ISSUER"), } +api_gateway = { + "key": get_env_str("API_GATEWAY_PUBLIC_KEY"), + "expires_at": get_env_int("API_KEY_EXPIRES_AT"), +} + otp: OTP = {"lifetime": get_env_int("OTP_LIFETIME")} -__all__ = ["app", "cache", "db", "env", "jwt_config", "log", "otp"] +__all__ = ["app", "cache", "db", "env", "jwt_config", "log", "otp", "api_gateway"] diff --git a/uv.lock b/uv.lock index 462b666..0753e19 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.12" [[package]] @@ -89,7 +90,6 @@ wheels = [ [[package]] name = "df-wallet-user-service" -version = "1.0.0" source = { virtual = "." } dependencies = [ { name = "bcrypt" }, From 5ef467cf81049ff0894cbc84b4f19a84f9a8d173 Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Mon, 14 Apr 2025 22:36:03 +0100 Subject: [PATCH 2/4] fixed lint error --- src/api/middlewares/GateWayMiddleware.py | 48 ++++++++++-------------- src/api/routes/User.py | 13 ++++--- src/api/routes/__init__.py | 30 +++++++-------- src/config/apps.py | 1 - src/config/settings.py | 5 ++- src/config/templates.py | 9 +---- src/env.py | 6 +-- 7 files changed, 50 insertions(+), 62 deletions(-) diff --git a/src/api/middlewares/GateWayMiddleware.py b/src/api/middlewares/GateWayMiddleware.py index f6fd175..438b990 100644 --- a/src/api/middlewares/GateWayMiddleware.py +++ b/src/api/middlewares/GateWayMiddleware.py @@ -1,13 +1,14 @@ +import hmac import base64 import hashlib -import hmac -from datetime import datetime from http import HTTPStatus +from datetime import datetime from django.http import HttpRequest from ninja.errors import HttpError -from ninja.openapi.schema import OpenAPISchema from ninja.security import APIKeyHeader +from ninja.openapi.schema import OpenAPISchema + from src.env import api_gateway from src.utils.logger import Logger @@ -15,15 +16,14 @@ class GateWayAuth(APIKeyHeader): def __init__(self, logger: Logger) -> None: self.logger = logger - # self.param_name = 'X-API-GATEWAY-KEY' super().__init__() - def authenticate(self, request: HttpRequest, api_key: str): - api_key = request.headers.get('X-API-GATEWAY-KEY') - api_timestamp = request.headers.get('X-API-GATEWAY-TIMESTAMP') - api_signature = request.headers.get('X-API-GATEWAY-SIGNATURE') - user_id = request.headers.get('X-USER-ID') - user_email = request.headers.get('X-USER-EMAIL') + def authenticate(self, request: HttpRequest, key: str) -> str | None: + api_key = request.headers.get("X-API-GATEWAY-KEY") + api_timestamp = request.headers.get("X-API-GATEWAY-TIMESTAMP") + api_signature = request.headers.get("X-API-GATEWAY-SIGNATURE") + user_id = request.headers.get("X-USER-ID") + user_email = request.headers.get("X-USER-EMAIL") if not all([api_key, api_timestamp, api_signature, user_id, user_email]): message = "Missing required headers!" @@ -35,7 +35,7 @@ def authenticate(self, request: HttpRequest, api_key: str): } ) raise HttpError(HTTPStatus.UNAUTHORIZED, message) - + valid_api_key = api_gateway["key"] if api_key != valid_api_key: message = "Invalid API key!" @@ -47,7 +47,7 @@ def authenticate(self, request: HttpRequest, api_key: str): } ) raise HttpError(HTTPStatus.UNAUTHORIZED, message) - + self._verify_signature(valid_api_key, valid_api_key, api_timestamp) self.logger.debug( @@ -65,11 +65,9 @@ def authenticate(self, request: HttpRequest, api_key: str): return api_signature - - - - def _verify_signature(self, valid_api_key: str, signature: str, timestamp: str) -> bool: - + def _verify_signature( + self, valid_api_key: str, signature: str, timestamp: str + ) -> bool: valid_signature = self.generate_signature(valid_api_key, timestamp) is_valid = hmac.compare_digest(valid_signature, signature) @@ -84,10 +82,9 @@ def _verify_signature(self, valid_api_key: str, signature: str, timestamp: str) ) raise HttpError(HTTPStatus.UNAUTHORIZED, message) - timestamp = int(timestamp) current_time = datetime.now().timestamp() * 1000 - lifespan = api_gateway['expires_at'] * 60 * 60 * 1000; + lifespan = api_gateway["expires_at"] * 60 * 60 * 1000 if abs(current_time - timestamp) > lifespan: message = "Signature expired!" @@ -102,21 +99,19 @@ def _verify_signature(self, valid_api_key: str, signature: str, timestamp: str) return True - def generate_signature(self, api_key: str, timestamp: str) -> str: signature = hmac.new( - key=api_key.encode(), - msg=timestamp.encode(), - digestmod=hashlib.sha256 + key=api_key.encode(), msg=timestamp.encode(), digestmod=hashlib.sha256 ).digest() - return base64.b64encode(signature).decode('utf-8') + return base64.b64encode(signature).decode("utf-8") def get_authentication() -> GateWayAuth: gateway_auth = GateWayAuth(Logger("Authentication")) return gateway_auth -def add_global_headers(schema: OpenAPISchema): + +def add_global_headers(schema: OpenAPISchema) -> OpenAPISchema: for path in schema["paths"]: for method in schema["paths"][path]: operation = schema["paths"][path][method] @@ -126,6 +121,3 @@ def add_global_headers(schema: OpenAPISchema): authentication = get_authentication() - - - diff --git a/src/api/routes/User.py b/src/api/routes/User.py index 99c43e0..f71d1be 100644 --- a/src/api/routes/User.py +++ b/src/api/routes/User.py @@ -1,15 +1,18 @@ from http import HTTPStatus -from django.http import HttpRequest from ninja import Router +from django.http import HttpRequest + +from src.utils.svcs import ADepends from src.api.controllers.UserController import UserController from src.api.models.payload.requests.Pin import Pin -from src.api.models.payload.requests.UpdateUserRequest import UpdateUserRequest +from src.api.models.payload.responses.User import UserResponse from src.api.models.payload.responses.ErrorResponse import ( - ErrorResponse, ServerErrorResponse) + ErrorResponse, + ServerErrorResponse, +) from src.api.models.payload.responses.SuccessResponse import SuccessResponse -from src.api.models.payload.responses.User import UserResponse -from src.utils.svcs import ADepends +from src.api.models.payload.requests.UpdateUserRequest import UpdateUserRequest router = Router() diff --git a/src/api/routes/__init__.py b/src/api/routes/__init__.py index bc604b2..2362a05 100644 --- a/src/api/routes/__init__.py +++ b/src/api/routes/__init__.py @@ -1,9 +1,11 @@ -from django.http import HttpRequest +from typing import _T + from ninja import NinjaAPI -# from src.api.middlewares.AppMiddleware import authentication -from src.api.middlewares.GateWayMiddleware import (add_global_headers, - authentication) +from django.http import HttpRequest +from ninja.openapi.schema import OpenAPISchema + from src.env import app +from src.api.middlewares.GateWayMiddleware import authentication, add_global_headers api: NinjaAPI = NinjaAPI( version=app["version"], @@ -14,16 +16,17 @@ original_get_openapi_schema = api.get_openapi_schema -def custom_openapi_schema(path_params=None): + +def custom_openapi_schema(path_params: _T | None = None) -> OpenAPISchema: schema = original_get_openapi_schema() - schema['components']['securitySchemes'] = { + schema["components"]["securitySchemes"] = { "Gateway Key": { "type": "apiKey", "in": "header", "name": "X-API-GATEWAY-KEY", }, - "API Timestamp": { + "API Timestamp": { "type": "apiKey", "in": "header", "name": "X-API-GATEWAY-TIMESTAMP", @@ -42,7 +45,7 @@ def custom_openapi_schema(path_params=None): "type": "apiKey", "in": "header", "name": "X-USER-EMAIL", - } + }, } schema["security"] = [ @@ -51,13 +54,14 @@ def custom_openapi_schema(path_params=None): "API Timestamp": [], "API Signature": [], "User ID": [], - "User Email": [] + "User Email": [], } ] schema = add_global_headers(schema) return schema + setattr(api, "get_openapi_schema", custom_openapi_schema) from src.api.utils import error_handlers # noqa: E402, F401 @@ -68,12 +72,8 @@ async def home(request: HttpRequest) -> dict: return {"message": "Hello, World!"} -api.add_router( - "/users", "src.api.routes.User.router", tags=["User"] -) -api.add_router( - "/kyc", "src.api.routes.UserKYC.router", tags=["User KYC"] -) +api.add_router("/users", "src.api.routes.User.router", tags=["User"]) +api.add_router("/kyc", "src.api.routes.UserKYC.router", tags=["User KYC"]) api.add_router( "/next-of-kin", "src.api.routes.UserNOK.router", diff --git a/src/config/apps.py b/src/config/apps.py index 77bccf2..963d990 100644 --- a/src/config/apps.py +++ b/src/config/apps.py @@ -16,7 +16,6 @@ ] INSTALLED_APPS = [ - *CONTRIB_APPS, *PROJECT_APPS, *THIRD_PARTY_APPS, ] diff --git a/src/config/settings.py b/src/config/settings.py index a6a2d0a..fb8cb6e 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -2,9 +2,10 @@ from .apps import INSTALLED_APPS as INSTALLED_APPS from .caches import CACHES as CACHES -from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS -from .databases import DATABASES as DATABASES from .logger import LOGGING as LOGGING +from .databases import DATABASES as DATABASES +from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS +from .templates import TEMPLATES as TEMPLATES from .middleware import MIDDLEWARE as MIDDLEWARE SECRET_KEY = app["secret_key"] diff --git a/src/config/templates.py b/src/config/templates.py index 4f9e46e..c6ec87f 100644 --- a/src/config/templates.py +++ b/src/config/templates.py @@ -1,8 +1,3 @@ -import os -from pathlib import Path - -BASE_DIR = Path(__file__).parent.parent - CONTRIB_CT_PROCESSORS = [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", @@ -11,14 +6,12 @@ TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [os.path.join(BASE_DIR, "templates")], + "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', ], }, }, diff --git a/src/env.py b/src/env.py index 2bf8db2..186dad1 100644 --- a/src/env.py +++ b/src/env.py @@ -1,7 +1,7 @@ from typing import TypedDict -from src import __description__, __display_name__, __name__, __version__ -from src.utils.env import get_env_int, get_env_list, get_env_str +from src import __name__, __version__, __description__, __display_name__ +from src.utils.env import get_env_int, get_env_str, get_env_list class Env: @@ -105,4 +105,4 @@ class OTP(TypedDict): otp: OTP = {"lifetime": get_env_int("OTP_LIFETIME")} -__all__ = ["app", "cache", "db", "env", "jwt_config", "log", "otp", "api_gateway"] +__all__ = ["api_gateway", "app", "cache", "db", "env", "jwt_config", "log", "otp"] From 58e49d2edaab7f0ee78043a6df8356e252c1df2e Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Wed, 16 Apr 2025 14:44:45 +0100 Subject: [PATCH 3/4] fixed pyright lint errors --- pyproject.toml | 1 + src/api/middlewares/GateWayMiddleware.py | 46 ++++++++++++------------ src/api/routes/__init__.py | 4 +-- uv.lock | 11 ++++++ 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 599f306..7f3065c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "pydantic[email]>=2.10.6", "django-ninja>=1.3.0", "pyjwt>=2.10.1", + "typing>=3.10.0.0", ] [dependency-groups] diff --git a/src/api/middlewares/GateWayMiddleware.py b/src/api/middlewares/GateWayMiddleware.py index 438b990..9c5a465 100644 --- a/src/api/middlewares/GateWayMiddleware.py +++ b/src/api/middlewares/GateWayMiddleware.py @@ -1,11 +1,9 @@ import hmac -import base64 import hashlib -from http import HTTPStatus -from datetime import datetime +from datetime import datetime, timedelta from django.http import HttpRequest -from ninja.errors import HttpError +from ninja.errors import AuthenticationError from ninja.security import APIKeyHeader from ninja.openapi.schema import OpenAPISchema @@ -18,15 +16,16 @@ def __init__(self, logger: Logger) -> None: self.logger = logger super().__init__() - def authenticate(self, request: HttpRequest, key: str) -> str | None: - api_key = request.headers.get("X-API-GATEWAY-KEY") - api_timestamp = request.headers.get("X-API-GATEWAY-TIMESTAMP") - api_signature = request.headers.get("X-API-GATEWAY-SIGNATURE") - user_id = request.headers.get("X-USER-ID") - user_email = request.headers.get("X-USER-EMAIL") + def authenticate(self, request: HttpRequest, key: str | None ) -> str | None: - if not all([api_key, api_timestamp, api_signature, user_id, user_email]): - message = "Missing required headers!" + try: + api_key = request.headers["X-API-GATEWAY-KEY"] + api_timestamp = request.headers["X-API-GATEWAY-TIMESTAMP"] + api_signature = request.headers["X-API-GATEWAY-SIGNATURE"] + user_id = request.headers["X-USER-ID"] + user_email = request.headers["X-USER-EMAIL"] + except KeyError as e: + message = f"Missing required header: {e}" self.logger.error( { "activity_type": "Authenticate User", @@ -34,7 +33,7 @@ def authenticate(self, request: HttpRequest, key: str) -> str | None: "metadata": {"headers": request.headers}, } ) - raise HttpError(HTTPStatus.UNAUTHORIZED, message) + raise AuthenticationError(message=message) valid_api_key = api_gateway["key"] if api_key != valid_api_key: @@ -46,9 +45,9 @@ def authenticate(self, request: HttpRequest, key: str) -> str | None: "metadata": {"headers": request.headers}, } ) - raise HttpError(HTTPStatus.UNAUTHORIZED, message) + raise AuthenticationError(message=message) - self._verify_signature(valid_api_key, valid_api_key, api_timestamp) + self._verify_signature(valid_api_key, api_signature, api_timestamp) self.logger.debug( { @@ -80,13 +79,12 @@ def _verify_signature( "metadata": {"signature": signature}, } ) - raise HttpError(HTTPStatus.UNAUTHORIZED, message) + raise AuthenticationError(message=message) - timestamp = int(timestamp) - current_time = datetime.now().timestamp() * 1000 - lifespan = api_gateway["expires_at"] * 60 * 60 * 1000 - - if abs(current_time - timestamp) > lifespan: + initial_time = datetime.fromtimestamp(int(timestamp) / 1000) + valid_window = initial_time + timedelta(minutes=api_gateway["expires_at"]) + if valid_window < datetime.now(): + message = "Signature expired!" self.logger.error( { @@ -95,15 +93,15 @@ def _verify_signature( "metadata": {"timestamp": timestamp}, } ) - raise HttpError(HTTPStatus.UNAUTHORIZED, message) + raise AuthenticationError(message=message) return True def generate_signature(self, api_key: str, timestamp: str) -> str: signature = hmac.new( key=api_key.encode(), msg=timestamp.encode(), digestmod=hashlib.sha256 - ).digest() - return base64.b64encode(signature).decode("utf-8") + ).hexdigest() + return signature def get_authentication() -> GateWayAuth: diff --git a/src/api/routes/__init__.py b/src/api/routes/__init__.py index 2362a05..277e494 100644 --- a/src/api/routes/__init__.py +++ b/src/api/routes/__init__.py @@ -1,5 +1,3 @@ -from typing import _T - from ninja import NinjaAPI from django.http import HttpRequest from ninja.openapi.schema import OpenAPISchema @@ -17,7 +15,7 @@ original_get_openapi_schema = api.get_openapi_schema -def custom_openapi_schema(path_params: _T | None = None) -> OpenAPISchema: +def custom_openapi_schema(path_params: dict| None =None) -> OpenAPISchema: schema = original_get_openapi_schema() schema["components"]["securitySchemes"] = { diff --git a/uv.lock b/uv.lock index 0753e19..d0cbf4e 100644 --- a/uv.lock +++ b/uv.lock @@ -106,6 +106,7 @@ dependencies = [ { name = "python-decouple" }, { name = "redis" }, { name = "svcs" }, + { name = "typing" }, ] [package.dev-dependencies] @@ -133,6 +134,7 @@ requires-dist = [ { name = "python-decouple", specifier = ">=3.8" }, { name = "redis", specifier = ">=5.2.1" }, { name = "svcs", specifier = ">=25.1.0" }, + { name = "typing", specifier = ">=3.10.0.0" }, ] [package.metadata.requires-dev] @@ -662,6 +664,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329 }, ] +[[package]] +name = "typing" +version = "3.10.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/1b/835d4431805939d2996f8772aca1d2313a57e8860fec0e48e8e7dfe3a477/typing-3.10.0.0.tar.gz", hash = "sha256:13b4ad211f54ddbf93e5901a9967b1e07720c1d1b78d596ac6a439641aa1b130", size = 78962 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/5d/865e17349564eb1772688d8afc5e3081a5964c640d64d1d2880ebaed002d/typing-3.10.0.0-py3-none-any.whl", hash = "sha256:12fbdfbe7d6cca1a42e485229afcb0b0c8259258cfb919b8a5e2a5c953742f89", size = 26320 }, +] + [[package]] name = "typing-extensions" version = "4.13.0" From c00a3564bcbbeb6f6bd9e3ed454a777582a6fbc2 Mon Sep 17 00:00:00 2001 From: CaptainAril Date: Wed, 16 Apr 2025 14:48:01 +0100 Subject: [PATCH 4/4] ruff format --- src/api/middlewares/GateWayMiddleware.py | 4 +--- src/api/routes/__init__.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/api/middlewares/GateWayMiddleware.py b/src/api/middlewares/GateWayMiddleware.py index 9c5a465..9ce6687 100644 --- a/src/api/middlewares/GateWayMiddleware.py +++ b/src/api/middlewares/GateWayMiddleware.py @@ -16,8 +16,7 @@ def __init__(self, logger: Logger) -> None: self.logger = logger super().__init__() - def authenticate(self, request: HttpRequest, key: str | None ) -> str | None: - + def authenticate(self, request: HttpRequest, key: str | None) -> str | None: try: api_key = request.headers["X-API-GATEWAY-KEY"] api_timestamp = request.headers["X-API-GATEWAY-TIMESTAMP"] @@ -84,7 +83,6 @@ def _verify_signature( initial_time = datetime.fromtimestamp(int(timestamp) / 1000) valid_window = initial_time + timedelta(minutes=api_gateway["expires_at"]) if valid_window < datetime.now(): - message = "Signature expired!" self.logger.error( { diff --git a/src/api/routes/__init__.py b/src/api/routes/__init__.py index 277e494..09f5c9b 100644 --- a/src/api/routes/__init__.py +++ b/src/api/routes/__init__.py @@ -15,7 +15,7 @@ original_get_openapi_schema = api.get_openapi_schema -def custom_openapi_schema(path_params: dict| None =None) -> OpenAPISchema: +def custom_openapi_schema(path_params: dict | None = None) -> OpenAPISchema: schema = original_get_openapi_schema() schema["components"]["securitySchemes"] = {