"""A long-lived key to a short-lived one. An access token here lasts a day and cannot be withdrawn: it is a signed statement, and nothing consults a table before believing it. That is workable for a browser, which can send the person back to a login form, and no use at all to an app on a phone — which would have to keep the password to survive a day, and that is the one thing a client must never store. So a refresh token is a row, not a signature. It can be listed, it can be withdrawn, and it is stored the way a password is: only the hash, because a database that leaks should not hand over live sessions with it. Rotation on every use is what makes a stolen one survivable. Each refresh issues a new token and marks the old one used; if a thief spends it first, the real client's next attempt presents a token already spent, and the whole family is withdrawn — the theft turns itself in. """ from datetime import datetime from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from app.database import Base class RefreshToken(Base): __tablename__ = "refresh_tokens" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) #: SHA-256 of the token. The token itself is shown once, at issue. token_hash = Column(String(64), unique=True, nullable=False, index=True) #: Every token descended from one sign-in shares this. Withdrawing a family #: ends the session, however many times it has been rotated. family = Column(String(32), nullable=False, index=True) #: What asked for it, as the client described itself. For the list of #: sessions a person is shown when they ask where they are signed in. label = Column(String(120), nullable=True) created_at = Column(DateTime, default=datetime.utcnow) expires_at = Column(DateTime, nullable=False) #: Set when it is spent. A spent token presented again is theft or a bug, #: and both are handled the same way: end the family. used_at = Column(DateTime, nullable=True) revoked_at = Column(DateTime, nullable=True) last_ip = Column(String(64), nullable=True)