écrits/tutorial/2026/08
Tutorial12 août 2026·27 min

Intégrer Nafath (النفاذ الوطني) OAuth2/OIDC en TypeScript

Guide pas à pas pour intégrer la plateforme nationale d'identité numérique saoudienne (Nafath) dans votre application TypeScript via OAuth2/OIDC. Couvre l'enregistrement API, le flux d'autorisation, la vérification du token JWT, l'extraction des données d'identité et l'alternative Keycloak.

Chaque application ciblant le marché saoudien finit par rencontrer le même obstacle : vos utilisateurs doivent vérifier leur identité nationale, et le standard de référence est Nafath (نفاذ) — la couche SSO nationale exploitée par SDAIA, déjà installée sur les téléphones de 16,3 millions de Saoudiens.

Ce n'est pas une intégration "Se connecter avec Google". Nafath est relié aux données d'état civil vérifiées du Centre national d'information (NIC). Lorsqu'un utilisateur s'authentifie via Nafath, vous recevez son numéro d'identité nationale, son nom légal, son numéro de téléphone vérifié et sa date de naissance — tous confirmés par les registres NIC. Pour les plateformes soumises à la conformité réglementaire, la vérification des locataires, le KYC e-commerce ou la prestation de services gouvernementaux, c'est la seule intégration qui compte vraiment.

Le problème : il n'existe pas de portail développeur public avec un guide de démarrage rapide. La recherche sur le web remonte des communiqués SDAIA, des pages de politique gouvernementale et des annonces sur mostaql.com demandant "veuillez intégrer Nafath sur notre plateforme". Ce guide comble cette lacune.

Prérequis

Avant de commencer, assurez-vous de disposer de :

  • Node.js 20+ et TypeScript 5.x
  • Un projet Next.js 14+ (ou tout framework TypeScript serveur — Express, Fastify, Hono)
  • Une application Nafath API approuvée (expliqué à l'Étape 1)
  • Une compréhension de base du flux OAuth2 (authorization code flow)
  • Un endpoint HTTPS pour votre redirect URI — Nafath rejette http://localhost en production

Ce que vous allez construire

Un flux d'authentification Nafath complet pour une application Next.js :

  1. Un bouton "Se connecter avec Nafath" qui redirige vers le SSO national
  2. Une route de callback OAuth2 qui échange le code d'autorisation contre des tokens
  3. Une couche de vérification JWT qui valide la signature du token d'identité
  4. Un gestionnaire de session qui extrait et stocke les données d'identité vérifiées
  5. Une route protégée accessible uniquement aux utilisateurs avec une identité saoudienne confirmée

Comment fonctionne Nafath

Nafath utilise le service iDART (Identity Access and Rights Token) — le fournisseur d'identité OIDC de SDAIA. Le flux est un OAuth2 standard avec une différence essentielle : au lieu de saisir un mot de passe, l'utilisateur ouvre l'application Nafath sur son téléphone et approuve la demande de connexion par biométrie ou code PIN.

Votre App              iDART / Nafath OIDC          Téléphone
   |                         |                           |
   |-- URL d'autorisation -> |                           |
   |                         |-- Notification push ----> |
   |                         |             [Approuver]   |
   |                         | <-- Confirmation bio -----|
   | <-- code (redirect) ----|                           |
   |                         |                           |
   |-- code + client_secret->|                           |
   | <-- access_token + id_token + refresh_token --------|
   |                         |                           |
   |-- Requête UserInfo ----->|                           |
   | <-- NID + nom + tél ----|                           |

Le détail d'implémentation critique : Nafath est un authentificateur mobile-first. Votre application web doit gérer l'approbation asynchrone — l'utilisateur peut prendre 30 à 60 secondes pour confirmer sur son téléphone. Vous avez besoin d'un mécanisme de polling ou webhook pour gérer cela correctement.

Étape 1 : Demander un accès API

L'intégration directe avec Nafath nécessite une application approuvée par SDAIA/NIC :

  1. Enregistrez votre plateforme sur my.gov.sa sous "إدارة التطبيقات" (Gestion des applications)
  2. Soumettez la description de votre plateforme, les URIs de redirection et le cas d'usage prévu
  3. SDAIA examine et approuve (généralement 5 à 15 jours ouvrables pour le secteur privé)
  4. Vous recevez un client_id et des instructions pour obtenir un client_secret

Pour les plateformes agréées en immobilier, fintech, RH et santé : l'intégration Nafath est souvent imposée par le régulateur concerné (REGA, SAMA, HRSD, MOH). Dans ce cas, votre interlocuteur réglementaire peut accélérer le processus d'approbation.

Chemin alternatif — courtier Rabet : Si vous avez besoin d'un délai de mise sur le marché plus court, la plateforme Rabet (legacy.rabet.sa) propose un accès Nafath par courtage via les identifiants Absher. Cela fonctionne mais ajoute une dépendance tierce et un mécanisme d'authentification plus ancien. L'intégration directe iDART est la voie recommandée pour la production.

Étape 2 : Configuration du projet

Installez les packages requis :

npm install openid-client jose zod
npm install -D @types/node
  • openid-client : bibliothèque client OIDC certifiée (gère discovery, PKCE, échange de tokens)
  • jose : vérification JWT avec support JWKS
  • zod : validation à l'exécution des claims d'identité

Créez le fichier de configuration d'environnement :

# .env.local
NAFATH_CLIENT_ID=votre_client_id_de_sdaia
NAFATH_CLIENT_SECRET=votre_client_secret
NAFATH_ISSUER=https://iam.gov.sa
NAFATH_REDIRECT_URI=https://votre-app.sa/api/auth/nafath/callback
SESSION_SECRET=une_longue_chaine_aleatoire_pour_signer_les_sessions

Note : SDAIA peut vous fournir une URL d'issuer différente spécifique à votre application approuvée. Utilisez toujours l'URL de votre lettre d'approbation — l'endpoint de découverte iDART suit le schéma ISSUER_URL/.well-known/openid-configuration.

Étape 3 : Configuration du client OIDC

Créez un module client OIDC partagé qui met en cache la configuration découverte :

// lib/nafath-oidc.ts
import { Issuer, Client, generators } from "openid-client";
 
let nafathClient: Client | null = null;
 
export async function getNafathClient(): Promise<Client> {
  if (nafathClient) return nafathClient;
 
  const issuer = await Issuer.discover(process.env.NAFATH_ISSUER!);
 
  nafathClient = new issuer.Client({
    client_id: process.env.NAFATH_CLIENT_ID!,
    client_secret: process.env.NAFATH_CLIENT_SECRET!,
    redirect_uris: [process.env.NAFATH_REDIRECT_URI!],
    response_types: ["code"],
    token_endpoint_auth_method: "client_secret_basic",
  });
 
  return nafathClient;
}
 
export function generatePKCE() {
  const codeVerifier = generators.codeVerifier();
  const codeChallenge = generators.codeChallenge(codeVerifier);
  return { codeVerifier, codeChallenge };
}

Issuer.discover() récupère le document de découverte OIDC et remplit automatiquement tous les endpoints. Votre code s'adapte ainsi si SDAIA met à jour les URLs.

Étape 4 : Construire l'URL d'autorisation

// app/api/auth/nafath/route.ts
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { getNafathClient, generatePKCE } from "@/lib/nafath-oidc";
import { generators } from "openid-client";
 
export async function GET() {
  const client = await getNafathClient();
  const { codeVerifier, codeChallenge } = generatePKCE();
  const state = generators.state();
  const nonce = generators.nonce();
 
  const cookieStore = cookies();
  cookieStore.set("nafath_code_verifier", codeVerifier, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 600,
    path: "/",
  });
  cookieStore.set("nafath_state", state, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 600,
    path: "/",
  });
  cookieStore.set("nafath_nonce", nonce, {
    httpOnly: true,
    secure: true,
    sameSite: "lax",
    maxAge: 600,
    path: "/",
  });
 
  const authorizationUrl = client.authorizationUrl({
    scope: "openid profile national_id phone",
    state,
    nonce,
    code_challenge: codeChallenge,
    code_challenge_method: "S256",
    acr_values: "urn:nafath:iam:push",
  });
 
  return NextResponse.redirect(authorizationUrl);
}

Sur les scopes : Les scopes OIDC de Nafath sont contrôlés par ce que SDAIA a approuvé pour votre application. Scopes courants :

  • openid — requis en toute circonstance
  • profile — nom légal complet et date de naissance
  • national_id — le numéro NID (la plupart des applications en ont besoin)
  • phone — numéro de mobile vérifié par NIC
  • address — adresse enregistrée (nécessite une approbation séparée)

Étape 5 : Gérer le callback OAuth2

// app/api/auth/nafath/callback/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { getNafathClient } from "@/lib/nafath-oidc";
import { verifyNafathToken } from "@/lib/nafath-verify";
import { createSession } from "@/lib/session";
 
export async function GET(request: NextRequest) {
  const cookieStore = cookies();
  const codeVerifier = cookieStore.get("nafath_code_verifier")?.value;
  const expectedState = cookieStore.get("nafath_state")?.value;
  const nonce = cookieStore.get("nafath_nonce")?.value;
 
  if (!codeVerifier || !expectedState || !nonce) {
    return NextResponse.redirect("/auth/error?reason=missing_session");
  }
 
  try {
    const client = await getNafathClient();
    const params = client.callbackParams(request.url);
 
    const tokenSet = await client.callback(
      process.env.NAFATH_REDIRECT_URI!,
      params,
      {
        code_verifier: codeVerifier,
        state: expectedState,
        nonce,
      }
    );
 
    if (!tokenSet.id_token) {
      throw new Error("Pas de token d'identité dans la réponse");
    }
 
    const identity = await verifyNafathToken(tokenSet.id_token, nonce);
 
    const sessionToken = await createSession({
      nationalId: identity.national_id,
      fullName: identity.name,
      phone: identity.phone_number,
      birthdate: identity.birthdate,
      accessToken: tokenSet.access_token!,
      expiresAt: tokenSet.expires_at!,
    });
 
    cookieStore.delete("nafath_code_verifier");
    cookieStore.delete("nafath_state");
    cookieStore.delete("nafath_nonce");
 
    const response = NextResponse.redirect("/dashboard");
    response.cookies.set("session", sessionToken, {
      httpOnly: true,
      secure: true,
      sameSite: "strict",
      maxAge: 60 * 60 * 8,
      path: "/",
    });
 
    return response;
  } catch (error) {
    console.error("Erreur callback Nafath:", error);
    return NextResponse.redirect("/auth/error?reason=callback_failed");
  }
}

Étape 6 : Vérifier le token d'identité

Ne faites jamais confiance à un token d'identité sans vérifier sa signature. Le iDART de Nafath publie un endpoint JWKS — utilisez-le :

// lib/nafath-verify.ts
import { createRemoteJWKSet, jwtVerify } from "jose";
import { z } from "zod";
 
const NafathClaimsSchema = z.object({
  sub: z.string(),
  national_id: z.string().regex(/^\d{10}$/),
  name: z.string().min(1),
  phone_number: z.string().optional(),
  birthdate: z.string().optional(),
  iss: z.string(),
  aud: z.union([z.string(), z.array(z.string())]),
  exp: z.number(),
  iat: z.number(),
  nonce: z.string().optional(),
});
 
export type NafathClaims = z.infer<typeof NafathClaimsSchema>;
 
let jwks: ReturnType<typeof createRemoteJWKSet> | null = null;
 
function getJWKS() {
  if (!jwks) {
    jwks = createRemoteJWKSet(
      new URL(`${process.env.NAFATH_ISSUER}/jwks`)
    );
  }
  return jwks;
}
 
export async function verifyNafathToken(
  idToken: string,
  nonce: string
): Promise<NafathClaims> {
  const { payload } = await jwtVerify(idToken, getJWKS(), {
    issuer: process.env.NAFATH_ISSUER!,
    audience: process.env.NAFATH_CLIENT_ID!,
  });
 
  if (payload.nonce !== nonce) {
    throw new Error("Nonce non correspondant — possible attaque par rejeu");
  }
 
  const now = Math.floor(Date.now() / 1000);
  if ((payload.exp ?? 0) < now) {
    throw new Error("Token d'identité expiré");
  }
 
  const claims = NafathClaimsSchema.parse(payload);
  return claims;
}

Le claim national_id contient le numéro d'identité nationale saoudienne — un nombre de 10 chiffres commençant par 1 pour les ressortissants saoudiens et par 2 pour les résidents (Iqama). Vous pouvez l'utiliser comme clé dans votre base de données, ou pour le recoupement avec les dossiers GOSI ou tout autre système utilisant le numéro national comme identifiant.

Étape 7 : Gestion des sessions

// lib/session.ts
import { SignJWT, jwtVerify } from "jose";
 
const SESSION_SECRET = new TextEncoder().encode(
  process.env.SESSION_SECRET!
);
 
export interface SessionPayload {
  nationalId: string;
  fullName: string;
  phone?: string;
  birthdate?: string;
  accessToken: string;
  expiresAt: number;
}
 
export async function createSession(payload: SessionPayload): Promise<string> {
  return new SignJWT(payload as Record<string, unknown>)
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("8h")
    .sign(SESSION_SECRET);
}
 
export async function getSession(token: string): Promise<SessionPayload | null> {
  try {
    const { payload } = await jwtVerify(token, SESSION_SECRET);
    return payload as unknown as SessionPayload;
  } catch {
    return null;
  }
}

En production, utilisez un store de session côté serveur (Redis, base de données) plutôt que tout encoder dans le cookie — surtout si vous devez invalider les sessions lors de la déconnexion ou de la suspension du compte Nafath d'un utilisateur.

Étape 8 : Protéger les routes

// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/session";
 
const PROTECTED_PATHS = ["/dashboard", "/account", "/services"];
 
export async function middleware(request: NextRequest) {
  const isProtected = PROTECTED_PATHS.some((path) =>
    request.nextUrl.pathname.startsWith(path)
  );
 
  if (!isProtected) return NextResponse.next();
 
  const sessionToken = request.cookies.get("session")?.value;
  if (!sessionToken) {
    return NextResponse.redirect(new URL("/auth/login", request.url));
  }
 
  const session = await getSession(sessionToken);
  if (!session) {
    const response = NextResponse.redirect(new URL("/auth/login", request.url));
    response.cookies.delete("session");
    return response;
  }
 
  const now = Math.floor(Date.now() / 1000);
  if (session.expiresAt < now) {
    return NextResponse.redirect(new URL("/auth/nafath", request.url));
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/dashboard/:path*", "/account/:path*", "/services/:path*"],
};

Étape 9 : L'alternative pont Keycloak

SDAIA a publié un plugin Keycloak open source pour Nafath sur oss.dga.gov.sa. Si votre organisation utilise déjà Keycloak (courant dans les grandes entreprises et agences gouvernementales), c'est le chemin le plus rapide :

Architecture :

Votre App --> Keycloak --> iDART Nafath --> Téléphone
         (OIDC/SAML)   (plugin iDART)

Depuis votre application TypeScript, vous vous intégrez avec Keycloak (pas directement avec Nafath) via OIDC standard. Keycloak gère les détails du protocole Nafath et vous présente une identité normalisée. Cette approche vous donne également la gestion des utilisateurs Keycloak, le contrôle des sessions et les journaux d'audit.

Tests en environnement Sandbox

SDAIA fournit un environnement sandbox pour le développement :

  • Utilisez les identifiants de test fournis dans la documentation développeur
  • L'application Nafath dispose d'un switch "sandbox" — demandez à vos testeurs de l'activer
  • Les durées de vie des tokens sont plus courtes (5 minutes) pour favoriser une gestion correcte du renouvellement
  • Les tokens sandbox n'accèdent pas aux données de l'état civil en production

Tester le cas timeout : simulez un utilisateur qui ignore la notification téléphonique. Après 60 secondes, l'endpoint d'autorisation Nafath retournera une erreur. Votre handler de callback doit rediriger proprement :

const errorCode = params.error;
if (errorCode === "access_denied") {
  return NextResponse.redirect("/auth/error?reason=nafath_rejected");
}

Résolution des problèmes

"redirect_uri_mismatch" — L'URI de redirection dans votre requête d'autorisation doit correspondre exactement à ce qui a été enregistré chez SDAIA. Une différence de barre oblique finale suffit à échouer.

"invalid_client" — Vérifiez soigneusement votre client_id et client_secret. Avec la méthode client_secret_basic, ils sont envoyés encodés en Base64 dans l'en-tête, pas dans le corps de la requête.

"nonce_expired" — L'utilisateur a mis trop de temps à approuver. Implémentez un compte à rebours sur votre écran d'attente et redirigez automatiquement au bout de 90 secondes.

Échec de récupération JWKS — Mettez en cache les JWKS localement avec une durée de vie de 24 heures. Les clés Nafath tournent périodiquement ; si la vérification échoue après une période de bon fonctionnement, effacez votre cache JWKS et récupérez à nouveau.

"scope_not_approved" — Vous avez demandé un scope (comme national_id) qui n'a pas été accordé dans votre application SDAIA. Vérifiez votre liste de scopes approuvés dans le portail développeur.

Checklist de sécurité

Avant la mise en production, vérifiez que tout est en place :

  • PKCE activé (code_challenge_method: "S256") — empêche l'interception du code d'autorisation
  • Nonce validé dans le token d'identité — empêche les attaques par rejeu
  • State validé — empêche les CSRF sur le callback
  • Signature du token d'identité vérifiée contre JWKS — ne jamais sauter cette étape
  • Claim exp vérifié — rejeter les tokens expirés
  • Redirect URI en HTTPS uniquement — Nafath rejette les URIs non sécurisées
  • Cookies de session avec httpOnly, secure, sameSite: "strict"
  • Access token jamais exposé au navigateur — conserver côté serveur uniquement
  • Renouvellement des tokens géré avant expiration — éviter les échecs d'authentification en cours de session

Guides de plateformes gouvernementales saoudites associés

Si vous construisez des plateformes saoudiennes soumises à la conformité, ces tutoriels couvrent les autres couches d'intégration dont vous aurez probablement besoin :

Prochaines étapes

Une fois l'authentification Nafath fonctionnelle :

  1. Ajoutez la déconnexion : appelez l'endpoint end-session de iDART pour invalider la session Nafath, pas seulement le cookie local
  2. Gérez le renouvellement des tokens : utilisez le refresh token pour prolonger les sessions sans réauthentification
  3. Stockez l'ID national comme clé étrangère : standardisez sur sub ou national_id comme ancre d'identité principale dans vos services
  4. Journaux d'audit : enregistrez chaque événement d'authentification avec horodatage, hash du numéro national (pas en clair) et résultat — requis pour les plateformes réglementées
  5. Envisagez Keycloak : si vous avez plusieurs applications nécessitant Nafath, le pont Keycloak évite d'implémenter ce flux dans chaque application séparément

Conclusion

L'intégration Nafath vaut la friction de l'enregistrement. Lorsqu'un utilisateur s'authentifie via Nafath, vous disposez d'une identité vérifiée qu'aucune autre méthode de connexion en Arabie Saoudite ne peut égaler. Le flux OAuth2/OIDC est standard — les seules parties non-standard sont la confirmation push mobile, le processus d'approbation des scopes avec SDAIA, et les claims d'état civil dans le token.

La documentation en langue arabe pour l'intégration Nafath est quasi inexistante. Si vous construisez des plateformes ciblant le marché saoudien, intégrer Nafath avant vos concurrents est un véritable avantage technique différenciateur.


Besoin d'aide pour le processus d'enregistrement Nafath ou l'architecture d'intégration ? Notre équipe a livré des intégrations de plateformes gouvernementales saoudiennes sur ZATCA, GOSI, WPS et les services d'identité nationale. Contactez-nous pour cadrer votre projet d'intégration.