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}}"}'{
"AccessToken": "<AccessToken>",
"RefreshToken": "<RefreshToken>"
}Livslängder#
| Token | Livslängd | Egenskap |
|---|---|---|
| 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-token | upp till 30 dagar | Engå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.
curl -X POST https://staging.zesec.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken": "<RefreshToken>"}'{
"AccessToken": "<AccessToken>",
"RefreshToken": "<RefreshToken>"
}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äge | Vad hände | Gör så här |
|---|---|---|
Anrop svarar 401 | Access-token har gått ut eller är ogiltig | Kör en refresh. Lyckas den: gör om det ursprungliga anropet en gång. |
Refresh svarar 2xx | Nytt par utfärdat | Spara båda tokens atomiskt och fortsätt. |
Refresh svarar 401/400 | Refresh-token är förbrukad eller utgången | Full ominloggning med {{phoneNumber}}/{{password}}. Retry:a inte samma refresh-token. |
| Ominloggning misslyckas | Uppgifterna är fel, kontot spärrat eller API:t nere | Sluta försöka. Larma en operatör. En loop här låser ute kontot. |
Anrop svarar 401 igen efter refresh | Sessionen togs över av en annan inloggning | Sluta försöka. Se En aktiv session per konto. |
Nätverksfel / 5xx på refresh | Tillfälligt fel — token kan vara förbrukad eller inte | Backa av och försök en gång. Fortsätter det: ominloggning. |
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