Skip to content

Commit 25fe348

Browse files
authored
Adding Auth Module (#18)
* token creation init * fixed JSON issues * fixed lint issues * token is generating * added method to get claims * added api-server
1 parent d171373 commit 25fe348

12 files changed

Lines changed: 171 additions & 13 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import os
2+
import jwt
3+
from datetime import datetime, timedelta
4+
from starlette.responses import JSONResponse
5+
6+
from ..models.token_request import TokenRequest
7+
from ..models.token_response import TokenResponse
8+
from ..models.token_claims import TokenClaims
9+
10+
from app.singletons.logs_manager import LogsManager
11+
12+
from app.user.models.user_database_model import User
13+
14+
logger = LogsManager().get_logger()
15+
16+
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
17+
if not JWT_SECRET_KEY:
18+
raise ValueError("JWT_SECRET_KEY environment variable is not set or is empty.")
19+
JWT_ALGORITHM = "HS256"
20+
JWT_EXPIRES_IN = 3600 # 1 hour
21+
22+
23+
async def create_token(request: TokenRequest, x_exosphere_request_id: str) -> TokenResponse:
24+
25+
try:
26+
logger.info("Finding user", x_exosphere_request_id=x_exosphere_request_id)
27+
28+
user = await User.find_one(User.identifier == request.identifier)
29+
30+
if not user:
31+
logger.error("User not found", x_exosphere_request_id=x_exosphere_request_id)
32+
return JSONResponse(status_code=404, content={"success": False, "detail": "User not found"})
33+
34+
if not user.verify_credential(request.credential):
35+
logger.error("Invalid credential", x_exosphere_request_id=x_exosphere_request_id)
36+
return JSONResponse(status_code=401, content={"success": False, "detail": "Invalid credential"})
37+
38+
logger.info("User found and credential verified", x_exosphere_request_id=x_exosphere_request_id)
39+
40+
logger.info("User is a super admin", x_exosphere_request_id=x_exosphere_request_id)
41+
42+
token_claims = TokenClaims(
43+
user_id=str(user.id),
44+
user_name=user.name,
45+
user_type=user.type,
46+
verification_status=user.verification_status,
47+
status=user.status,
48+
exp=int((datetime.now() + timedelta(seconds=JWT_EXPIRES_IN)).timestamp())
49+
)
50+
51+
return TokenResponse(
52+
access_token=jwt.encode(token_claims.model_dump(), JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
53+
)
54+
55+
except Exception as e:
56+
logger.error("Error creating token", error=e, x_exosphere_request_id=x_exosphere_request_id)
57+
raise e
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import jwt
2+
import os
3+
4+
from starlette.middleware.base import BaseHTTPMiddleware
5+
from app.singletons.logs_manager import LogsManager
6+
from starlette.requests import Request
7+
from starlette.responses import JSONResponse
8+
9+
from ..models.token_claims import TokenClaims
10+
11+
logger = LogsManager().get_logger()
12+
13+
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY")
14+
if not JWT_SECRET_KEY:
15+
raise ValueError("JWT_SECRET_KEY environment variable is not set or is empty.")
16+
JWT_ALGORITHM = "HS256"
17+
18+
19+
class GetTokenClaimsMiddleware(BaseHTTPMiddleware):
20+
async def dispatch(self, request: Request, call_next):
21+
22+
token = request.headers.get("Authorization")
23+
24+
if token:
25+
if not token.startswith("Bearer "):
26+
logger.error("Invalid token format", x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None))
27+
return JSONResponse(status_code=401, content={"message": "Invalid token format", "success": False})
28+
29+
try:
30+
token_claims = jwt.decode(token.split(" ")[1], JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
31+
request.state.token_claims = TokenClaims(**token_claims)
32+
logger.info("Token claims decoded", x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None), user_id=request.state.token_claims.user_id)
33+
34+
except Exception as e:
35+
logger.error("Error decoding token", error=e, x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None))
36+
37+
return JSONResponse(status_code=401, content={"message": "Invalid token", "success": False})
38+
39+
else:
40+
logger.error("No token provided", x_exosphere_request_id=getattr(request.state, 'x_exosphere_request_id', None))
41+
request.state.token_claims = None
42+
43+
return await call_next(request)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pydantic import BaseModel
2+
3+
class TokenClaims(BaseModel):
4+
user_id: str
5+
user_name: str
6+
user_type: str
7+
verification_status: str
8+
status: str
9+
exp: int
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pydantic import BaseModel, Field
2+
3+
class TokenRequest(BaseModel):
4+
identifier: str = Field(..., description="Identifier of the user, could be an email, phone, username, etc.")
5+
credential: str = Field(..., description="Credential of the user, could be a password, api secret, etc.")
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from pydantic import BaseModel, Field
2+
3+
class TokenResponse(BaseModel):
4+
access_token: str = Field(..., description="Access token for the user")

api-server/app/auth/router.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from fastapi import APIRouter, status, Request
2+
3+
from .controllers.create_token import create_token
4+
from .models.token_request import TokenRequest
5+
from .models.token_response import TokenResponse
6+
7+
router = APIRouter(prefix="/auth", tags=["auth"])
8+
9+
@router.post(
10+
"/token",
11+
response_model=TokenResponse,
12+
status_code=status.HTTP_200_OK,
13+
response_description="Token created successfully"
14+
)
15+
async def create_token_route(body: TokenRequest, request: Request):
16+
x_exosphere_request_id = getattr(request.state, "x_exosphere_request_id", None)
17+
return await create_token(body, x_exosphere_request_id)

api-server/app/main.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from contextlib import asynccontextmanager
88
from dotenv import load_dotenv
99
from motor.motor_asyncio import AsyncIOMotorClient
10+
from fastapi.security import OAuth2PasswordBearer
1011

1112
# injecting singletons
1213
from .singletons.logs_manager import LogsManager
@@ -16,13 +17,15 @@
1617
UnhandledExceptionsMiddleware,
1718
)
1819
from .middlewares.request_id_middleware import RequestIdMiddleware
20+
from .auth.middlewares.get_token_claims import GetTokenClaimsMiddleware
1921

2022
# injecting databases
2123
from .user.models.user_database_model import User
2224
from .project.models.project_database_model import Project
2325

2426
# injecting routers
2527
from .user.routes import router as user_router
28+
from .auth.router import router as auth_router
2629

2730
load_dotenv()
2831

@@ -48,16 +51,21 @@ async def lifespan(app: FastAPI):
4851

4952
app = FastAPI(lifespan=lifespan)
5053

54+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
55+
5156
# this middleware should be the first one
57+
app.add_middleware(GetTokenClaimsMiddleware)
58+
5259
app.add_middleware(RequestIdMiddleware)
5360

54-
# this middleware should be the last one
5561
app.add_middleware(UnhandledExceptionsMiddleware)
5662

5763

64+
5865
@app.get("/health-check")
5966
def health() -> dict:
6067
return {"message": "OK"}
6168

62-
63-
app.include_router(user_router)
69+
# injecting routers
70+
app.include_router(user_router)
71+
app.include_router(auth_router)

api-server/app/middlewares/unhandled_exceptions_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ async def dispatch(self, request: Request, call_next):
2525
status_code=500,
2626
content={
2727
"success": False,
28-
"detail": "internal server error, please reach out to support team at support@exosphere.com",
28+
"detail": "internal server error, please reach out to support team at nivedit@exosphere.host",
2929
},
3030
)

api-server/app/user/controllers/create_user.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from fastapi import HTTPException
1+
from pymongo.errors import DuplicateKeyError
2+
from starlette.responses import JSONResponse
23

34
from ..models.create_user_request import CreateUserRequest
45
from ..models.create_user_response import CreateUserResponse
@@ -19,10 +20,9 @@ async def create_user(request: CreateUserRequest, x_exosphere_request_id: str) -
1920
identifier=request.identifier,
2021
credential=request.credential
2122
)
22-
await new_user.save()
23+
await new_user.insert()
2324
logger.info("User created", x_exosphere_request_id=x_exosphere_request_id)
2425

25-
2626
return CreateUserResponse(
2727
id=str(new_user.id),
2828
name=new_user.name,
@@ -33,8 +33,11 @@ async def create_user(request: CreateUserRequest, x_exosphere_request_id: str) -
3333
created_at=new_user.created_at,
3434
updated_at=new_user.updated_at
3535
)
36-
36+
37+
except DuplicateKeyError as e:
38+
logger.error("Error creating user", error=e, x_exosphere_request_id=x_exosphere_request_id)
39+
return JSONResponse(status_code=400, content={"success": False, "detail": "User already exists"})
40+
3741
except Exception as e:
38-
logger.error("Error creating user error", error=e, x_exosphere_request_id=x_exosphere_request_id)
39-
40-
raise HTTPException(status_code=500, detail="Error creating user")
42+
logger.error("Error creating user", error=e, x_exosphere_request_id=x_exosphere_request_id)
43+
raise e

api-server/app/user/models/user_database_model.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import bcrypt
22
from datetime import datetime
3-
from beanie import Document, before_event, Replace, Save
3+
from beanie import Document, before_event, Replace, Save, Indexed
44
from pydantic import Field
55
from .user_type_enum import UserTypeEnum
66
from .verification_status_enum import VerificationStatusEnum
@@ -13,7 +13,7 @@ class User(Document):
1313

1414
type: UserTypeEnum = Field(..., description="Type of the user, human, api, etc. (enum)")
1515

16-
identifier: str = Field(..., description="Unique identifier for the user, could be an email, phone, username, etc.", unique=True)
16+
identifier: Indexed(str, unique=True) = Field(..., description="Unique identifier for the user, could be an email, phone, username, etc.")
1717

1818
verification_status: VerificationStatusEnum = Field(default=VerificationStatusEnum.NOT_VERIFIED, description="Verification status of the user, verified, not_verified, blocked, deleted, not_required (enum)")
1919

0 commit comments

Comments
 (0)