mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-03 07:18:25 +00:00
Port RFC-001 authentication core from PR #1728: - JWT token handling (create_access_token, decode_token, TokenPayload) - Password hashing (bcrypt) with verify_password - SQLite UserRepository with base interface - Provider Factory pattern (LocalAuthProvider) - CLI reset_admin tool - Auth-specific errors (AuthErrorCode, TokenError, AuthErrorResponse) Deps: - bcrypt>=4.0.0 - pyjwt>=2.9.0 - email-validator>=2.0.0 - backend/uv.toml pins public PyPI index Tests: 12 pure unit tests (test_auth_config.py, test_auth_errors.py). Scope note: authz.py, test_auth.py, and test_auth_type_system.py are deferred to commit 2 because they depend on middleware and deps wiring that is not yet in place. Commit 1 stays "pure new files only" as the spec mandates.
56 lines
1.6 KiB
Python
56 lines
1.6 KiB
Python
"""JWT token creation and verification."""
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
|
|
import jwt
|
|
from pydantic import BaseModel
|
|
|
|
from app.gateway.auth.config import get_auth_config
|
|
from app.gateway.auth.errors import TokenError
|
|
|
|
|
|
class TokenPayload(BaseModel):
|
|
"""JWT token payload."""
|
|
|
|
sub: str # user_id
|
|
exp: datetime
|
|
iat: datetime | None = None
|
|
ver: int = 0 # token_version — must match User.token_version
|
|
|
|
|
|
def create_access_token(user_id: str, expires_delta: timedelta | None = None, token_version: int = 0) -> str:
|
|
"""Create a JWT access token.
|
|
|
|
Args:
|
|
user_id: The user's UUID as string
|
|
expires_delta: Optional custom expiry, defaults to 7 days
|
|
token_version: User's current token_version for invalidation
|
|
|
|
Returns:
|
|
Encoded JWT string
|
|
"""
|
|
config = get_auth_config()
|
|
expiry = expires_delta or timedelta(days=config.token_expiry_days)
|
|
|
|
now = datetime.now(UTC)
|
|
payload = {"sub": user_id, "exp": now + expiry, "iat": now, "ver": token_version}
|
|
return jwt.encode(payload, config.jwt_secret, algorithm="HS256")
|
|
|
|
|
|
def decode_token(token: str) -> TokenPayload | TokenError:
|
|
"""Decode and validate a JWT token.
|
|
|
|
Returns:
|
|
TokenPayload if valid, or a specific TokenError variant.
|
|
"""
|
|
config = get_auth_config()
|
|
try:
|
|
payload = jwt.decode(token, config.jwt_secret, algorithms=["HS256"])
|
|
return TokenPayload(**payload)
|
|
except jwt.ExpiredSignatureError:
|
|
return TokenError.EXPIRED
|
|
except jwt.InvalidSignatureError:
|
|
return TokenError.INVALID_SIGNATURE
|
|
except jwt.PyJWTError:
|
|
return TokenError.MALFORMED
|