feat(auth): add get_current_user_from_api_token dependency

- Validates API tokens (or_api_* prefix)
- SHA-256 hash lookup in api_tokens table
- Updates last_used_at on each request
- Distinguishes from JWT tokens (401 with clear error)
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 16:15:34 +02:00
parent a8095f4df7
commit 3253293dd4
2 changed files with 98 additions and 4 deletions

View File

@@ -1,4 +1,22 @@
"""Dependencies package for OpenRouter Monitor."""
from openrouter_monitor.dependencies.auth import get_current_user, security
from openrouter_monitor.dependencies.auth import (
get_current_user,
get_current_user_from_api_token,
security,
api_token_security,
)
from openrouter_monitor.dependencies.rate_limit import (
RateLimiter,
rate_limit_dependency,
rate_limiter,
)
__all__ = ["get_current_user", "security"]
__all__ = [
"get_current_user",
"get_current_user_from_api_token",
"security",
"api_token_security",
"RateLimiter",
"rate_limit_dependency",
"rate_limiter",
]

View File

@@ -1,20 +1,25 @@
"""Authentication dependencies.
T21: get_current_user dependency for protected endpoints.
T36: get_current_user_from_api_token dependency for public API endpoints.
"""
import hashlib
from datetime import datetime
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError
from sqlalchemy.orm import Session
from openrouter_monitor.database import get_db
from openrouter_monitor.models import User
from openrouter_monitor.models import User, ApiToken
from openrouter_monitor.schemas import TokenData
from openrouter_monitor.services import decode_access_token
# HTTP Bearer security scheme
# HTTP Bearer security schemes
security = HTTPBearer()
api_token_security = HTTPBearer(auto_error=False)
async def get_current_user(
@@ -77,3 +82,74 @@ async def get_current_user(
)
return user
async def get_current_user_from_api_token(
credentials: HTTPAuthorizationCredentials = Depends(api_token_security),
db: Session = Depends(get_db)
) -> User:
"""Get current authenticated user from API token (for public API endpoints).
This dependency extracts the API token from the Authorization header,
verifies it against the database, updates last_used_at, and returns
the corresponding user.
API tokens start with 'or_api_' prefix and are different from JWT tokens.
Args:
credentials: HTTP Authorization credentials containing the Bearer token
db: Database session
Returns:
The authenticated User object
Raises:
HTTPException: 401 if token is invalid, inactive, or user not found/inactive
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API token",
headers={"WWW-Authenticate": "Bearer"},
)
# Check if credentials were provided
if credentials is None:
raise credentials_exception
token = credentials.credentials
# Check if token looks like an API token (starts with 'or_api_')
# JWT tokens don't have this prefix
if not token.startswith("or_api_"):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type. Use API token, not JWT.",
headers={"WWW-Authenticate": "Bearer"},
)
# Hash the token with SHA-256 for lookup
token_hash = hashlib.sha256(token.encode()).hexdigest()
# Look up the token in the database
api_token = db.query(ApiToken).filter(
ApiToken.token_hash == token_hash,
ApiToken.is_active == True
).first()
if not api_token:
raise credentials_exception
# Update last_used_at timestamp
api_token.last_used_at = datetime.utcnow()
db.commit()
# Get the user associated with this token
user = db.query(User).filter(
User.id == api_token.user_id,
User.is_active == True
).first()
if not user:
raise credentials_exception
return user