WIP checkpoint: Implement /auth/refresh endpoint
This commit is contained in:
@@ -5,7 +5,7 @@ from datetime import date, datetime, timedelta, timezone
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from fastapi.security import OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
from jose import jwt
|
from jose import JWTError, jwt
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from pydantic import BaseModel, EmailStr, field_validator
|
from pydantic import BaseModel, EmailStr, field_validator
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -31,6 +31,10 @@ class TokenResponse(BaseModel):
|
|||||||
access_token_expires_minutes: int
|
access_token_expires_minutes: int
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshRequest(BaseModel):
|
||||||
|
refresh_token: str
|
||||||
|
|
||||||
|
|
||||||
class RegisterRequest(BaseModel):
|
class RegisterRequest(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
@@ -232,6 +236,72 @@ async def login(form: OAuth2PasswordRequestForm = Depends(), db: AsyncSession =
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/refresh", response_model=TokenResponse)
|
||||||
|
async def refresh(req: RefreshRequest, db: AsyncSession = Depends(get_db)):
|
||||||
|
"""Exchange a valid refresh token for a new access token.
|
||||||
|
|
||||||
|
Validates the refresh token against the database:
|
||||||
|
- Must exist in the refresh_tokens table
|
||||||
|
- Must not be expired (expires_at > now)
|
||||||
|
- Must be active (is_active == true)
|
||||||
|
|
||||||
|
On success, returns a new access token. The refresh token itself is
|
||||||
|
NOT rotated (the client keeps using the same refresh token).
|
||||||
|
"""
|
||||||
|
credentials_exception = HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid or expired refresh token",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 1: Decode and validate the JWT signature/claims
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(
|
||||||
|
req.refresh_token,
|
||||||
|
settings.public_pem,
|
||||||
|
algorithms=[settings.ALGORITHM],
|
||||||
|
)
|
||||||
|
except JWTError:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
user_id_str: str | None = payload.get("sub")
|
||||||
|
if user_id_str is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id = int(user_id_str)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
# Step 2: Look up the refresh token in the database
|
||||||
|
result = await db.execute(
|
||||||
|
select(RefreshToken).where(
|
||||||
|
RefreshToken.token == req.refresh_token,
|
||||||
|
RefreshToken.user_id == user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stored_token = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if stored_token is None:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
# Step 3: Check if the token is expired
|
||||||
|
if datetime.now(timezone.utc) >= stored_token.expires_at:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
# Step 4: Check if the token is active
|
||||||
|
if not stored_token.is_active:
|
||||||
|
raise credentials_exception
|
||||||
|
|
||||||
|
# Step 5: Issue a new access token
|
||||||
|
access_token = _create_access_token({"sub": str(user_id)})
|
||||||
|
|
||||||
|
return TokenResponse(
|
||||||
|
access_token=access_token,
|
||||||
|
refresh_token=req.refresh_token, # Return the same refresh token
|
||||||
|
access_token_expires_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me", response_model=UserResponse)
|
@router.get("/me", response_model=UserResponse)
|
||||||
async def get_current_user(current_user: CurrentUser):
|
async def get_current_user(current_user: CurrentUser):
|
||||||
"""Get current user profile from the JWT token."""
|
"""Get current user profile from the JWT token."""
|
||||||
|
|||||||
Reference in New Issue
Block a user