Autentisering & tokens

Den här sidan avgör om integrationen blir stabil. Läs den innan ni skriver den första raden klientkod.

Inloggning#

Logga in med telefonnummer och lösenord via POST /api/auth/login-by-phone. Svaret innehåller en access-token (en JWT) och en refresh-token.

curl -X POST https://staging.zesec.com/api/auth/login-by-phone \
  -H "Content-Type: application/json" \
  -d '{"phoneNumber": "{{phoneNumber}}", "password": "{{password}}"}'
Svar
{
  "AccessToken": "<AccessToken>",
  "RefreshToken": "<RefreshToken>"
}

Livslängder#

TokenLivslängdEgenskap
Access-token (JWT)~2 dagar (48 h)Skickas som Authorization: Bearer <AccessToken>. Exakt utgång står i exp i JWT:n — den är auktoritativ.
Refresh-tokenupp till 30 dagarEngångs och roterande. En refresh returnerar både ny access- och ny refresh-token, och den gamla dör omedelbart.

Planera för proaktiv förnyelse var 36:e timme, eller läs exp ur access-token och förnya några timmar innan den går ut. Det är samma intervall i hela portalen — hittar ni en annan siffra någonstans är det ett fel, rapportera det.

Token-refresh#

POST /api/auth/refresh med den nuvarande refresh-token i bodyn. Ingen Authorization-header behövs.

Begäran
curl -X POST https://staging.zesec.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "<RefreshToken>"}'
Svar
{
  "AccessToken": "<AccessToken>",
  "RefreshToken": "<RefreshToken>"
}
Spara båda i en skrivning
async function rotate(currentRefreshToken: string) {
  const res = await fetch(`${BASE_URL}/auth/refresh`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken: currentRefreshToken }),
  });
  if (!res.ok) throw new RefreshFailed(res.status);

  const { AccessToken, RefreshToken } = await res.json();
  // One write. Never persist the access token without the refresh token that came with it:
  // the token you sent is already dead, so a partial save loses the session for good.
  await session.replace({ accessToken: AccessToken, refreshToken: RefreshToken, rotatedAt: Date.now() });
  return AccessToken;
}

Var tokens ska lagras#

  • Server-side, i er backend. Aldrig i en webbklient, mobilapp eller i frontend-kod.
  • En delad lagring per API-konto (databas, Redis eller motsvarande) så att alla instanser ser samma par.
  • Skriv paret atomiskt. En halv skrivning förlorar sessionen permanent.
  • Logga aldrig tokenvärden. Logga rotationstidpunkter, inte innehåll.

Beslutstabell vid fel#

Det finns exakt två felvägar att implementera, och de ska hanteras olika.

LägeVad händeGör så här
Anrop svarar 401Access-token har gått ut eller är ogiltigKör en refresh. Lyckas den: gör om det ursprungliga anropet en gång.
Refresh svarar 2xxNytt par utfärdatSpara båda tokens atomiskt och fortsätt.
Refresh svarar 401/400Refresh-token är förbrukad eller utgångenFull ominloggning med {{phoneNumber}}/{{password}}. Retry:a inte samma refresh-token.
Ominloggning misslyckasUppgifterna är fel, kontot spärrat eller API:t nereSluta försöka. Larma en operatör. En loop här låser ute kontot.
Anrop svarar 401 igen efter refreshSessionen togs över av en annan inloggningSluta försöka. Se En aktiv session per konto.
Nätverksfel / 5xx på refreshTillfälligt fel — token kan vara förbrukad eller inteBacka av och försök en gång. Fortsätter det: ominloggning.
Komplett tokenhanterare
const PROACTIVE_REFRESH_MS = 36 * 60 * 60 * 1000;

let inFlight: Promise<string> | null = null;

/** Single active session per account: one process, one token pair, one refresh at a time. */
export async function getValidAccessToken(): Promise<string> {
  const current = await session.read();

  if (current && Date.now() - current.rotatedAt < PROACTIVE_REFRESH_MS) {
    return current.accessToken;
  }

  // Single-flight guard: concurrent callers must not each burn a single-use refresh token.
  inFlight ??= (async () => {
    try {
      return current ? await rotate(current.refreshToken) : await login();
    } catch (err) {
      if (err instanceof RefreshFailed) return await login(); // refresh token spent or expired
      throw err;
    } finally {
      inFlight = null;
    }
  })();

  return inFlight;
}

/** Wrap every API call: one refresh, one replay, then surface the failure. */
export async function apiCall(path: string, init: RequestInit = {}) {
  let token = await getValidAccessToken();
  let res = await fetch(`${BASE_URL}${path}`, withAuth(init, token));

  if (res.status === 401) {
    await session.invalidateAccessToken();
    token = await getValidAccessToken();
    res = await fetch(`${BASE_URL}${path}`, withAuth(init, token));
  }
  return res; // still 401 -> stop and alert an operator; do not loop
}

En aktiv session per konto#

Ett API-konto har en session. En ny inloggning på samma konto avlivar den föregående. Kör därför inte flera klienter som loggar in med samma konto — låt backend vara den enda som loggar in, och avsluta sessioner ni är klara med via POST /api/auth/logout.

Checklista#

  • Inloggning sker server-side, inte vid varje öppning
  • Proaktiv förnyelse var 36:e timme eller utifrån exp
  • Både access- och refresh-token sparas i samma atomiska skrivning
  • 401 → refresh → gör om anropet en gång
  • Misslyckad refresh → ominloggning, aldrig retry på samma token
  • Single-flight-lås runt förnyelsen
  • Tokens lagras server-side och loggas aldrig

Vidare

Kopiera sidan som Markdown: Visa som Markdown