File size: 1,906 Bytes
165a274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90adb95
 
 
 
 
 
165a274
 
 
 
 
 
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
"""Password hashing and JWT helpers for authentication.

Pure functions — no FastAPI imports — so they stay unit-testable. bcrypt for
password storage (via passlib), PyJWT for short-lived HS256 access tokens.
"""
from datetime import datetime, timedelta, timezone
from typing import Optional

import jwt
from passlib.context import CryptContext

from . import config

_pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# A throwaway hash to verify against when a login email is unknown, so that the
# "no such user" path costs roughly the same time as "wrong password" (this
# blunts user-enumeration via timing).
_DUMMY_HASH = _pwd_context.hash("dummy-password-for-constant-time-compare")


def hash_password(password: str) -> str:
    return _pwd_context.hash(password)


def verify_password(password: str, password_hash: Optional[str]) -> bool:
    """Verify a password against a stored hash. When no hash is given (unknown
    user) we still run a compare against a dummy hash and return False, keeping
    timing roughly constant."""
    if password_hash is None:
        _pwd_context.verify(password, _DUMMY_HASH)
        return False
    return _pwd_context.verify(password, password_hash)


def create_access_token(subject: str, expires_minutes: Optional[int] = None) -> str:
    minutes = expires_minutes or config.ACCESS_TOKEN_EXPIRE_MINUTES
    now = datetime.now(timezone.utc)
    payload = {
        "sub": subject,
        "type": "access",  # so a future token kind can't be replayed as an access token
        "iat": now,
        "exp": now + timedelta(minutes=minutes),
    }
    return jwt.encode(payload, config.AUTH_SECRET_KEY, algorithm=config.AUTH_ALGORITHM)


def decode_token(token: str) -> dict:
    """Decode and validate a JWT. Raises jwt.PyJWTError on any problem."""
    return jwt.decode(token, config.AUTH_SECRET_KEY, algorithms=[config.AUTH_ALGORITHM])