Authentification OAuth 2.0
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 :
| Flux | Type de client | Cas d’usage | Refresh Token |
|---|---|---|---|
| Client Credentials | Confidentiel | Services back-end, middleware, intégrations ERP, outils BI — aucun utilisateur impliqué | Non |
| Authorization Code + PKCE | Public | Portails, SPA, applications mobiles — un utilisateur réel se connecte | Oui (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.
URL de base
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
Flux 1 — Client Credentials (Applications confidentielles)
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 HTTP | Champ error | Cause |
|---|---|---|
| 401 Unauthorized | invalid_client | ClientId ou ClientSecret incorrect, ou client désactivé |
| 400 Bad Request | invalid_scope | Le scope demandé n’est pas autorisé pour ce client |
| 403 Forbidden | ip_not_allowed | L’IP de la requête n’est pas dans la liste des IP autorisées |
| 403 Forbidden | unauthorized_client | Le 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."
}
Flux 2 — Authorization Code + PKCE (Applications publiques)
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ètre | Requis | Valeur |
|---|---|---|
| response_type | Oui | code |
| client_id | Oui | Votre ClientId |
| redirect_uri | Oui | Doit correspondre exactement à une URI de redirection enregistrée |
| scope | Oui | Scopes séparés par des espaces, ex. api:read offline_access |
| state | Oui | Chaîne opaque aléatoire — à vérifier dans le callback |
| code_challenge | Oui | base64url SHA-256 du code_verifier |
| code_challenge_method | Oui | S256 (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",
}),
});
Introspection de jeton
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.
Validation JWT (Resource Server)
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 :
| Claim | Description |
|---|---|
| iss | Émetteur — l’URL du serveur OAuth de CS |
| sub | Sujet — client_id (Client Credentials) ou identifiant utilisateur (Auth Code) |
| aud | Audience — l’URL de l’API CS |
| iat | Horodatage d’émission (Unix) |
| exp | Horodatage d’expiration (Unix) — à rejeter s’il est dépassé |
| jti | Identifiant unique du jeton — à utiliser pour les contrôles anti-rejeu |
| scope | Scopes accordés, séparés par des espaces |
| client_id | Le client_id de l’Application Connectée |
| auth_mode | CLIENT_CREDENTIALS ou AUTHORIZATION_CODE |
| cs_user_id | Utilisateur CS associé à ce jeton |
| cs_profile | Profil CS de l’utilisateur associé |
| cs_org_id | Identifiant 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.
Scopes
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.
| Scope | Accès accordé |
|---|---|
| api | Accès complet — lecture, écriture et suppression |
| api:read | Accès en lecture seule aux données CRM |
| api:write | Création et mise à jour des données CRM |
| api:delete | Suppression des données CRM |
| offline_access | Autorise 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.
Erreurs courantes et solutions
| Erreur | Cause | Solution |
|---|---|---|
| 401 invalid_client | ClientId 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_scope | Le scope demandé n’est pas autorisé pour ce client | Ne demandez que les scopes configurés pour votre Application Connectée. |
| 400 invalid_grant | Code 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_challenge | Assurez-vous d’avoir stocké le bon verifier et de le transmettre inchangé. |
| 401 token_revoked | Refresh token expiré (>7 jours) ou réutilisé après rotation | Supprimez les jetons et relancez le flux de connexion. |
| 403 ip_not_allowed | L’IP de la requête n’est pas dans les IP autorisées pour ce client | Demandez à votre administrateur CS d’ajouter votre IP à l’Application Connectée. |
| 429 Too Many Requests | Limite 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 API | Jeton expiré ou révoqué en cours de session | Invalidez le jeton en cache, demandez-en un nouveau, réessayez l’appel une seule fois. |
Liste de vérification sécurité
- 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.