Skip to content

Commit ffe8ed4

Browse files
committed
gateway and broker auth
1 parent adcc92b commit ffe8ed4

File tree

12 files changed

+250
-63
lines changed

12 files changed

+250
-63
lines changed

src/api/constants/queues.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import TypedDict
2+
3+
4+
class QueueNames(TypedDict):
5+
USER_REGISTRATION: str
6+
EMAIL_VALIDATION: str
7+
8+
9+
QUEUE_NAMES: QueueNames = {
10+
"USER_REGISTRATION": "create_user",
11+
"EMAIL_VALIDATION": "validate_email",
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import TypedDict
2+
3+
4+
class SignatureSources(TypedDict):
5+
gateway: str
6+
queue: str
7+
8+
9+
SIGNATURE_SOURCES: SignatureSources = {
10+
"gateway": "Gateway",
11+
"queue": "Broker Queue",
12+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from types import CoroutineType
2+
from typing import Any
3+
from collections.abc import Callable, Awaitable
4+
5+
from faststream import BaseMiddleware
6+
from faststream.broker.message import StreamMessage
7+
from faststream.rabbit.message import RabbitMessage
8+
9+
from src.env import queue
10+
from src.utils.logger import Logger
11+
from src.api.services.UtilityService import UtilityService
12+
from src.api.constants.signature_sources import SIGNATURE_SOURCES
13+
14+
15+
class PublishMiddleware(BaseMiddleware):
16+
"""
17+
Middleware to handle subscription messages.
18+
"""
19+
20+
async def publish_scope(
21+
self,
22+
call_next: Callable[..., Awaitable[Any]],
23+
msg: RabbitMessage,
24+
*args: tuple[Any, ...],
25+
**kwargs: dict[str, Any],
26+
) -> CoroutineType:
27+
timestamp = UtilityService.get_timestamp()
28+
signature = UtilityService.generate_signature(queue["key"], timestamp)
29+
30+
headers = {}
31+
32+
headers["X-BROKER-SIGNATURE"] = signature
33+
headers["X-BROKER-TIMESTAMP"] = timestamp
34+
headers["X-BROKER-KEY"] = queue["key"]
35+
36+
kwargs["headers"] = headers
37+
return await super().publish_scope(call_next, msg, *args, **kwargs)
38+
39+
40+
class SubscribeMiddleware(BaseMiddleware):
41+
async def consume_scope(
42+
self, call_next: Callable[[Any], Awaitable[Any]], msg: StreamMessage
43+
) -> CoroutineType | None:
44+
logger = Logger(__name__)
45+
try:
46+
UtilityService.verify_signature(
47+
logger=logger,
48+
signature_data={
49+
"signature": msg.headers["X-BROKER-SIGNATURE"],
50+
"timestamp": msg.headers["X-BROKER-TIMESTAMP"],
51+
"key": queue["key"],
52+
"ttl": queue["ttl"],
53+
"title": SIGNATURE_SOURCES["gateway"],
54+
},
55+
)
56+
57+
return await super().consume_scope(call_next, msg)
58+
except KeyError as e:
59+
message = f"Missing required header: {e}"
60+
logger.error(
61+
{
62+
"activity_type": "Authenticate GatewaBroker Queue Request",
63+
"message": message,
64+
"metadata": {"headers": msg.headers},
65+
}
66+
)
67+
except Exception as e:
68+
queue_operation = msg.raw_message.routing_key
69+
message = f"`{queue_operation}` operation failed: {e}"
70+
logger.error(
71+
{
72+
"activity_type": "Authenticate GatewaBroker Queue Request",
73+
"message": message,
74+
"metadata": {"headers": msg.headers, "message": msg._decoded_body},
75+
}
76+
)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from django.http import HttpRequest
2+
from ninja.errors import AuthenticationError
3+
from ninja.security import APIKeyHeader
4+
from ninja.openapi.schema import OpenAPISchema
5+
6+
from src.env import api_gateway
7+
from src.utils.logger import Logger
8+
from src.api.services.UtilityService import SignatureData, UtilityService
9+
from src.api.constants.signature_sources import SIGNATURE_SOURCES
10+
11+
12+
class GateWayAuth(APIKeyHeader):
13+
def __init__(self, logger: Logger) -> None:
14+
self.logger = logger
15+
super().__init__()
16+
17+
def authenticate(self, request: HttpRequest, key: str | None) -> str | None:
18+
try:
19+
api_key = request.headers["X-API-GATEWAY-KEY"]
20+
api_timestamp = request.headers["X-API-GATEWAY-TIMESTAMP"]
21+
api_signature = request.headers["X-API-GATEWAY-SIGNATURE"]
22+
except KeyError as e:
23+
message = f"Missing required header: {e}"
24+
self.logger.error(
25+
{
26+
"activity_type": "Authenticate Gateway Request",
27+
"message": message,
28+
"metadata": {"headers": request.headers},
29+
}
30+
)
31+
raise AuthenticationError(message=message)
32+
33+
valid_api_key = api_gateway["key"]
34+
if api_key != valid_api_key:
35+
message = "Invalid API key!"
36+
self.logger.error(
37+
{
38+
"activity_type": "Authenticate Gateway Request",
39+
"message": message,
40+
"metadata": {"headers": request.headers},
41+
}
42+
)
43+
raise AuthenticationError(message=message)
44+
45+
signature_data: SignatureData = {
46+
"signature": api_signature,
47+
"timestamp": api_timestamp,
48+
"key": valid_api_key,
49+
"ttl": 5,
50+
"title": SIGNATURE_SOURCES["gateway"],
51+
}
52+
53+
UtilityService.verify_signature(
54+
logger=self.logger, signature_data=signature_data
55+
)
56+
57+
self.logger.debug(
58+
{
59+
"activity_type": "Authenticate Gateway Request",
60+
"message": "Successfully authenticated gateway request",
61+
"metadata": {
62+
"headers": request.headers,
63+
},
64+
}
65+
)
66+
67+
return api_signature
68+
69+
70+
def get_authentication() -> GateWayAuth:
71+
gateway_auth = GateWayAuth(Logger("Authentication"))
72+
return gateway_auth
73+
74+
75+
def add_global_headers(schema: OpenAPISchema) -> OpenAPISchema:
76+
for path in schema["paths"]:
77+
for method in schema["paths"][path]:
78+
operation = schema["paths"][path][method]
79+
if operation.get("security"):
80+
operation["security"] = schema["security"]
81+
return schema
82+
83+
84+
authentication = get_authentication()

src/api/routes/__init__.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
from django.http import HttpRequest
21
from ninja import NinjaAPI
2+
from django.http import HttpRequest
33
from ninja.openapi.schema import OpenAPISchema
44

5-
from src.api.middlewares.GateWayMiddleware import (add_global_headers,
6-
authentication)
75
from src.env import app
6+
from src.api.middlewares.GateWayMiddleware import authentication, add_global_headers
87

98
api: NinjaAPI = NinjaAPI(
109
version=app["version"],
@@ -36,25 +35,13 @@ def custom_openapi_schema(path_params: dict | None = None) -> OpenAPISchema:
3635
"in": "header",
3736
"name": "X-API-GATEWAY-SIGNATURE",
3837
},
39-
# "User ID": {
40-
# "type": "apiKey",
41-
# "in": "header",
42-
# "name": "X-USER-ID",
43-
# },
44-
# "User Email": {
45-
# "type": "apiKey",
46-
# "in": "header",
47-
# "name": "X-USER-EMAIL",
48-
# },
4938
}
5039

5140
schema["security"] = [
5241
{
5342
"Gateway Key": [],
5443
"API Timestamp": [],
5544
"API Signature": [],
56-
# "User ID": [],
57-
# "User Email": [],
5845
}
5946
]
6047

src/api/services/AuthService.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
from typing import Annotated
22

3-
from src.api.constants.activity_types import ACTIVITY_TYPES
4-
from src.api.constants.messages import DYNAMIC_MESSAGES, MESSAGES
5-
from src.api.constants.queues import QUEUE_NAMES
6-
from src.api.models.payload.requests.AuthenticateUserOtp import \
7-
AuthenticateUserOtp
8-
from src.api.models.payload.requests.AuthenticateUserRequest import \
9-
AuthenticateUserRequest
10-
from src.api.models.payload.requests.ChangeUserPasswordRequest import \
11-
ChangeUserPasswordRequest
12-
from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest
13-
from src.api.models.payload.requests.JWT import JWT
14-
from src.api.models.payload.requests.ResendUserOtp import ResendUserOtp
15-
from src.api.repositories.UserRepository import UserRepository
3+
from src.utils.svcs import Service
4+
from src.config.asgi import broker
5+
from src.utils.logger import Logger
166
from src.api.typing.JWT import JWTSuccess
7+
from src.api.constants.queues import QUEUE_NAMES
178
from src.api.typing.UserExists import UserExists
9+
from src.api.constants.messages import MESSAGES, DYNAMIC_MESSAGES
1810
from src.api.typing.UserSuccess import UserSuccess
19-
from src.config.asgi import broker
20-
from src.utils.logger import Logger
21-
from src.utils.svcs import Service
11+
from src.api.constants.activity_types import ACTIVITY_TYPES
12+
from src.api.models.payload.requests.JWT import JWT
13+
from src.api.repositories.UserRepository import UserRepository
14+
from src.api.models.payload.requests.ResendUserOtp import ResendUserOtp
15+
from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest
16+
from src.api.models.payload.requests.AuthenticateUserOtp import AuthenticateUserOtp
17+
from src.api.models.payload.requests.AuthenticateUserRequest import (
18+
AuthenticateUserRequest,
19+
)
20+
from src.api.models.payload.requests.ChangeUserPasswordRequest import (
21+
ChangeUserPasswordRequest,
22+
)
2223

2324
from .OtpService import OtpService
2425
from .UtilityService import UtilityService

src/api/services/UtilityService.py

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
1-
import hashlib
21
import hmac
3-
from datetime import datetime, timedelta
4-
from typing import TypedDict
2+
import hashlib
53
from uuid import uuid4
4+
from typing import TypedDict
5+
from datetime import datetime, timedelta
66

7-
import bcrypt
87
import jwt
9-
from django.utils import timezone
8+
import bcrypt
109
from faker import Faker
10+
from django.utils import timezone
1111
from ninja.errors import AuthenticationError
1212

13-
from src.api.enums.CharacterCasing import CharacterCasing
14-
from src.api.models.postgres import User
15-
from src.api.typing.ExpireUUID import ExpireUUID
16-
from src.api.typing.JWT import JWTData
1713
from src.env import jwt_config
18-
from src.utils.logger import Logger
1914
from src.utils.svcs import Service
15+
from src.utils.logger import Logger
16+
from src.api.typing.JWT import JWTData
17+
from src.api.models.postgres import User
18+
from src.api.typing.ExpireUUID import ExpireUUID
19+
from src.api.enums.CharacterCasing import CharacterCasing
2020

2121
DEFAULT_CHARACTER_LENGTH = 12
2222
fake = Faker()
2323

24+
2425
class SignatureData(TypedDict):
2526
title: str
2627
signature: str
2728
timestamp: str
2829
key: str
2930
ttl: int | float
3031

32+
3133
@Service()
3234
class UtilityService:
3335
@staticmethod
@@ -114,17 +116,14 @@ def generate_signature(key: str, timestamp: str) -> str:
114116
).hexdigest()
115117
return signature
116118

117-
118119
@staticmethod
119120
def verify_signature(signature_data: SignatureData, logger: Logger) -> bool:
120-
121121
signature = signature_data["signature"]
122122
timestamp = signature_data["timestamp"]
123123
key = signature_data["key"]
124124
ttl = signature_data["ttl"]
125125
title = signature_data["title"]
126126

127-
128127
valid_signature = UtilityService.generate_signature(key, timestamp)
129128
is_valid = hmac.compare_digest(valid_signature, signature)
130129

src/config/asgi.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111

1212
from django.core.asgi import get_asgi_application
1313
from faststream.rabbit import RabbitBroker
14-
from starlette.applications import Starlette
1514
from starlette.routing import Mount
15+
from starlette.applications import Starlette
1616

1717
from src.env import rabbitmq_config
1818

@@ -22,9 +22,11 @@
2222
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.config.settings")
2323

2424

25-
def setup_broker_middlewares():
26-
from src.api.middlewares.BrokerMiddleware import (PublishMiddleware,
27-
SubscribeMiddleware)
25+
def setup_broker_middlewares() -> None:
26+
from src.api.middlewares.BrokerMiddleware import (
27+
PublishMiddleware,
28+
SubscribeMiddleware,
29+
)
2830

2931
broker.add_middleware(SubscribeMiddleware)
3032
broker.add_middleware(PublishMiddleware)
@@ -37,5 +39,4 @@ def setup_broker_middlewares():
3739
)
3840

3941

40-
from src.api.services.external import \
41-
RabbitMQRoutes as RabbitMQRoutes # noqa: E402
42+
from src.api.services.external import RabbitMQRoutes as RabbitMQRoutes # noqa: E402

src/config/settings.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
from .apps import INSTALLED_APPS as INSTALLED_APPS
44
from .caches import CACHES as CACHES
5-
from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS
6-
from .databases import DATABASES as DATABASES
75
from .logger import LOGGING as LOGGING
8-
from .middleware import MIDDLEWARE as MIDDLEWARE
6+
from .databases import DATABASES as DATABASES
7+
from .databases import DATABASE_ROUTERS as DATABASE_ROUTERS
98
from .templates import TEMPLATES as TEMPLATES
9+
from .middleware import MIDDLEWARE as MIDDLEWARE
1010

1111
SECRET_KEY = app["secret_key"]
1212
DEBUG = app["debug"]
@@ -19,10 +19,10 @@
1919
STATIC_URL = "static/"
2020
MEDIA_URL = "media/"
2121

22-
LANGUAGE_CODE = 'en-us'
22+
LANGUAGE_CODE = "en-us"
2323

24-
TIME_ZONE = 'UTC'
24+
TIME_ZONE = "UTC"
2525

2626
USE_I18N = True
2727

28-
USE_TZ = True
28+
USE_TZ = True

0 commit comments

Comments
 (0)