from sqlalchemy import String, Integer, ForeignKey, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column from datetime import datetime from app.database import Base class TOTPUsage(Base): __tablename__ = "totp_usage" id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column( ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True ) # The 6-digit code that was used code: Mapped[str] = mapped_column(String(6), nullable=False) # The TOTP time window in which the code was valid (floor(unix_time / 30)) window: Mapped[int] = mapped_column(Integer, nullable=False) used_at: Mapped[datetime] = mapped_column(default=func.now()) # NOTE on replay prevention design: # The unique constraint is on (user_id, code, window) — NOT (user_id, window). # This allows a user to present the T-1 window code and then the T window code # consecutively — these are different 6-digit OTPs, so both should succeed. # Constraining only (user_id, window) would reject the second legitimate code. # The actual attack we prevent is reusing the *same code string in the same window*, # which is the only true replay. This is compliant with RFC 6238 §5.2. # The 5-minute MFA token expiry provides the outer time bound on any abuse window. __table_args__ = ( UniqueConstraint("user_id", "code", "window", name="uq_totp_user_code_window"), )