Skip to content

Commit 22e85f1

Browse files
feat: add auth0 api endpoints
1 parent b2de603 commit 22e85f1

File tree

6 files changed

+371
-2
lines changed

6 files changed

+371
-2
lines changed

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies = [
1717
"fastapi-pagination>=0.12.34",
1818
"bcrypt==4.0.1",
1919
"google-genai>=1.5.0",
20+
"itsdangerous (>=2.2.0,<3.0.0)",
2021
]
2122

2223
[tool.uv]

backend/src/auth/auth0_api.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from typing import Annotated, Any
2+
3+
from fastapi import APIRouter, Depends, HTTPException, Request
4+
from sqlmodel import Session
5+
from starlette.responses import RedirectResponse
6+
7+
from src.core.db import get_db
8+
from src.dependencies.auth0 import (
9+
get_auth0_service,
10+
get_current_user,
11+
get_current_user_claims
12+
)
13+
from src.services.auth0 import Auth0Service
14+
from src.users.auth0 import get_or_create_user_from_auth0
15+
from src.users.models import User
16+
from src.users.schemas import UserPublic
17+
18+
router = APIRouter(prefix="/auth0", tags=["auth0"])
19+
20+
21+
@router.get("/login")
22+
async def login(
23+
request: Request,
24+
auth_service: Annotated[Auth0Service, Depends(get_auth0_service)]
25+
) -> RedirectResponse:
26+
return await auth_service.login(request)
27+
28+
29+
@router.get("/callback")
30+
async def callback(
31+
request: Request,
32+
session: Annotated[Session, Depends(get_db)],
33+
auth_service: Annotated[Auth0Service, Depends(get_auth0_service)]
34+
) -> RedirectResponse:
35+
try:
36+
# Exchange auth code for tokens
37+
token_response = await auth_service.callback(request)
38+
access_token = token_response.get("access_token")
39+
40+
# Store access token in session for later use
41+
request.session["access_token"] = access_token
42+
43+
# Get user info from Auth0
44+
user_info = await auth_service.get_user_info(access_token)
45+
46+
# Get or create user in our database
47+
db_user = await get_or_create_user_from_auth0(session, user_info)
48+
49+
# Store user ID in session
50+
request.session["user_id"] = str(db_user.id)
51+
52+
# Redirect to the frontend after successful authentication
53+
return RedirectResponse(url="/")
54+
except Exception as e:
55+
# Log the error and redirect to error page
56+
return RedirectResponse(url=f"/auth0/error?message={str(e)}")
57+
58+
59+
@router.get("/logout")
60+
async def logout(
61+
auth_service: Annotated[Auth0Service, Depends(get_auth0_service)]
62+
) -> RedirectResponse:
63+
return auth_service.logout()
64+
65+
66+
@router.get("/me", response_model=UserPublic)
67+
async def read_users_me(
68+
current_user: Annotated[User, Depends(get_current_user)]
69+
) -> User:
70+
return current_user
71+
72+
73+
@router.get("/validate")
74+
async def validate_token(
75+
claims: Annotated[dict[str, Any], Depends(get_current_user_claims)]
76+
) -> dict[str, Any]:
77+
return claims
78+
79+
80+
@router.get("/error")
81+
async def auth_error(message: str = "Authentication error"):
82+
raise HTTPException(
83+
status_code=401,
84+
detail=message
85+
)

backend/src/core/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ class Settings(BaseSettings):
4949
# Auth0 Configuration
5050
AUTH0_CLIENT_ID: str
5151
AUTH0_CLIENT_SECRET: str
52-
AUTH0_ISSUER: str
52+
AUTH0_DOMAIN: str
53+
AUTH0_ISSUER: str = "" # Default empty string to make it optional
5354
AUTH0_CALLBACK_URL: str
54-
AUTH0_LOGOUT_URL: str
55+
AUTH0_LOGOUT_URL: str = "" # Default empty string to make it optional
5556
AUTH0_AUDIENCE: str
5657

5758
# Session Configuration

backend/src/dependencies/auth0.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from typing import Annotated, Any
2+
3+
from fastapi import Depends, HTTPException, Request
4+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
5+
from sqlmodel import Session
6+
7+
from src.core.db import get_db
8+
from src.services.auth0 import Auth0Service, UserInfo
9+
from src.users.auth0 import get_or_create_user_from_auth0, get_user_by_auth0_id
10+
from src.users.models import User
11+
12+
from functools import lru_cache
13+
14+
security = HTTPBearer()
15+
16+
17+
# Initialize Auth0Service
18+
# This is a singleton instance of Auth0Service
19+
# to be reused across requests.
20+
@lru_cache()
21+
def get_auth0_service() -> Auth0Service:
22+
"""
23+
Provides a singleton Auth0Service instance using lru_cache.
24+
This is the recommended way in FastAPI for services that should be reused.
25+
"""
26+
return Auth0Service()
27+
28+
29+
async def get_token_from_header(
30+
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]
31+
) -> str:
32+
"""
33+
Extract and return the JWT token from Authorization header.
34+
"""
35+
return credentials.credentials
36+
37+
38+
async def get_current_user_claims(
39+
token: Annotated[str, Depends(get_token_from_header)],
40+
auth_service: Annotated[Auth0Service, Depends(get_auth0_service)]
41+
) -> dict[str, Any]:
42+
"""
43+
Validate token and return the user claims.
44+
"""
45+
claims = await auth_service.validate_token(token)
46+
47+
return claims
48+
49+
50+
async def get_current_user_info(
51+
request: Request,
52+
token: Annotated[str, Depends(get_token_from_header)],
53+
claims: Annotated[dict[str, Any], Depends(get_current_user_claims)],
54+
auth_service: Annotated[Auth0Service, Depends(get_auth0_service)]
55+
) -> UserInfo:
56+
"""
57+
Get user information from Auth0.
58+
"""
59+
# Access token should be available in the request's session
60+
# after the callback flow
61+
access_token = request.session.get("access_token")
62+
if not access_token:
63+
# If not in session, use the token from header
64+
access_token = token
65+
66+
return await auth_service.get_user_info(access_token)
67+
68+
69+
async def get_current_user(
70+
request: Request,
71+
session: Annotated[Session, Depends(get_db)],
72+
user_info: Annotated[UserInfo, Depends(get_current_user_info)]
73+
) -> User:
74+
"""
75+
Get the current user from the database.
76+
"""
77+
try:
78+
# Check if user_id is in session
79+
user_id = request.session.get("user_id")
80+
if user_id:
81+
# Use the user ID from the session
82+
user = session.get(User, user_id)
83+
if user:
84+
return user
85+
86+
# If no valid user in session, look up by Auth0 ID
87+
auth0_id = user_info.get("sub")
88+
if not auth0_id:
89+
raise ValueError("Missing Auth0 user ID")
90+
91+
# Try to find by Auth0 ID
92+
user = await get_user_by_auth0_id(session, auth0_id)
93+
if user:
94+
# Store user ID in session for future requests
95+
request.session["user_id"] = str(user.id)
96+
return user
97+
98+
# Create or link user if not found
99+
user = await get_or_create_user_from_auth0(session, user_info)
100+
101+
# Store user ID in session for future requests
102+
request.session["user_id"] = str(user.id)
103+
return user
104+
105+
except Exception as e:
106+
raise HTTPException(
107+
status_code=401,
108+
detail=f"User integration failed: {str(e)}"
109+
)

backend/src/routers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from fastapi import APIRouter
22

33
from src.auth.api import router as auth_router
4+
from src.auth.auth0_api import router as auth0_router
45
from src.flashcards.api import router as flashcards_router
56
from src.stats.api import router as stats_router
67
from src.users.api import router as user_router
78

89
api_router = APIRouter()
910

1011
api_router.include_router(auth_router, tags=["login"])
12+
api_router.include_router(auth0_router)
1113
api_router.include_router(user_router, prefix="/users", tags=["users"])
1214
api_router.include_router(flashcards_router, tags=["flashcards"])
1315
api_router.include_router(stats_router, tags=["stats"])

0 commit comments

Comments
 (0)