1. Accueil
  2. /
  3. Guide Développeur
  4. /
  5. Rest API
  6. /
  7. 1. AuthToken

1. AuthToken

Cirrus Shield propose désormais un serveur d’autorisation OAuth 2.0 intégré. Les applications externes qui doivent accéder à l’API REST de Cirrus Shield devraient s’authentifier via OAuth 2.0 plutôt que via l’ancien flux de jeton nom d’utilisateur/mot de passe.

Il existe deux flux d’authentification, selon le type d’application :

FluxType de clientCas d’usageRefresh Token
Client CredentialsConfidentielServices back-end, middleware, intégrations ERP, outils BI — aucun utilisateur impliquéNon
Authorization Code + PKCEPublicPortails, SPA, applications mobiles — un utilisateur réel se connecteOui (7 jours)

ℹ️ Avant d’implémenter l’un ou l’autre flux, votre application doit être enregistrée comme Application Connectée dans Cirrus Shield. Demandez à votre administrateur CS de créer l’Application Connectée et de vous fournir le ClientId et (si Confidentiel) le ClientSecret.

Tous les endpoints OAuth sont relatifs à l’URL de base Cirrus Shield de votre organisation. L’URL de base suit ce modèle :

https://www.cirrus-shield.net

Utilisez ce flux lorsque votre application s’exécute côté serveur et s’authentifie en son propre nom, sans utilisateur final. Exemples : un traitement en arrière-plan qui synchronise des factures, un outil BI qui récupère des données CRM, un middleware qui envoie des documents.

Fonctionnement

Votre application présente son ClientId et son ClientSecret pour obtenir un jeton d’accès JWT de courte durée. Ce jeton est ensuite joint à chaque appel API. Lorsqu’il expire, votre application en demande un nouveau avec les mêmes identifiants.

Étape 1 — Demander un jeton d’accès

Envoyez une requête POST au endpoint de jeton avec vos identifiants dans l’en-tête Authorization (HTTP Basic Auth) et le type d’octroi dans le corps.

Endpoint : POST {baseUrl}/oauth/token

Requête (.NET / HttpClient) :

var credentials = Convert.ToBase64String(
    Encoding.UTF8.GetBytes($"{clientId}:{clientSecret}"));

var request = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/oauth/token");
request.Headers.Authorization =
    new AuthenticationHeaderValue("Basic", credentials);
request.Content = new FormUrlEncodedContent(new[]
{
    new KeyValuePair<string, string>("grant_type", "client_credentials"),
    new KeyValuePair<string, string>("client_id", clientId),
    new KeyValuePair<string, string>("client_secret", clientSecret),
    // optionnel : new KeyValuePair<string, string>("scope", "api:read api:write"),
});

var response = await httpClient.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();

Requête (JavaScript / fetch) :

const credentials = btoa(`${clientId}:${clientSecret}`);

const response = await fetch(`${baseUrl}/oauth/token`, {
  method: "POST",
  headers: {
    "Authorization": `Basic ${credentials}`,
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    client_id: clientId,
    client_secret: clientSecret,
    // scope: "api:read api:write",  // optionnel
  }),
});
const data = await response.json();

Réponse de succès (200 OK) :

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "api:read api:write"
}

ℹ️ Aucun refresh_token n’est émis pour le flux Client Credentials. Lorsque le jeton expire, demandez-en un nouveau avec le même ClientId et ClientSecret.

Étape 2 — Utiliser le jeton

Joignez le jeton d’accès à chaque appel API via l’en-tête Authorization. Ne le transmettez jamais en paramètre d’URL.

.NET :

request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

JavaScript :

headers: { "Authorization": `Bearer ${accessToken}` }

Étape 3 — Cycle de vie et mise en cache du jeton

Le jeton d’accès est valide pour la durée indiquée dans expires_in (valeur par défaut : 3600 secondes / 1 heure). Vous devez mettre le jeton en cache et le réutiliser d’un appel à l’autre plutôt que d’en demander un nouveau à chaque appel API.

Stratégie de mise en cache recommandée :

  • Stockez le jeton et sa date d’expiration (heure actuelle + expires_in − une marge de 60 secondes).
  • Avant chaque appel API, vérifiez si le jeton en cache est encore valide.
  • S’il est expiré, demandez un nouveau jeton avant de poursuivre.
  • Si un appel API renvoie 401 Unauthorized, invalidez le jeton en cache, demandez-en un nouveau, puis réessayez l’appel une seule fois.

Exemple de mise en cache du jeton (.NET) :

private string _cachedToken;
private DateTime _tokenExpiresAt = DateTime.MinValue;

public async Task<string> GetTokenAsync()
{
    if (_cachedToken != null && DateTime.UtcNow < _tokenExpiresAt)
        return _cachedToken;

    var token = await RequestNewTokenAsync();
    _cachedToken = token.AccessToken;
    _tokenExpiresAt = DateTime.UtcNow.AddSeconds(token.ExpiresIn - 60);
    return _cachedToken;
}

Réponses d’erreur

Statut HTTPChamp errorCause
401 Unauthorizedinvalid_clientClientId ou ClientSecret incorrect, ou client désactivé
400 Bad Requestinvalid_scopeLe scope demandé n’est pas autorisé pour ce client
403 Forbiddenip_not_allowedL’IP de la requête n’est pas dans la liste des IP autorisées
403 Forbiddenunauthorized_clientLe flux client_credentials n’est pas autorisé pour ce client
429 Too Many Requests(en-tête : Retry-After)Limite de débit dépassée — attendez le nombre de secondes indiqué dans Retry-After

Corps de la réponse d’erreur :

{
  "error": "invalid_client",
  "error_description": "Client credentials are invalid or client is disabled."
}

Utilisez ce flux lorsqu’un utilisateur réel doit s’authentifier avec son propre compte Cirrus Shield. Exemples : un portail client, une SPA, une application mobile. L’utilisateur se connecte sur une page Cirrus Shield et autorise votre application à accéder à ses données.

⚠️ PKCE (Proof Key for Code Exchange) est obligatoire pour tous les clients publics. Seule la méthode S256 est acceptée — plain est refusée. Ne sautez jamais l’étape PKCE, même si votre framework la rend optionnelle.

Fonctionnement

Le flux comporte 4 phases : génération PKCE → connexion et consentement de l’utilisateur → échange du code d’autorisation → utilisation et rafraîchissement du jeton.

Phase 1 — Générer la paire PKCE

Générez un code_verifier (une chaîne aléatoire) et dérivez-en un code_challenge via SHA-256. Le code_challenge est envoyé à CS ; le code_verifier est gardé secret et utilisé plus tard pour prouver que votre application est à l’origine du flux.

.NET :

// Génération du code_verifier
var bytes = new byte[32];
RandomNumberGenerator.Fill(bytes);
var codeVerifier = Base64UrlEncode(bytes);  // à stocker en session

// Dérivation du code_challenge
var hash = SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier));
var codeChallenge = Base64UrlEncode(hash);  // à envoyer à CS

static string Base64UrlEncode(byte[] b) =>
    Convert.ToBase64String(b).TrimEnd('=').Replace('+','-').Replace('/','_');

JavaScript :

async function generatePKCE() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  const verifier = base64UrlEncode(array);  // à stocker en sessionStorage

  const hash = await crypto.subtle.digest(
    "SHA-256", new TextEncoder().encode(verifier));
  const challenge = base64UrlEncode(new Uint8Array(hash));
  return { verifier, challenge };
}

function base64UrlEncode(bytes) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

ℹ️ Stockez le code_verifier côté serveur en session (.NET) ou dans sessionStorage (JavaScript). Vous en aurez besoin en Phase 3. Ne l’envoyez jamais sur le réseau avant la Phase 3.

Phase 2 — Rediriger l’utilisateur vers la connexion CS

Construisez l’URL d’autorisation et redirigez le navigateur de l’utilisateur vers celle-ci. Générez une valeur state aléatoire pour la protection CSRF et stockez-la en session avec le code_verifier.

Paramètres de l’URL d’autorisation :

ParamètreRequisValeur
response_typeOuicode
client_idOuiVotre ClientId
redirect_uriOuiDoit correspondre exactement à une URI de redirection enregistrée
scopeOuiScopes séparés par des espaces, ex. api:read offline_access
stateOuiChaîne opaque aléatoire — à vérifier dans le callback
code_challengeOuibase64url SHA-256 du code_verifier
code_challenge_methodOuiS256 (seule valeur acceptée)

.NET :

var state = Guid.NewGuid().ToString("N");
HttpContext.Session.SetString("oauth_state", state);
HttpContext.Session.SetString("pkce_verifier", codeVerifier);

var url = $"{baseUrl}/oauth/authorize" +
  $"?response_type=code" +
  $"&client_id={Uri.EscapeDataString(clientId)}" +
  $"&redirect_uri={Uri.EscapeDataString(redirectUri)}" +
  $"&scope={Uri.EscapeDataString("api:read offline_access")}" +
  $"&state={Uri.EscapeDataString(state)}" +
  $"&code_challenge={Uri.EscapeDataString(codeChallenge)}" +
  $"&code_challenge_method=S256";

return Redirect(url);

JavaScript (SPA) :

const state = crypto.randomUUID().replace(/-/g, "");
sessionStorage.setItem("oauth_state", state);
sessionStorage.setItem("pkce_verifier", verifier);

const params = new URLSearchParams({
  response_type: "code",
  client_id: clientId,
  redirect_uri: redirectUri,
  scope: "api:read offline_access",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
});
window.location.href = `${baseUrl}/oauth/authorize?${params}`;

L’utilisateur voit la page de connexion Cirrus Shield, puis un écran de consentement listant les permissions demandées par votre application. Une fois approuvé, CS redirige vers votre redirect_uri avec un code et le state renvoyé tel quel :

https://yourapp.com/callback?code=ABC123&state=<echo>

⚠️ Le code d’autorisation est valide 60 secondes et ne peut être utilisé qu’une seule fois. Échangez-le immédiatement en Phase 3.

Phase 3 — Échanger le code contre des jetons

Dans votre gestionnaire de callback : vérifiez d’abord le paramètre state, puis échangez le code contre des jetons en envoyant le code_verifier stocké en Phase 2.

Gestionnaire de callback .NET :

public async Task<IActionResult> Callback(string code, string state, string error)
{
    if (!string.IsNullOrEmpty(error))
        return RedirectToAction("Error");

    // Vérification du state — protection CSRF
    var expectedState = HttpContext.Session.GetString("oauth_state");
    if (state != expectedState) return BadRequest("State mismatch.");

    var codeVerifier = HttpContext.Session.GetString("pkce_verifier");
    HttpContext.Session.Remove("pkce_verifier");
    HttpContext.Session.Remove("oauth_state");

    var response = await httpClient.PostAsync($"{baseUrl}/oauth/token",
        new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string,string>("grant_type", "authorization_code"),
            new KeyValuePair<string,string>("code", code),
            new KeyValuePair<string,string>("redirect_uri", redirectUri),
            new KeyValuePair<string,string>("client_id", clientId),
            new KeyValuePair<string,string>("code_verifier", codeVerifier),
        }));

    var body = await response.Content.ReadAsStringAsync();
    // parser et stocker les jetons...
}

Réponse de succès (200 OK) :

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "rt_aB3kPq7mN2xR8vT1...",
  "scope": "api:read offline_access"
}

ℹ️ Stockez l’access_token en mémoire (jamais dans localStorage). Stockez le refresh_token dans un cookie httpOnly Secure ou en session côté serveur.

Phase 4 — Rafraîchir le jeton d’accès

Le jeton d’accès expire après expires_in secondes (généralement 15 minutes). Utilisez le refresh token pour obtenir un nouveau jeton d’accès sans que l’utilisateur ait à se reconnecter.

⚠️ La rotation du refresh token est appliquée. Chaque utilisation d’un refresh token émet un NOUVEAU refresh token et invalide immédiatement l’ancien. Enregistrez toujours le nouveau refresh_token reçu dans la réponse. Si vous réutilisez un ancien refresh token, toute la session est révoquée et l’utilisateur doit se reconnecter.

.NET :

var response = await httpClient.PostAsync($"{baseUrl}/oauth/token",
    new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string,string>("grant_type", "refresh_token"),
        new KeyValuePair<string,string>("refresh_token", currentRefreshToken),
        new KeyValuePair<string,string>("client_id", clientId),
        // Pas de client_secret pour les clients publics
    }));

var body = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
    // Refresh token expiré ou rejoué — l'utilisateur doit se reconnecter
    ClearSession();
    return Redirect(loginUrl);
}
// Enregistrer le nouvel access_token et le nouveau refresh_token depuis la réponse

JavaScript :

const response = await fetch(`${baseUrl}/oauth/token`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "refresh_token",
    refresh_token: currentRefreshToken,
    client_id: clientId,
  }),
});

if (!response.ok) {
  // Refresh token expiré ou rejoué — rediriger vers la connexion
  startLoginFlow();
  return;
}
const data = await response.json();
// Enregistrer data.access_token et data.refresh_token (rotation !)

Déconnexion — Révocation du refresh token

Lors de la déconnexion, révoquez le refresh token côté serveur pour invalider immédiatement la session.

.NET :

await httpClient.PostAsync($"{baseUrl}/oauth/revoke",
    new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string,string>("token", refreshToken),
        new KeyValuePair<string,string>("token_type_hint", "refresh_token"),
    }));
// Renvoie toujours 200 OK, même si le jeton est inconnu

JavaScript :

await fetch(`${baseUrl}/oauth/revoke`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    token: refreshToken,
    token_type_hint: "refresh_token",
  }),
});

Si votre application agit comme un Resource Server et doit vérifier un jeton reçu, utilisez le endpoint d’introspection. Remarque : si vous utilisez des jetons d’accès JWT, vous pouvez aussi les valider localement en vérifiant la signature RS256 à l’aide des clés publiques du endpoint JWKS.

Endpoint : POST {baseUrl}/oauth/introspect

ℹ️ Le endpoint d’introspection exige que l’appelant s’authentifie avec son propre ClientId/ClientSecret de Resource Server (et non les identifiants du propriétaire du jeton). Limite de débit : 100 requêtes/min par client RS.

POST {baseUrl}/oauth/introspect
Authorization: Basic base64(rsClientId:rsClientSecret)
Content-Type: application/x-www-form-urlencoded

token=eyJhbGciOiJSUzI1NiIsImtpZCI6...
&token_type_hint=access_token

Réponse pour un jeton actif :

{
  "active": true,
  "scope": "api:read",
  "client_id": "cc_ecommerce_01",
  "sub": "user-123",
  "exp": 1711370400,
  "jti": "550e8400-...",
  "cs_profile": "Contact Client"
}

Jeton inactif / expiré / révoqué :

{ "active": false }

⚠️ Vérifiez toujours active: true avant d’utiliser tout autre champ. Si active vaut false, tous les autres champs sont absents ou dénués de sens.

Les jetons d’accès émis par Cirrus Shield sont des JWT signés (RS256). Si votre application est un Resource Server, vous pouvez valider les jetons localement sans appeler le endpoint d’introspection, en vérifiant la signature à l’aide des clés publiques de CS.

Endpoint JWKS : GET {baseUrl}/oauth/.well-known/jwks.json

Le payload du JWT contient les claims suivants :

ClaimDescription
issÉmetteur — l’URL du serveur OAuth de CS
subSujet — client_id (Client Credentials) ou identifiant utilisateur (Auth Code)
audAudience — l’URL de l’API CS
iatHorodatage d’émission (Unix)
expHorodatage d’expiration (Unix) — à rejeter s’il est dépassé
jtiIdentifiant unique du jeton — à utiliser pour les contrôles anti-rejeu
scopeScopes accordés, séparés par des espaces
client_idLe client_id de l’Application Connectée
auth_modeCLIENT_CREDENTIALS ou AUTHORIZATION_CODE
cs_user_idUtilisateur CS associé à ce jeton
cs_profileProfil CS de l’utilisateur associé
cs_org_idIdentifiant de l’organisation

Liste de vérification :

  • Vérifiez la signature RS256 avec la clé publique correspondant au claim kid de l’en-tête, via JWKS
  • Vérifiez que exp est dans le futur (tolérance d’horloge de 5 secondes maximum)
  • Vérifiez que iss correspond à l’URL de base CS attendue
  • Vérifiez que aud correspond à l’URL de votre API
  • Vérifiez que jti ne figure pas dans votre liste de jetons révoqués
  • Vérifiez que le jeton possède le scope requis pour l’opération demandée

ℹ️ Lors d’une rotation de clés, CS publie simultanément l’ancienne et la nouvelle clé publique pendant au moins 24 heures. Utilisez toujours le claim kid pour sélectionner la bonne clé plutôt que de supposer qu’il n’y en a qu’une.

Les scopes contrôlent à quelles données et opérations un jeton d’accès donne accès. Ne demandez que les scopes réellement nécessaires à votre application.

ScopeAccès accordé
apiAccès complet — lecture, écriture et suppression
api:readAccès en lecture seule aux données CRM
api:writeCréation et mise à jour des données CRM
api:deleteSuppression des données CRM
offline_accessAutorise l’émission d’un refresh token (flux Authorization Code uniquement)

ℹ️ Les scopes que votre jeton peut demander sont limités par la configuration de votre Application Connectée définie par l’administrateur Cirrus Shield. Demander un scope non autorisé pour votre application renvoie 400 invalid_scope.

ErreurCauseSolution
401 invalid_clientClientId ou ClientSecret incorrect, ou client désactivéVérifiez les identifiants auprès de votre administrateur CS. Vérifiez que le client est actif.
400 invalid_scopeLe scope demandé n’est pas autorisé pour ce clientNe demandez que les scopes configurés pour votre Application Connectée.
400 invalid_grantCode d’autorisation expiré (>60s) ou déjà utiliséÉchangez le code immédiatement après l’avoir reçu dans le callback.
400 invalid_grant (code_verifier)Le code_verifier ne correspond pas au code_challengeAssurez-vous d’avoir stocké le bon verifier et de le transmettre inchangé.
401 token_revokedRefresh token expiré (>7 jours) ou réutilisé après rotationSupprimez les jetons et relancez le flux de connexion.
403 ip_not_allowedL’IP de la requête n’est pas dans les IP autorisées pour ce clientDemandez à votre administrateur CS d’ajouter votre IP à l’Application Connectée.
429 Too Many RequestsLimite de débit atteinte (10 demandes de jeton/min)Mettez en place la mise en cache des jetons. Respectez l’en-tête Retry-After.
401 sur un appel APIJeton expiré ou révoqué en cours de sessionInvalidez le jeton en cache, demandez-en un nouveau, réessayez l’appel une seule fois.
  • Ne jamais journaliser ni exposer le ClientSecret, les jetons d’accès ou les refresh tokens
  • Ne jamais stocker les jetons dans localStorage ou dans des cookies accessibles en JavaScript — utilisez des cookies httpOnly Secure ou une session côté serveur
  • Toujours vérifier le paramètre state dans le callback OAuth avant de traiter le code
  • Toujours utiliser PKCE (S256) — ne jamais utiliser la méthode plain
  • Toujours mettre les jetons en cache et les réutiliser d’un appel à l’autre — ne jamais demander un nouveau jeton à chaque appel API
  • Toujours enregistrer le nouveau refresh token après chaque rafraîchissement — ne jamais réutiliser l’ancien
  • Toujours envoyer le jeton d’accès dans l’en-tête Authorization: Bearer — jamais en paramètre d’URL
  • Toujours appeler /oauth/revoke à la déconnexion pour invalider le refresh token côté serveur
  • Mettre en place une seule tentative de retry sur 401 : invalider le jeton en cache, en obtenir un nouveau, réessayer une fois. Si le retry échoue aussi, remonter l’erreur.
Cet article vous a-t-il été utile ? Non Oui 1

Comment pouvons-nous aider ?