Bootcamp i FastAPI-backendudvikling · Lektion

Refresh tokens og tokenrotation

Design kortlivede access tokens med roterende refresh tokens og tilbagekaldelse på serversiden for at begrænse token-tyveri.

Lektion 3 af 413 trin

Refresh tokens og tokenrotation er en gratis Bootcamp i FastAPI-backendudvikling-lektion på CoddyKit. Dette er lektion 3 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i Bootcamp i FastAPI-backendudvikling, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Bootcamp i FastAPI-backendudvikling-kurset indeholder 4 lektioner i alt.

Hvorfor to tokens?

Et enkelt JWT-adgangstoken med lang levetid er praktisk, men farligt: Hvis det lækkes, kan en angriber bruge det, indtil det udløber. Da JWT'er er tilstandsløse, kan du ikke nemt tilbagekalde et token undervejs.

OAuth2's standardløsning er at dele ansvaret mellem to tokens:

  • Adgangstoken — kort levetid (5-15 minutter), sendes med hver forespørgsel og verificeres kun med signaturen (ingen databaseforespørgsel).
  • Fornyelsestoken — lang levetid (dage/uger), bruges kun til at hente et nyt adgangstoken og registreres på serversiden, så det kan tilbagekaldes.

På den måde er et stjålet adgangstoken ubrugeligt efter få minutter, mens brugerne stadig forbliver logget ind i lang tid.

Tokenparrets opbygning

Login-endpointet returnerer begge tokens. Klienten opbevarer adgangstokenet i hukommelsen og fornyelsestokenet et mere beskyttet sted (f.eks. i en HttpOnly-cookie).

Bemærk de meget forskellige udløbsperioder. Adgangstokenet er med vilje kortlivet, så et læk får et meget begrænset skadeomfang.

from datetime import datetime, timedelta, timezone

ACCESS_TOKEN_TTL = timedelta(minutes=15)
REFRESH_TOKEN_TTL = timedelta(days=7)

def expiry(ttl: timedelta) -> str:
    return (datetime.now(timezone.utc) + ttl).isoformat()

login_response = {
    "access_token": "<jwt>",
    "access_expires": expiry(ACCESS_TOKEN_TTL),
    "refresh_token": "<opaque-or-jwt>",
    "refresh_expires": expiry(REFRESH_TOKEN_TTL),
    "token_type": "bearer",
}

for k, v in login_response.items():
    print(f"{k}: {v}")

Signering af et adgangstoken

Adgangstokens er JWT'er, der er signeret med din hemmelige nøgle. De indeholder et sub (bruger-id), et exp-claim og et type-claim, så serveren kan afvise et fornyelsestoken, hvor der forventes et adgangstoken.

Dette kodestykke bruger python-jose, som er det bibliotek, de fleste FastAPI-vejledninger bygger på. Det afhænger af en ekstern pakke, så det kan ikke køres selvstændigt her.

from datetime import datetime, timedelta, timezone
from jose import jwt

SECRET = "change-me"
ALGO = "HS256"

def create_access_token(user_id: str) -> str:
    now = datetime.now(timezone.utc)
    payload = {
        "sub": user_id,
        "type": "access",
        "iat": now,
        "exp": now + timedelta(minutes=15),
    }
    return jwt.encode(payload, SECRET, algorithm=ALGO)

Fornyelsestokenet kræver tilstand

Et adgangstoken verificeres alene med signaturen — der kræves ingen database. Et fornyelsestoken er anderledes: For at understøtte tilbagekaldelse og rotation skal serveren huske det.

Tricket er aldrig at gemme det rå fornyelsestoken. Gem kun en hash af det, ligesom med en adgangskode. Hvis din database lækkes, kan de gemte hashes ikke genbruges.

  • Generér en tilfældig streng med høj entropi som token.
  • Hash den (SHA-256), og gem hashen, bruger-id'et, udløbstiden og et revoked-flag.
  • Ved fornyelse hasher du det indkomne token og slår det op.
import hashlib, secrets

def new_refresh_token() -> tuple[str, str]:
    raw = secrets.token_urlsafe(48)          # give this to the client
    token_hash = hashlib.sha256(raw.encode()).hexdigest()  # store this
    return raw, token_hash

raw, stored = new_refresh_token()
print("client receives:", raw[:16], "...")
print("db stores hash :", stored[:16], "...")
print("lookup matches :", hashlib.sha256(raw.encode()).hexdigest() == stored)

Hvad er tokenrotation?

Rotation betyder, at et fornyelsestoken hver gang det bruges, bliver forbrugt og erstattet af et helt nyt. Et fornyelsestoken kan derfor kun bruges én gang.

Forløbet ved hver fornyelse:

  • Validér det præsenterede fornyelsestoken (findes, er ikke tilbagekaldt og er ikke udløbet).
  • Markér det som tilbagekaldt/brugt.
  • Udsted et nyt adgangstoken og et nyt fornyelsestoken.
  • Returnér det nye par til klienten.

Uden rotation virker et stjålet fornyelsestoken i hele sin levetid. Med rotation ændres det, når det bruges — og det er netop det, der gør det muligt at opdage tyveri.

Modellering af det gemte token

Her er en minimal model af lageret til fornyelsestokens i hukommelsen, så du kan se de enkelte dele, før du kobler en rigtig database på. Hver post kender sin ejer, udløbstid og om den er blevet brugt eller tilbagekaldt.

I produktion svarer dette til en SQL-tabel (med hashen som nøgle) eller en Redis-post med en TTL.

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone

@dataclass
class RefreshRecord:
    token_hash: str
    user_id: str
    expires_at: datetime
    revoked: bool = False

    def is_valid(self, now: datetime) -> bool:
        return not self.revoked and now < self.expires_at

now = datetime.now(timezone.utc)
rec = RefreshRecord("abc123", "user-7", now + timedelta(days=7))
print("valid now      :", rec.is_valid(now))
rec.revoked = True
print("valid revoked  :", rec.is_valid(now))

Rotationslogikken

Dette er lektionens kerne: en ren funktion, der tager et præsenteret fornyelsestoken, validerer det, tilbagekalder det og udsteder en erstatning. Intet framework er involveret — kun algoritmen.

Kør den: Den første fornyelse lykkes og returnerer et nyt token; hvis du bruger det gamle token igen bagefter, mislykkes det, fordi det er blevet roteret væk.

import hashlib, secrets
from datetime import datetime, timedelta, timezone

store = {}  # token_hash -> {user_id, exp, revoked}

def _hash(raw): return hashlib.sha256(raw.encode()).hexdigest()

def issue(user_id):
    raw = secrets.token_urlsafe(32)
    store[_hash(raw)] = {
        "user_id": user_id,
        "exp": datetime.now(timezone.utc) + timedelta(days=7),
        "revoked": False,
    }
    return raw

def rotate(raw):
    rec = store.get(_hash(raw))
    now = datetime.now(timezone.utc)
    if not rec or rec["revoked"] or now >= rec["exp"]:
        raise ValueError("invalid refresh token")
    rec["revoked"] = True            # consume the old one
    return issue(rec["user_id"])     # mint a fresh one

old = issue("user-7")
new = rotate(old)
print("rotated to new token:", new[:12], "...")
try:
    rotate(old)
except ValueError as e:
    print("reuse of old token blocked:", e)

Registrering af token-tyveri ved genbrug

Rotation giver et stærkt sikkerhedssignal. Hvis et fornyelsestoken, der allerede er brugt, dukker op igen, er der kun to forklaringer:

  • Den legitime klient modtog aldrig det nye token (sjældent), eller
  • En angriber stjal det gamle token og genbruger det.

Da du ikke kan afgøre hvilken forklaring der gælder, er den sikre reaktion at betragte genbruget som et brud på hele tokenfamilien: tilbagekald alle fornyelsestokens for brugeren (eller den pågældende sessionslinje), og kræv nyt login.

Dette kaldes automatisk registrering af genbrug og anbefales i OAuth2 Security Best Current Practice (RFC 9700).

Registrering af genbrug i kode

For at registrere genbrug vedligeholder vi en tokenfamilie pr. bruger. Ved normal rotation tilbagekaldes ét token, og dets efterfølger tilføjes. Hvis et token med used=True præsenteres igen, sletter vi hele familien.

Kør det for at se, hvordan genbrug af et stjålet token udløser sletning af hele familien.

import secrets

family = {}  # token -> {"used": bool}

def issue():
    t = secrets.token_urlsafe(16)
    family[t] = {"used": False}
    return t

def rotate(t):
    rec = family.get(t)
    if rec is None:
        raise ValueError("unknown token")
    if rec["used"]:
        family.clear()  # reuse detected -> revoke whole family
        raise ValueError("REUSE DETECTED: all sessions revoked")
    rec["used"] = True
    return issue()

t1 = issue()
t2 = rotate(t1)      # normal rotation
print("rotation ok, new token issued")
try:
    rotate(t1)       # attacker replays the stolen old token
except ValueError as e:
    print(e)
print("tokens remaining:", len(family))

FastAPI-endpointet til fornyelse

Nu kommer FastAPI-koblingen. Fornyelsestokenet ankommer i forespørgslens brødtekst (eller i en HttpOnly-cookie). Endpointet roterer det og returnerer et nyt par.

De vigtigste valg, du kan se her:

  • Afvis alt, der ikke er en token-type på refresh.
  • Returnér 401 ved enhver valideringsfejl — læk aldrig årsagen.
  • Udsted altid et nyt fornyelsestoken sammen med adgangstokenet.

Dette afhænger af FastAPI og dit lager, så det er illustrativ kode og kan ikke køres selvstændigt.

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel

router = APIRouter()

class RefreshIn(BaseModel):
    refresh_token: str

class TokenPair(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "bearer"

@router.post("/auth/refresh", response_model=TokenPair)
async def refresh(body: RefreshIn):
    try:
        user_id = validate_and_consume(body.refresh_token)  # raises on reuse/expiry
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid refresh token",
        )
    return TokenPair(
        access_token=create_access_token(user_id),
        refresh_token=create_refresh_token(user_id),
    )

Lagring og tilbagekaldelse på serversiden

Tilstand på serversiden gør tilbagekaldelse mulig. En logout, en adgangskodeændring eller et registreret sikkerhedsbrud bør straks ugyldiggøre fornyelsestokens.

Praktiske retningslinjer:

  • Hvor: En SQL-tabel for holdbarhed eller Redis med en TTL, der svarer til tokenets levetid, for hastighed og automatisk udløb.
  • Hvad skal gemmes: SHA-256-hashen, bruger-id, udløbstidspunkt, et revoked-flag og eventuelt et family_id til registrering af genbrug.
  • Logout: Markér det præsenterede token (og eventuelt hele dets familie) som tilbagekaldt.
  • Global logout: Tilbagekald alle brugerens tokens — f.eks. efter en nulstilling af adgangskoden.

Adgangstokens forbliver tilstandsløse og udløber ganske enkelt af sig selv inden for få minutter, og derfor er korte TTL'er så vigtige.

def logout(token_hash: str, conn) -> None:
    conn.execute(
        "UPDATE refresh_tokens SET revoked = TRUE WHERE token_hash = %s",
        (token_hash,),
    )

def revoke_all_for_user(user_id: str, conn) -> None:
    conn.execute(
        "UPDATE refresh_tokens SET revoked = TRUE WHERE user_id = %s",
        (user_id,),
    )

Hurtigt tjek

Test din forståelse af den centrale beslutning om rotation.

Opsummering

Du ved nu, hvordan du bygger sikker sessionshåndtering med roterende fornyelsestokens:

  • To tokens: kortlivede, tilstandsløse adgangstokens (5-15 min.) samt langlivede fornyelsestokens, der spores på serversiden.
  • Hash, gem aldrig rå tokens: Gem kun SHA-256-hashen af fornyelsestokenet, ligesom med en adgangskode.
  • Rotation: Hver fornyelse bruger det gamle token (til engangsbrug) og udsteder et nyt par.
  • Registrering af genbrug: Et afspillet, allerede brugt token betyder sandsynligvis tyveri — tilbagekald hele familien, og kræv nyt login (RFC 9700).
  • Tilbagekaldelse: Tilstand på serversiden (SQL eller Redis med TTL) gør det muligt straks at ugyldiggøre tokens ved logout, ændring af adgangskode og håndtering af sikkerhedsbrud.

Resultatet er, at et stjålet adgangstoken udløber efter få minutter, et stjålet fornyelsestoken opdages ved første genafspilning, og brugerne stadig kan have langvarige sessioner.

Gratis at komme i gang

Lær Bootcamp i FastAPI-backendudvikling med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
21
Lektioner
84

Ofte stillede spørgsmål

Er lektionen “Refresh tokens og tokenrotation” gratis?

Ja — alle 3 lektioner i læringssporet Bootcamp i FastAPI-backendudvikling, inklusive “Refresh tokens og tokenrotation”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Bootcamp i FastAPI-backendudvikling-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Refresh tokens og tokenrotation”?

Design kortlivede access tokens med roterende refresh tokens og tilbagekaldelse på serversiden for at begrænse token-tyveri. Du øver dig i Bootcamp i FastAPI-backendudvikling med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Bootcamp i FastAPI-backendudvikling?

Der kræves ingen tidligere erfaring. Bootcamp i FastAPI-backendudvikling på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 3 af 4.

Hvor lang tid tager lektionen “Refresh tokens og tokenrotation”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Bootcamp i FastAPI-backendudvikling-lektion?

Ja. Alle Bootcamp i FastAPI-backendudvikling-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. OAuth2 Password Flow og udstedelse af tokens
  2. Signering og verificering af JWT'er med python-jose
  3. Refresh tokens og tokenrotation
  4. Scope-baseret autorisation og rolleguards
← Tilbage til Bootcamp i FastAPI-backendudvikling