Complete OAuth-based authentication with JWT session management:
Core Services:
- JWT service for access/refresh token creation and verification
- Token store with Redis-backed refresh token revocation
- User service for CRUD operations and OAuth-based creation
- Google and Discord OAuth services with full flow support
API Endpoints:
- GET /api/auth/{google,discord} - Start OAuth flows
- GET /api/auth/{google,discord}/callback - Handle OAuth callbacks
- POST /api/auth/refresh - Exchange refresh token for new access token
- POST /api/auth/logout - Revoke single refresh token
- POST /api/auth/logout-all - Revoke all user sessions
- GET/PATCH /api/users/me - User profile management
- GET /api/users/me/linked-accounts - List OAuth providers
- GET /api/users/me/sessions - Count active sessions
Infrastructure:
- Pydantic schemas for auth/user request/response models
- FastAPI dependencies (get_current_user, get_current_premium_user)
- OAuthLinkedAccount model for multi-provider support
- Alembic migration for oauth_linked_accounts table
Dependencies added: email-validator, fakeredis (dev), respx (dev)
84 new tests, 1058 total passing
207 lines
5.2 KiB
Python
207 lines
5.2 KiB
Python
"""JWT token service for Mantimon TCG.
|
|
|
|
This module provides functions for creating and verifying JWT tokens
|
|
used in the authentication system.
|
|
|
|
Token Types:
|
|
- Access tokens: Short-lived (30 min default), used for API authentication
|
|
- Refresh tokens: Longer-lived (7 days default), used to obtain new access tokens
|
|
|
|
Example:
|
|
from app.services.jwt_service import create_access_token, verify_token
|
|
|
|
# Create tokens
|
|
access_token = create_access_token(user_id)
|
|
refresh_token, jti = create_refresh_token(user_id)
|
|
|
|
# Verify token
|
|
user_id = verify_token(access_token)
|
|
if user_id:
|
|
print(f"Valid token for user {user_id}")
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
from jose import JWTError, jwt
|
|
|
|
from app.config import settings
|
|
from app.schemas.auth import TokenPayload, TokenType
|
|
|
|
|
|
def create_access_token(user_id: uuid.UUID) -> str:
|
|
"""Create a short-lived JWT access token.
|
|
|
|
Args:
|
|
user_id: The user's UUID to encode in the token.
|
|
|
|
Returns:
|
|
Encoded JWT access token string.
|
|
|
|
Example:
|
|
token = create_access_token(user.id)
|
|
# Use token in Authorization header: Bearer {token}
|
|
"""
|
|
now = datetime.now(UTC)
|
|
expires = now + timedelta(minutes=settings.jwt_expire_minutes)
|
|
|
|
payload = {
|
|
"sub": str(user_id),
|
|
"exp": expires,
|
|
"iat": now,
|
|
"type": TokenType.ACCESS.value,
|
|
}
|
|
|
|
return jwt.encode(
|
|
payload,
|
|
settings.secret_key.get_secret_value(),
|
|
algorithm=settings.jwt_algorithm,
|
|
)
|
|
|
|
|
|
def create_refresh_token(user_id: uuid.UUID) -> tuple[str, str]:
|
|
"""Create a longer-lived JWT refresh token.
|
|
|
|
Refresh tokens include a JTI (JWT ID) for tracking and revocation.
|
|
|
|
Args:
|
|
user_id: The user's UUID to encode in the token.
|
|
|
|
Returns:
|
|
Tuple of (encoded JWT refresh token, JTI string).
|
|
The JTI should be stored in Redis for revocation tracking.
|
|
|
|
Example:
|
|
token, jti = create_refresh_token(user.id)
|
|
await token_store.store_refresh_token(user.id, jti, expires_at)
|
|
"""
|
|
now = datetime.now(UTC)
|
|
expires = now + timedelta(days=settings.jwt_refresh_expire_days)
|
|
jti = str(uuid.uuid4())
|
|
|
|
payload = {
|
|
"sub": str(user_id),
|
|
"exp": expires,
|
|
"iat": now,
|
|
"type": TokenType.REFRESH.value,
|
|
"jti": jti,
|
|
}
|
|
|
|
token = jwt.encode(
|
|
payload,
|
|
settings.secret_key.get_secret_value(),
|
|
algorithm=settings.jwt_algorithm,
|
|
)
|
|
|
|
return token, jti
|
|
|
|
|
|
def decode_token(token: str) -> TokenPayload | None:
|
|
"""Decode and validate a JWT token.
|
|
|
|
Args:
|
|
token: The JWT token string to decode.
|
|
|
|
Returns:
|
|
TokenPayload if valid, None if invalid or expired.
|
|
|
|
Example:
|
|
payload = decode_token(token)
|
|
if payload:
|
|
user_id = UUID(payload.sub)
|
|
"""
|
|
try:
|
|
payload_dict = jwt.decode(
|
|
token,
|
|
settings.secret_key.get_secret_value(),
|
|
algorithms=[settings.jwt_algorithm],
|
|
)
|
|
return TokenPayload(**payload_dict)
|
|
except JWTError:
|
|
return None
|
|
|
|
|
|
def verify_access_token(token: str) -> uuid.UUID | None:
|
|
"""Verify an access token and extract the user ID.
|
|
|
|
Args:
|
|
token: The JWT access token to verify.
|
|
|
|
Returns:
|
|
User UUID if valid access token, None otherwise.
|
|
|
|
Example:
|
|
user_id = verify_access_token(token)
|
|
if user_id:
|
|
user = await user_service.get_user_by_id(db, user_id)
|
|
"""
|
|
payload = decode_token(token)
|
|
if payload is None:
|
|
return None
|
|
|
|
if payload.type != TokenType.ACCESS:
|
|
return None
|
|
|
|
try:
|
|
return uuid.UUID(payload.sub)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def verify_refresh_token(token: str) -> tuple[uuid.UUID, str] | None:
|
|
"""Verify a refresh token and extract user ID and JTI.
|
|
|
|
The JTI should be checked against the token store to ensure
|
|
the token hasn't been revoked.
|
|
|
|
Args:
|
|
token: The JWT refresh token to verify.
|
|
|
|
Returns:
|
|
Tuple of (user UUID, JTI) if valid refresh token, None otherwise.
|
|
|
|
Example:
|
|
result = verify_refresh_token(token)
|
|
if result:
|
|
user_id, jti = result
|
|
if await token_store.is_token_valid(user_id, jti):
|
|
# Issue new access token
|
|
"""
|
|
payload = decode_token(token)
|
|
if payload is None:
|
|
return None
|
|
|
|
if payload.type != TokenType.REFRESH:
|
|
return None
|
|
|
|
if payload.jti is None:
|
|
return None
|
|
|
|
try:
|
|
user_id = uuid.UUID(payload.sub)
|
|
return user_id, payload.jti
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def get_token_expiration_seconds() -> int:
|
|
"""Get the access token expiration time in seconds.
|
|
|
|
Useful for the expires_in field in token responses.
|
|
|
|
Returns:
|
|
Access token lifetime in seconds.
|
|
"""
|
|
return settings.jwt_expire_minutes * 60
|
|
|
|
|
|
def get_refresh_token_expiration() -> datetime:
|
|
"""Get the expiration datetime for a new refresh token.
|
|
|
|
Useful for setting TTL when storing in Redis.
|
|
|
|
Returns:
|
|
Datetime when a refresh token created now would expire.
|
|
"""
|
|
return datetime.now(UTC) + timedelta(days=settings.jwt_refresh_expire_days)
|