{
  "info": {
    "_postman_id": "fasologin-api-v1",
    "name": "FASO LOGIN API",
    "description": "Collection officielle pour tester l'API FASO LOGIN — IDP souverain burkinabè.\n\n## Démarrage rapide\n1. Importez cette collection dans Postman\n2. Créez un environnement avec les variables ci-dessous\n3. Renseignez `client_id`, `client_secret`, `redirect_uri`\n4. Lancez `Discovery` pour vérifier la connectivité\n5. Suivez les dossiers dans l'ordre\n\n## Variables requises\n- `auth_url` : base URL OIDC (ex: https://api.fasologin.tino-ti.com/oidc)\n- `client_id` : votre client_id (format fasologin_xxx)\n- `client_secret` : votre client_secret\n- `redirect_uri` : l'une de vos redirect URIs enregistrées\n\n## Documentation\nhttps://auth.fasologin.tino-ti.com/dev",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "version": { "major": 1, "minor": 0, "patch": 0 }
  },
  "variable": [
    {
      "key": "auth_url",
      "value": "https://api.fasologin.tino-ti.com/oidc",
      "description": "URL de base OIDC — ne pas inclure de slash final"
    },
    {
      "key": "base_url",
      "value": "https://api.fasologin.tino-ti.com",
      "description": "URL de base API sans /oidc — utilisée pour /health et autres endpoints non-OIDC"
    },
    {
      "key": "client_id",
      "value": "fasologin_votre_client_id",
      "description": "Votre client_id (reçu par email après approbation)"
    },
    {
      "key": "client_secret",
      "value": "votre_client_secret",
      "description": "Votre client_secret — gardez-le confidentiel"
    },
    {
      "key": "redirect_uri",
      "value": "https://votreapp.bf/auth/callback",
      "description": "Doit correspondre exactement à une URI enregistrée"
    },
    {
      "key": "access_token",
      "value": "",
      "description": "Rempli automatiquement après échange du code"
    },
    {
      "key": "refresh_token",
      "value": "",
      "description": "Rempli automatiquement après échange du code"
    },
    {
      "key": "id_token",
      "value": "",
      "description": "Rempli automatiquement après échange du code"
    },
    {
      "key": "code",
      "value": "",
      "description": "Code d'autorisation — obtenu via le navigateur, à coller ici"
    },
    {
      "key": "code_verifier",
      "value": "",
      "description": "Généré automatiquement par le pre-request script PKCE"
    }
  ],
  "item": [
    {
      "name": "1. Santé & Discovery",
      "item": [
        {
          "name": "Health check",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/health",
              "host": ["{{base_url}}"],
              "path": ["health"]
            },
            "description": "Vérifie que l'API, la base de données et Redis sont opérationnels.\n\nRéponse attendue : `{ \"status\": \"ok\", \"db\": true, \"redis\": true, \"version\": \"...\" }`"
          }
        },
        {
          "name": "OpenID Configuration (Discovery)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{auth_url}}/.well-known/openid-configuration",
              "host": ["{{auth_url}}"],
              "path": [".well-known", "openid-configuration"]
            },
            "description": "Récupère le document de discovery OpenID Connect.\n\nContient tous les endpoints, scopes supportés, algorithmes de signature et méthodes d'authentification.\n\nUtilisez cette URL dans vos librairies OAuth2 pour l'auto-configuration."
          }
        },
        {
          "name": "JWKS (clés publiques)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{auth_url}}/jwks",
              "host": ["{{auth_url}}"],
              "path": ["jwks"]
            },
            "description": "Clés publiques RSA utilisées pour vérifier les signatures des tokens JWT.\n\nUtilisé par les librairies comme `jose` pour vérifier les id_token et logout_token."
          }
        }
      ]
    },
    {
      "name": "2. Authorization Code Flow (PKCE)",
      "item": [
        {
          "name": "Générer l'URL d'autorisation (PKCE)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// Génération PKCE — code_verifier + code_challenge",
                  "const array = new Uint8Array(32);",
                  "crypto.getRandomValues(array);",
                  "const verifier = btoa(String.fromCharCode(...array))",
                  "  .replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');",
                  "pm.collectionVariables.set('code_verifier', verifier);",
                  "",
                  "// SHA-256 du verifier",
                  "const encoder = new TextEncoder();",
                  "const data = encoder.encode(verifier);",
                  "crypto.subtle.digest('SHA-256', data).then(hash => {",
                  "  const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))",
                  "    .replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');",
                  "  pm.collectionVariables.set('code_challenge', challenge);",
                  "  pm.collectionVariables.set('state', Math.random().toString(36).substring(2));",
                  "  console.log('code_verifier:', verifier);",
                  "  console.log('code_challenge:', challenge);",
                  "  console.log('Ouvrez cette URL dans votre navigateur pour vous authentifier.');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{auth_url}}/auth?client_id={{client_id}}&redirect_uri={{redirect_uri}}&response_type=code&scope=openid profile phone email&state={{state}}&code_challenge={{code_challenge}}&code_challenge_method=S256",
              "host": ["{{auth_url}}"],
              "path": ["auth"],
              "query": [
                { "key": "client_id", "value": "{{client_id}}" },
                { "key": "redirect_uri", "value": "{{redirect_uri}}" },
                { "key": "response_type", "value": "code" },
                { "key": "scope", "value": "openid profile phone email" },
                { "key": "state", "value": "{{state}}" },
                { "key": "code_challenge", "value": "{{code_challenge}}" },
                { "key": "code_challenge_method", "value": "S256" }
              ]
            },
            "description": "Construit l'URL d'autorisation avec PKCE.\n\n**Instructions :**\n1. Envoyez cette requête — le pre-request script génère le PKCE automatiquement\n2. Copiez l'URL complète depuis la barre d'adresse Postman\n3. Ouvrez-la dans votre navigateur\n4. Connectez-vous avec FASO LOGIN\n5. Récupérez le paramètre `code` dans l'URL de redirection\n6. Collez-le dans la variable `code` de la collection\n7. Lancez la requête `Échanger le code contre les tokens`"
          }
        },
        {
          "name": "Échanger le code contre les tokens",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const body = pm.response.json();",
                  "  if (body.access_token) pm.collectionVariables.set('access_token', body.access_token);",
                  "  if (body.refresh_token) pm.collectionVariables.set('refresh_token', body.refresh_token);",
                  "  if (body.id_token) pm.collectionVariables.set('id_token', body.id_token);",
                  "  console.log('Tokens sauvegardés dans les variables de collection.');",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/x-www-form-urlencoded" }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                { "key": "grant_type", "value": "authorization_code" },
                { "key": "code", "value": "{{code}}" },
                { "key": "redirect_uri", "value": "{{redirect_uri}}" },
                { "key": "client_id", "value": "{{client_id}}" },
                { "key": "client_secret", "value": "{{client_secret}}" },
                { "key": "code_verifier", "value": "{{code_verifier}}" }
              ]
            },
            "url": {
              "raw": "{{auth_url}}/token",
              "host": ["{{auth_url}}"],
              "path": ["token"]
            },
            "description": "Échange le code d'autorisation contre un access_token, refresh_token et id_token.\n\n**Prérequis :** avoir un `code` valide dans la variable de collection (obtenu via le navigateur).\n\nLe test script sauvegarde automatiquement les tokens dans les variables."
          }
        }
      ]
    },
    {
      "name": "3. Opérations token",
      "item": [
        {
          "name": "UserInfo",
          "request": {
            "method": "GET",
            "header": [
              { "key": "Authorization", "value": "Bearer {{access_token}}" }
            ],
            "url": {
              "raw": "{{auth_url}}/userinfo",
              "host": ["{{auth_url}}"],
              "path": ["userinfo"]
            },
            "description": "Récupère les claims de l'utilisateur authentifié.\n\nLes claims retournés dépendent des scopes accordés lors du consentement.\n\nRéférence claims : sub (UUID immuable), given_name, family_name, phone_number, phone_number_verified, email, email_verified, birthdate, gender, locale, address."
          }
        },
        {
          "name": "Rafraîchir l'access_token",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.response.code === 200) {",
                  "  const body = pm.response.json();",
                  "  if (body.access_token) pm.collectionVariables.set('access_token', body.access_token);",
                  "  if (body.refresh_token) pm.collectionVariables.set('refresh_token', body.refresh_token);",
                  "  console.log('Tokens rafraîchis.');",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/x-www-form-urlencoded" }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                { "key": "grant_type", "value": "refresh_token" },
                { "key": "refresh_token", "value": "{{refresh_token}}" },
                { "key": "client_id", "value": "{{client_id}}" },
                { "key": "client_secret", "value": "{{client_secret}}" }
              ]
            },
            "url": {
              "raw": "{{auth_url}}/token",
              "host": ["{{auth_url}}"],
              "path": ["token"]
            },
            "description": "Obtient un nouvel access_token à partir du refresh_token.\n\nLa rotation est activée : un nouveau refresh_token est émis à chaque appel. L'ancien est immédiatement invalidé."
          }
        },
        {
          "name": "Introspection (vérifier un token)",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/x-www-form-urlencoded" }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                { "key": "token", "value": "{{access_token}}" },
                { "key": "client_id", "value": "{{client_id}}" },
                { "key": "client_secret", "value": "{{client_secret}}" }
              ]
            },
            "url": {
              "raw": "{{auth_url}}/token/introspection",
              "host": ["{{auth_url}}"],
              "path": ["token", "introspection"]
            },
            "description": "Vérifie la validité d'un access_token côté backend sans le décoder.\n\nRéponse : `{ \"active\": true, \"sub\": \"...\", \"exp\": ..., \"scope\": \"...\" }`\n\nSi `active: false`, le token est expiré ou révoqué."
          }
        },
        {
          "name": "Révoquer un token",
          "request": {
            "method": "POST",
            "header": [
              { "key": "Content-Type", "value": "application/x-www-form-urlencoded" }
            ],
            "body": {
              "mode": "urlencoded",
              "urlencoded": [
                { "key": "token", "value": "{{refresh_token}}" },
                { "key": "client_id", "value": "{{client_id}}" },
                { "key": "client_secret", "value": "{{client_secret}}" }
              ]
            },
            "url": {
              "raw": "{{auth_url}}/token/revocation",
              "host": ["{{auth_url}}"],
              "path": ["token", "revocation"]
            },
            "description": "Révoque un access_token ou refresh_token.\n\nÀ appeler à la déconnexion de l'utilisateur. Retourne toujours 200, même si le token est invalide (RFC 7009)."
          }
        }
      ]
    },
    {
      "name": "4. Session & Logout",
      "item": [
        {
          "name": "End Session (RP-Initiated Logout)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{auth_url}}/session/end?client_id={{client_id}}&id_token_hint={{id_token}}&post_logout_redirect_uri={{redirect_uri}}",
              "host": ["{{auth_url}}"],
              "path": ["session", "end"],
              "query": [
                { "key": "client_id", "value": "{{client_id}}" },
                { "key": "id_token_hint", "value": "{{id_token}}", "description": "id_token obtenu lors de l'échange" },
                { "key": "post_logout_redirect_uri", "value": "{{redirect_uri}}", "description": "Doit être enregistrée comme postLogoutRedirectUri" }
              ]
            },
            "description": "Déconnecte l'utilisateur de FASO LOGIN et redirige vers votre application.\n\n**Recommandation :** Révoquez d'abord le refresh_token, puis appelez cet endpoint.\n\nLe `post_logout_redirect_uri` doit être enregistré dans la configuration de votre client."
          }
        }
      ]
    }
  ]
}
