Spaces:
Paused
Paused
Download apps/api/src/app/auth.py from nathycodes/presentercoach: direct link, hf CLI and curl.
- Browser
- Download file 9.09 kB
-
https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/api/src/app/auth.py
- Command line
-
hf download hf://spaces/nathycodes/presentercoach/apps/api/src/app/auth.py
-
curl -L -o auth.py https://huggingface.co/spaces/nathycodes/presentercoach/resolve/main/apps/api/src/app/auth.py
9.09 kB
| import hashlib | |
| import uuid | |
| from datetime import datetime, timedelta | |
| import bcrypt | |
| from fastapi import Body, Cookie, Depends, Header, HTTPException, Query, Response, status | |
| from fastapi.security import OAuth2PasswordBearer | |
| from jose import JWTError, jwt | |
| from pydantic import BaseModel | |
| from sqlalchemy.orm import Session | |
| from app import models | |
| from app.config import settings | |
| from app.db import get_db | |
| oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login", auto_error=False) | |
| REFRESH_COOKIE_NAME = "refresh_token" | |
| ACCESS_HEADER_NAME = "X-Presenter-Token" | |
| REFRESH_HEADER_NAME = "X-Refresh-Token" | |
| class TokenPayload(BaseModel): | |
| sub: str | |
| email: str | None = None | |
| role: str = "user" | |
| jti: str | |
| type: str | |
| def hash_password(password: str) -> str: | |
| return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("utf-8") | |
| def verify_password(password: str, hashed_password: str) -> bool: | |
| return bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8")) | |
| def _utcnow() -> datetime: | |
| return datetime.utcnow() | |
| def _hash_jti(jti: str) -> str: | |
| return hashlib.sha256(jti.encode("utf-8")).hexdigest() | |
| def _token_expire_delta(token_type: str) -> timedelta: | |
| if token_type == "refresh": | |
| return timedelta(days=settings.refresh_token_expire_days) | |
| return timedelta(minutes=settings.access_token_expire_minutes) | |
| def _create_token(user: models.User, token_type: str, jti: str | None = None) -> tuple[str, str, datetime]: | |
| token_jti = jti or str(uuid.uuid4()) | |
| expires_at = _utcnow() + _token_expire_delta(token_type) | |
| payload = { | |
| "sub": str(user.id), | |
| "email": user.email, | |
| "role": "admin" if user.is_admin else "user", | |
| "iat": _utcnow(), | |
| "exp": expires_at, | |
| "jti": token_jti, | |
| "type": token_type, | |
| } | |
| token = jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) | |
| return token, token_jti, expires_at | |
| def create_access_token(user: models.User) -> str: | |
| token, _jti, _expires_at = _create_token(user, "access") | |
| return token | |
| def create_refresh_token(db: Session, user: models.User) -> str: | |
| token, jti, expires_at = _create_token(user, "refresh") | |
| db.add( | |
| models.RefreshToken( | |
| user_id=user.id, | |
| jti_hash=_hash_jti(jti), | |
| expires_at=expires_at, | |
| ) | |
| ) | |
| db.commit() | |
| return token | |
| def set_refresh_cookie(response: Response, token: str) -> None: | |
| response.set_cookie( | |
| key=REFRESH_COOKIE_NAME, | |
| value=token, | |
| httponly=True, | |
| secure=settings.cookie_secure, | |
| samesite="lax", | |
| max_age=settings.refresh_token_expire_days * 24 * 60 * 60, | |
| path=settings.cookie_path, | |
| ) | |
| def clear_refresh_cookie(response: Response) -> None: | |
| response.delete_cookie( | |
| key=REFRESH_COOKIE_NAME, | |
| httponly=True, | |
| secure=settings.cookie_secure, | |
| samesite="lax", | |
| path=settings.cookie_path, | |
| ) | |
| def decode_token(token: str, expected_type: str = "access") -> TokenPayload: | |
| credentials_exception = HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Invalid or expired token", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| try: | |
| payload = jwt.decode( | |
| token, | |
| settings.jwt_secret, | |
| algorithms=[settings.jwt_algorithm], | |
| options={"require_exp": True, "require_iat": True}, | |
| ) | |
| except JWTError as exc: | |
| raise credentials_exception from exc | |
| if payload.get("type") != expected_type: | |
| raise credentials_exception | |
| try: | |
| return TokenPayload( | |
| sub=str(payload["sub"]), | |
| email=payload.get("email"), | |
| role=payload.get("role", "user"), | |
| jti=str(payload["jti"]), | |
| type=str(payload["type"]), | |
| ) | |
| except (KeyError, ValueError) as exc: | |
| raise credentials_exception from exc | |
| def authenticate_user(db: Session, email: str, password: str) -> models.User | None: | |
| user = db.query(models.User).filter(models.User.email == email).first() | |
| if not user or not verify_password(password, user.hashed_password): | |
| return None | |
| return user | |
| def issue_token_pair(db: Session, response: Response, user: models.User) -> dict: | |
| access_token = create_access_token(user) | |
| refresh_token = create_refresh_token(db, user) | |
| set_refresh_cookie(response, refresh_token) | |
| return { | |
| "access_token": access_token, | |
| "refresh_token": refresh_token, | |
| "token_type": "bearer", | |
| "expires_in": settings.access_token_expire_minutes * 60, | |
| "user": {"id": user.id, "email": user.email, "is_admin": user.is_admin}, | |
| } | |
| def rotate_refresh_token( | |
| db: Session, | |
| response: Response, | |
| refresh_token: str | None, | |
| ) -> dict: | |
| if not refresh_token: | |
| raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing refresh token") | |
| payload = decode_token(refresh_token, expected_type="refresh") | |
| token_row = ( | |
| db.query(models.RefreshToken) | |
| .filter(models.RefreshToken.jti_hash == _hash_jti(payload.jti)) | |
| .first() | |
| ) | |
| if token_row is None or token_row.revoked_at is not None or token_row.expires_at <= _utcnow(): | |
| raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") | |
| try: | |
| user_id = int(payload.sub) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") from exc | |
| user = db.query(models.User).filter(models.User.id == user_id).first() | |
| if user is None: | |
| raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") | |
| access_token = create_access_token(user) | |
| new_refresh_token, new_jti, new_expires_at = _create_token(user, "refresh") | |
| token_row.revoked_at = _utcnow() | |
| token_row.replaced_by_jti_hash = _hash_jti(new_jti) | |
| db.add( | |
| models.RefreshToken( | |
| user_id=user.id, | |
| jti_hash=_hash_jti(new_jti), | |
| expires_at=new_expires_at, | |
| ) | |
| ) | |
| db.commit() | |
| set_refresh_cookie(response, new_refresh_token) | |
| return { | |
| "access_token": access_token, | |
| "refresh_token": new_refresh_token, | |
| "token_type": "bearer", | |
| "expires_in": settings.access_token_expire_minutes * 60, | |
| "user": {"id": user.id, "email": user.email, "is_admin": user.is_admin}, | |
| } | |
| def revoke_refresh_token(db: Session, response: Response, refresh_token: str | None) -> None: | |
| if refresh_token: | |
| try: | |
| payload = decode_token(refresh_token, expected_type="refresh") | |
| except HTTPException: | |
| payload = None | |
| if payload is not None: | |
| token_row = ( | |
| db.query(models.RefreshToken) | |
| .filter(models.RefreshToken.jti_hash == _hash_jti(payload.jti)) | |
| .first() | |
| ) | |
| if token_row is not None and token_row.revoked_at is None: | |
| token_row.revoked_at = _utcnow() | |
| db.commit() | |
| clear_refresh_cookie(response) | |
| def get_current_user( | |
| db: Session = Depends(get_db), | |
| bearer_token: str | None = Depends(oauth2_scheme), | |
| header_token: str | None = Header(default=None, alias=ACCESS_HEADER_NAME), | |
| query_token: str | None = Query(default=None, alias="_pt"), | |
| ) -> models.User: | |
| credentials_exception = HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Could not validate credentials", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| candidates = [tok for tok in (header_token, query_token, bearer_token) if tok] | |
| if not candidates: | |
| raise HTTPException( | |
| status_code=status.HTTP_401_UNAUTHORIZED, | |
| detail="Not authenticated", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| payload: TokenPayload | None = None | |
| last_error: HTTPException = credentials_exception | |
| for candidate in candidates: | |
| try: | |
| payload = decode_token(candidate, expected_type="access") | |
| break | |
| except HTTPException as exc: | |
| last_error = exc | |
| if payload is None: | |
| raise last_error | |
| try: | |
| user_id = int(payload.sub) | |
| except ValueError as exc: | |
| raise credentials_exception from exc | |
| user = db.query(models.User).filter(models.User.id == user_id).first() | |
| if user is None: | |
| raise credentials_exception | |
| return user | |
| def get_refresh_cookie( | |
| refresh_token: str | None = Cookie(default=None, alias=REFRESH_COOKIE_NAME), | |
| header_refresh_token: str | None = Header(default=None, alias=REFRESH_HEADER_NAME), | |
| body_payload: dict | None = Body(default=None), | |
| ) -> str | None: | |
| body_token = None | |
| if isinstance(body_payload, dict): | |
| raw = body_payload.get("refresh_token") | |
| if isinstance(raw, str) and raw.strip(): | |
| body_token = raw.strip() | |
| return refresh_token or header_refresh_token or body_token | |