KOHAMI Docs
Getting started

Authentification

KOHAMI utilise un schéma d'authentification custom NestJS + JWT (cf. ADR apps/api/docs/decisions/div-005-auth-custom-nest.md). Aucun fournisseur tiers (Auth0, Clerk, Cognito), l'identité est gérée intégralement dans Postgres et Scaleway Secret Manager.

Trois rôles sont définis, chacun avec un périmètre d'endpoints distinct :

RôleEndpoints accessibles2FA requisDurée du JWT
senior/v1/livekit/token, /v1/widgets/*, /v1/consents, /v1/profileNon15 min (refresh 7 j)
aidant/v1/alerts/*, /v1/conversations, /v1/seniors/:id/... (RLS)Oui (TOTP)15 min (refresh 24 h)
adminTout, incluant /v1/admin/*, /v1/export/*, /v1/audit/*Oui (TOTP)15 min (refresh 24 h)

L'invariant INV-P05 (RLS, un senior ne voit que ses propres données) est appliqué côté Postgres : chaque connexion JWT injecte app.current_user_id via SET LOCAL, et chaque table sensible expose une policy USING (user_id = current_setting('app.current_user_id')).

Login, 3 endpoints

auth-endpoints.ts
type AuthLoginRequest = {
  email: string;
  password: string;
  role: "senior" | "aidant" | "admin";
  totpCode?: string;
};

type AuthLoginResponse = {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
  requiresTwoFactor?: true;
};

Les types canoniques vivent dans @kohami/shared/types/auth.

Senior, login simple

const response = await fetch(`${apiBaseUrl}/v1/auth/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "senior@example.com",
    password: "...",
    role: "senior",
  }),
});

Aidant ou admin, login plus TOTP

Le premier appel sans totpCode retourne requiresTwoFactor: true (HTTP 200, token absent). Le second appel inclut le code TOTP six chiffres généré par l'app Google Authenticator (ou équivalent).

const init = await fetch(`${apiBaseUrl}/v1/auth/login`, {
  method: "POST",
  body: JSON.stringify({ email, password, role: "aidant" }),
});
const initJson = (await init.json()) as { requiresTwoFactor?: true };

if (initJson.requiresTwoFactor) {
  const final = await fetch(`${apiBaseUrl}/v1/auth/login`, {
    method: "POST",
    body: JSON.stringify({ email, password, role: "aidant", totpCode: "123456" }),
  });
  const finalJson = (await final.json()) as AuthLoginResponse;
}

Le rollout 2FA TOTP est tracé dans l'ADR auth-2fa-totp-rollout.md (PR #466).

Refresh

L'accessToken est volontairement court (15 minutes). Pour le renouveler :

const response = await fetch(`${apiBaseUrl}/v1/auth/refresh`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ refreshToken }),
});

Le refreshToken est rotaté à chaque refresh, l'ancien est invalidé immédiatement (defense-in-depth contre le replay). Stockez toujours le nouveau token retourné.

Codes d'erreur

HTTPCauseAction
400 Bad RequestPayload mal forméVérifier le contrat Zod
401 UnauthorizedJWT manquant, expiré ou signature invalideRetenter un refresh, sinon login
403 ForbiddenJWT valide mais rôle insuffisant pour la routePas de retry possible
409 ConflictTOTP requis mais non fourniVoir requiresTwoFactor dans le body
429 Too Many RequestsRate limit auth dépasséBackoff exponentiel, voir Gestion d'erreurs

Le contrat NestJS standard est uniforme :

{
  "statusCode": 401,
  "message": "Token expired",
  "error": "Unauthorized"
}

Sécurité opérationnelle

  • Stockage côté tablette : expo-secure-store (iOS Keychain, Android KeyStore), jamais AsyncStorage
  • HTTPS only, aucun appel HTTP plain accepté côté back (CSP + HSTS)
  • Idle timeout, la session aidant expire après 30 minutes d'inactivité observable
  • Audit log, toute opération sensible est tracée, voir /v1/audit/... (admin only)