# 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.

**Begäran**

_cURL_

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

_Node / TypeScript_

```typescript
const res = await fetch(`${BASE_URL}/auth/login-by-phone`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ phoneNumber: PHONE_NUMBER, password: PASSWORD }),
});
if (!res.ok) throw new Error(`login failed: ${res.status}`);
const { AccessToken, RefreshToken } = await res.json();
```

_Python_

```python
import requests

res = requests.post(
    f"{BASE_URL}/auth/login-by-phone",
    json={"phoneNumber": PHONE_NUMBER, "password": PASSWORD},
    timeout=10,
)
res.raise_for_status()
tokens = res.json()  # {"AccessToken": "...", "RefreshToken": "..."}
```

**Svar**

```json
{
  "AccessToken": "<AccessToken>",
  "RefreshToken": "<RefreshToken>"
}
```

> **Fältnamnens skiftläge** — Begäran följer OpenAPI-kontraktet och använder `phoneNumber`, `password` och `refreshToken`. Svarets tokenfält dokumenteras som `AccessToken` och `RefreshToken`. Läs svaret skiftlägestolerant om ni vill vara helt robusta.

## 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.

**Begäran**

```bash
curl -X POST https://staging.zesec.com/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "<RefreshToken>"}'
```

**Svar**

```json
{
  "AccessToken": "<AccessToken>",
  "RefreshToken": "<RefreshToken>"
}
```

> **Den gamla refresh-token är död i samma ögonblick** — Så fort svaret är skickat är den token ni skickade in ogiltig. Att spara den nya access-token men glömma den nya refresh-token är det vanligaste felet i den här integrationen — nästa förnyelse misslyckas och kontot måste logga in på nytt. Se [Felsökning & FAQ](/docs/troubleshooting#lost-refresh-token).

**Spara båda i en skrivning**

```typescript
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](#one-session-per-account). |
| 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. |

**Komplett tokenhanterare**

```typescript
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
}
```

> **Single-flight är inte valfritt** — Två samtidiga 401:or utan lås ger två parallella refresh-anrop. Det andra använder en token som det första just förbrukade, och sessionen dör. Guarden `inFlight` i exemplet ovan är det som förhindrar det.

## 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`.

> **Detta drabbar er egen testning** — Ett lokalt testskript som loggar in med produktionskontot slår ut produktionssessionen. Använd separata konton per miljö.

## 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**
- [Statuskoder & felsvar — hela katalogen](/docs/errors)
- [Gränser & retry — vad som får försökas om](/docs/limits)
- [Felsökning & FAQ](/docs/troubleshooting)
