-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.py
More file actions
69 lines (54 loc) · 2.47 KB
/
Copy pathauth.py
File metadata and controls
69 lines (54 loc) · 2.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""JWT authentication utilities: password hashing, token creation, and user resolution."""
from datetime import datetime, timedelta, timezone
from typing import Annotated
import structlog
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
import bcrypt
from models.auth import TokenData
from models.user import UserOut
from repositories.sqlalchemy import get_user_repo, SqlUserRepository
logger = structlog.get_logger()
# In production, load this from environment variables
SECRET_KEY = "change-me-in-production-use-a-long-random-string"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
def hash_password(password: str) -> str:
"""Return a bcrypt hash of the given plain-text password."""
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Check a plain-text password against its bcrypt hash."""
return bcrypt.checkpw(plain_password.encode(), hashed_password.encode())
def create_access_token(data: dict) -> str:
"""Create a signed JWT with an expiration claim."""
payload = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
payload.update({"exp": expire})
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
user_repo: SqlUserRepository = Depends(get_user_repo),
) -> UserOut:
"""Decode the Bearer token and return the authenticated user, or raise 401."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
logger.warning("auth_failed", reason="missing_sub_claim")
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
logger.warning("auth_failed", reason="invalid_jwt")
raise credentials_exception
user = user_repo.get_by_username(token_data.username)
if user is None:
logger.warning("auth_failed", reason="user_not_found", username=token_data.username)
raise credentials_exception
return user