From 3253293dd48a28a6c7ef1a1a0465ad12df3aa3c1 Mon Sep 17 00:00:00 2001 From: Luca Sacchi Ricciardi Date: Tue, 7 Apr 2026 16:15:34 +0200 Subject: [PATCH] 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) --- .../dependencies/__init__.py | 22 ++++- src/openrouter_monitor/dependencies/auth.py | 80 ++++++++++++++++++- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/openrouter_monitor/dependencies/__init__.py b/src/openrouter_monitor/dependencies/__init__.py index f91cd59..1022a70 100644 --- a/src/openrouter_monitor/dependencies/__init__.py +++ b/src/openrouter_monitor/dependencies/__init__.py @@ -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", +] diff --git a/src/openrouter_monitor/dependencies/auth.py b/src/openrouter_monitor/dependencies/auth.py index d7fe56c..01ee1e6 100644 --- a/src/openrouter_monitor/dependencies/auth.py +++ b/src/openrouter_monitor/dependencies/auth.py @@ -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