Skip to content

Commit d37bd5e

Browse files
authored
Merge pull request #6 from Ifechukwu001/feat/jwt-auth
Exposed an API for validating JWT tokens for authentication
2 parents 9750061 + 65467db commit d37bd5e

File tree

9 files changed

+91
-41
lines changed

9 files changed

+91
-41
lines changed

src/api/constants/activity_types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class ActivityTypes(TypedDict):
99
RESEND_EMAIL: str
1010
EMAIL_VALIDATION: str
1111
USER_LOGIN: str
12+
VALIDATE_TOKEN: str
1213
CHANGE_PASSWORD: str
1314
REQUEST_RESET_PASSWORD: str
1415
CONFIRM_RESET_PASSWORD: str
@@ -22,6 +23,7 @@ class ActivityTypes(TypedDict):
2223
"RESEND_EMAIL": "Resend email validation",
2324
"EMAIL_VALIDATION": "Email validation",
2425
"USER_LOGIN": "User login",
26+
"VALIDATE_TOKEN": "Validate token",
2527
"CHANGE_PASSWORD": "Change user password",
2628
"REQUEST_RESET_PASSWORD": "Request for password reset",
2729
"CONFIRM_RESET_PASSWORD": "Confirm password reset",

src/api/constants/messages.py

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class AuthMessages(TypedDict):
1717
NOT_ENABLED: str
1818
IS_DELETED: str
1919
SUCCESS: str
20+
TOKEN_ERROR: str
21+
TOKEN_SUCCESS: str
2022

2123

2224
class OtpMessages(TypedDict):
@@ -28,30 +30,11 @@ class OtpMessages(TypedDict):
2830

2931
class UserMessages(TypedDict):
3032
DOESNT_EXIST: str
31-
UPDATED: str
3233
SANITIZED: str
33-
NOT_ALLOWED: str
34-
PIN_EXISTS: str
35-
PIN_SET: str
3634
INCORRECT_PASSWORD: str
3735
PASSWORD_CHANGED: str
3836

3937

40-
class AccountMessages(TypedDict):
41-
SAVED: str
42-
UPDATED: str
43-
44-
45-
class NextOfKinMessages(TypedDict):
46-
FETCHED: str
47-
UPDATED: str
48-
49-
50-
class KYCInformationMessages(TypedDict):
51-
FETCHED: str
52-
UPDATED: str
53-
54-
5538
class CommonMessages(TypedDict):
5639
INTERNAL_SERVER_ERROR: str
5740
JWT_GENERATED: str
@@ -73,9 +56,6 @@ class Messages(TypedDict):
7356
AUTH: AuthMessages
7457
OTP: OtpMessages
7558
USER: UserMessages
76-
ACCOUNT: AccountMessages
77-
NOK: NextOfKinMessages
78-
KYC: KYCInformationMessages
7959
COMMON: CommonMessages
8060
PASSWORD_RESET: PasswordResetMessages
8161

@@ -116,6 +96,8 @@ class DynamicMessages(TypedDict):
11696
"NOT_ENABLED": "User account is disabled. Please contact support",
11797
"IS_DELETED": "User account has been deleted. Please contact support if you want to restore your account",
11898
"SUCCESS": "Successfully logged in",
99+
"TOKEN_ERROR": "Invalid token",
100+
"TOKEN_SUCCESS": "Valid token",
119101
},
120102
"OTP": {
121103
"SEND_SUCCESS": "OTP sent successfully",
@@ -125,26 +107,10 @@ class DynamicMessages(TypedDict):
125107
},
126108
"USER": {
127109
"DOESNT_EXIST": "User doesn't exist",
128-
"UPDATED": "User updated successfully",
129110
"SANITIZED": "User object was sanitized",
130-
"PIN_EXISTS": "You already have a transaction PIN on your account!",
131-
"PIN_SET": "Transaction PIN set successfully",
132-
"NOT_ALLOWED": "Unauthorized request!",
133111
"INCORRECT_PASSWORD": "Incorrect old password",
134112
"PASSWORD_CHANGED": "Password changed successfully",
135113
},
136-
"ACCOUNT": {
137-
"SAVED": "Withdrawal account details saved succesfully",
138-
"UPDATED": "User Withdraw account updated successfully",
139-
},
140-
"NOK": {
141-
"FETCHED": "Next of Kin fetched successfully",
142-
"UPDATED": "Next of Kin updated successfully",
143-
},
144-
"KYC": {
145-
"FETCHED": "KYC Information fetched successfully",
146-
"UPDATED": "KYC Information updated successfully",
147-
},
148114
"COMMON": {
149115
"INTERNAL_SERVER_ERROR": "Something went wrong",
150116
"JWT_GENERATED": "JWT was generated",

src/api/controllers/AuthController.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from src.api.constants.messages import MESSAGES
77
from src.api.services.AuthService import AuthService
88
from src.api.utils.response_format import error_response, success_response
9+
from src.api.models.payload.requests.JWT import JWT
910
from src.api.models.payload.requests.ResendUserOtp import ResendUserOtp
1011
from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest
1112
from src.api.models.payload.requests.AuthenticateUserOtp import AuthenticateUserOtp
@@ -71,6 +72,18 @@ async def login(self, credentials: AuthenticateUserRequest) -> tuple:
7172
status_code=HTTPStatus.OK,
7273
)
7374

75+
async def validate_token(self, credentials: JWT) -> tuple:
76+
jwt_details = await self.auth_service.validate_token(credentials)
77+
if not jwt_details["is_success"]:
78+
return error_response(
79+
message=jwt_details["message"], status_code=HTTPStatus.UNAUTHORIZED
80+
)
81+
return success_response(
82+
message=jwt_details["message"],
83+
data=jwt_details["data"],
84+
status_code=HTTPStatus.OK,
85+
)
86+
7487
async def change_password(
7588
self, id: str, user_data: ChangeUserPasswordRequest
7689
) -> tuple:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pydantic import BaseModel
2+
3+
4+
class JWT(BaseModel):
5+
token: str

src/api/routes/Auth.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from src.utils.svcs import ADepends
77
from src.api.controllers.AuthController import AuthController
8+
from src.api.models.payload.requests.JWT import JWT
89
from src.api.models.payload.responses.User import UserResponse, UserLoginResponse
910
from src.api.models.payload.requests.ResendUserOtp import ResendUserOtp
1011
from src.api.models.payload.responses.ErrorResponse import (
@@ -78,6 +79,19 @@ async def login(request: HttpRequest, credentials: AuthenticateUserRequest) -> t
7879
return await auth_controller.login(credentials)
7980

8081

82+
@router.post(
83+
"/validate-token",
84+
response={
85+
HTTPStatus.OK: SuccessResponse,
86+
HTTPStatus.UNAUTHORIZED: ErrorResponse,
87+
HTTPStatus.INTERNAL_SERVER_ERROR: ServerErrorResponse,
88+
},
89+
)
90+
async def validate_jwt_token(request: HttpRequest, credentials: JWT) -> tuple:
91+
auth_controller = await ADepends(AuthController)
92+
return await auth_controller.validate_token(credentials)
93+
94+
8195
@router.put(
8296
"/change-password",
8397
response={

src/api/services/AuthService.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
from src.utils.svcs import Service
44
from src.utils.logger import Logger
5+
from src.api.typing.JWT import JWTSuccess
56
from src.api.typing.UserExists import UserExists
67
from src.api.constants.messages import MESSAGES, DYNAMIC_MESSAGES
78
from src.api.typing.UserSuccess import UserSuccess
89
from src.api.constants.activity_types import ACTIVITY_TYPES
10+
from src.api.models.payload.requests.JWT import JWT
911
from src.api.repositories.UserRepository import UserRepository
1012
from src.api.models.payload.requests.ResendUserOtp import ResendUserOtp
1113
from src.api.models.payload.requests.CreateUserRequest import CreateUserRequest
@@ -233,6 +235,29 @@ async def login(self, req: AuthenticateUserRequest) -> UserSuccess:
233235
"token": jwt_details,
234236
}
235237

238+
async def validate_token(self, req: JWT) -> JWTSuccess:
239+
data = self.utility_service.decrypt_jwt(req.token)
240+
if not data:
241+
message = MESSAGES["AUTH"]["TOKEN_ERROR"]
242+
self.logger.info(
243+
{
244+
"activity_type": ACTIVITY_TYPES["VALIDATE_TOKEN"],
245+
"message": message,
246+
"metadata": {"token": req.token},
247+
}
248+
)
249+
return {"is_success": False, "message": message}
250+
251+
message = MESSAGES["AUTH"]["TOKEN_SUCCESS"]
252+
self.logger.info(
253+
{
254+
"activity_type": ACTIVITY_TYPES["VALIDATE_TOKEN"],
255+
"message": message,
256+
"metadata": {"user": {"email": data["email"], "id": data["user_id"]}},
257+
}
258+
)
259+
return {"is_success": True, "message": message, "data": data}
260+
236261
async def change_password(
237262
self, id: str, req: ChangeUserPasswordRequest
238263
) -> UserSuccess:

src/api/services/UtilityService.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from src.env import jwt_config
1010
from src.utils.svcs import Service
11+
from src.api.typing.JWT import JWTData
1112
from src.api.models.postgres import User
1213
from src.api.typing.ExpireUUID import ExpireUUID
1314
from src.api.enums.CharacterCasing import CharacterCasing
@@ -70,10 +71,24 @@ def generate_jwt(email: str, user_uuid: str) -> str:
7071
jwt_claims = {
7172
"exp": timezone.now() + timedelta(seconds=3600),
7273
"iss": jwt_config["issuer"],
73-
"aud": email,
74+
"aud": jwt_config["issuer"],
7475
}
7576
return jwt.encode(dict(jwt_data, **jwt_claims), jwt_config["secret"])
7677

78+
@staticmethod
79+
def decrypt_jwt(token: str) -> JWTData | None:
80+
try:
81+
data = jwt.decode(
82+
token,
83+
jwt_config["secret"],
84+
algorithms=["HS256"],
85+
issuer=jwt_config["issuer"],
86+
audience=jwt_config["issuer"],
87+
)
88+
return {"email": data["email"], "user_id": data["user_id"]}
89+
except jwt.exceptions.InvalidTokenError:
90+
...
91+
7792
@staticmethod
7893
def generate_uuid() -> ExpireUUID:
7994
current_time = timezone.now()

src/api/typing/JWT.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from typing import TypedDict, NotRequired
2+
3+
4+
class JWTData(TypedDict):
5+
email: str
6+
user_id: str
7+
8+
9+
class JWTSuccess(TypedDict):
10+
is_success: bool
11+
message: str
12+
data: NotRequired[JWTData]

src/api/utils/error_handlers.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import traceback
22
from http import HTTPStatus
33

4-
import jwt
54
from django.http import HttpRequest, HttpResponse
65
from ninja.errors import ValidationError, AuthenticationError
76

@@ -15,7 +14,6 @@
1514
exception_logger = Logger("API Exception")
1615

1716

18-
@api.exception_handler(jwt.exceptions.InvalidTokenError)
1917
@api.exception_handler(AuthenticationError)
2018
def on_invalid_token(request: HttpRequest, exc: Exception) -> HttpResponse:
2119
exception_logger.debug(

0 commit comments

Comments
 (0)