Authentification
Fonctionnalité déployée et prise en charge. Les champs et commandes décrits ici sont stables au sein de la version v1alpha1.
Toute requête sur votre tenant porte une session de tenant : un jeton signé par StackNGO, valable 1 heure, qui dit qui vous êtes et dans quel tenant vous agissez. Vous l'obtenez en deux temps : un jeton d'identité (preuve de qui vous êtes), puis un échange POST /v1/iam/session.
API=https://api.dev.stackngo.fr/v1
# 1. identité (ici, celle que sng sso login a enregistrée)
ID_TOKEN=$(yq '.access_token' ~/.config/stackngo/config.yaml)
# 2. session sur le tenant acme
SESSION=$(curl -fsS -X POST "$API/iam/session" \
-H "Authorization: Bearer $ID_TOKEN" -H "Content-Type: application/json" \
-d '{"tenant":"acme"}' | jq -r .access_token)
# 3. appel d'une route du tenant
curl -fsS "$API/secrets" -H "Authorization: Bearer $SESSION"
Le modèle (pourquoi deux jetons, la frontière de tenant) est expliqué dans Le modèle identité, session, tenant.
Un seul jeton par requête
| Routes | Jeton attendu dans Authorization: Bearer |
|---|---|
POST /v1/iam/session | le jeton d'identité (un des quatre ci-dessous) |
POST /v1/iam/session/assume | une session de tenant |
GET /v1/iam/me, GET /v1/tenants, POST /v1/tenants, GET /v1/templates[/{kind}] | le jeton d'identité utilisateur du SSO |
POST /v1/iam/logout | une session de tenant, ou le jeton d'identité (voir Se déconnecter) |
toutes les autres routes /v1/… (/v1/apply, /v1/secrets, /v1/iam/policies, /v1/tenants/{tenant}/…, /v1/tier…) | la session de tenant |
GET /v1/iam/jwks.json | aucun (public) |
- Un seul jeton, toujours dans
Authorization: Bearer <jeton>. - Le tenant d'une requête vient uniquement de la session. Aucun en-tête ni paramètre ne permet d'en désigner un autre.
- L'ancien en-tête
X-Tenant-Tokenest refusé sur toutes les routes/v1, y comprisPOST /v1/iam/sessionet/v1/iam/policies:400 {"error": "X-Tenant-Token is no longer supported: upgrade sng"}. Retirez-le : la session de tenant dansAuthorizationsuffit. - Présenter le mauvais jeton (une identité sur une route du tenant, une session sur une route d'identité) donne
401.
1. Obtenir un jeton d'identité
Quatre identités sont reconnues. Chacune devient un sujet auquel les policies du tenant s'appliquent.
| Identité | Sujet | Tenant d'origine | Entrer dans son tenant d'origine |
|---|---|---|---|
| Utilisateur du SSO (navigateur) | user:<sub> + rôles org/nom | aucun | — |
Application machine (client_credentials) | app:<nom SSO> | le tenant de son IAMApplication, si elle est enregistrée comme machine | sans policy |
Application qui tourne sur la plateforme (jeton d'application, audience stackngo-api) | sa:<tenant>/<nom> | son tenant | sans policy |
Job CI (jeton OIDC GitLab ou GitHub, déclaré par un IAMOIDCTrust) | oidc:<tenant>/<trust> | le tenant du trust | sans policy |
Entrer dans un autre tenant, ou entrer pour un utilisateur, exige l'action iam:assumerole sur tenant/<tenant>, accordée par une policy du tenant visé (policy gérée assume-role).
Utilisateur : navigateur (PKCE)
Le SSO de la plateforme est https://auth.dev.stackngo.app. C'est ce que fait sng sso login ; pour l'implémenter dans un outil à vous :
- Ouvrir
https://auth.dev.stackngo.app/login/oauth/authorizeavecclient_id,response_type=code,redirect_uri,scope=openid profile email,state,code_challengeetcode_challenge_method=S256. - Au retour sur
redirect_uri, échanger le code :
curl -fsS -X POST https://auth.dev.stackngo.app/api/login/oauth/access_token \
-d grant_type=authorization_code \
-d client_id="$CLIENT_ID" \
-d code="$CODE" \
-d code_verifier="$VERIFIER" \
-d redirect_uri="http://127.0.0.1:53817/callback"
{ "access_token": "eyJhbGciOi…", "refresh_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 604800 }
Le client_id est celui d'une application OAuth2 du SSO de la plateforme ; celui de la CLI est intégré au binaire sng. L'enregistrement d'une application tierce sur le SSO de la plateforme n'est pas documenté dans cette version : pour un outil à vous, le plus simple est de réutiliser le jeton obtenu par sng sso login (access_token du fichier de configuration).
Seuls les comptes de l'organisation de la plateforme ouvrent une session : un utilisateur de l'annuaire de votre tenant (sng iam user invite) est refusé avec 401 invalid identity token. La durée du jeton d'identité est fixée par le SSO ; la valeur ci-dessus est illustrative.
Machine externe : client_credentials
Déclarez une IAMApplication avec grantTypes: [client_credentials], lisez clientId et clientSecret dans son Secret <nom>-oidc, puis :
ACCESS=$(curl -fsS -X POST https://auth.dev.stackngo.app/api/login/oauth/access_token \
-d grant_type=client_credentials \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" | jq -r .access_token)
SESSION=$(curl -fsS -X POST https://api.dev.stackngo.fr/v1/iam/session \
-H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" \
-d '{"tenant":"acme"}' | jq -r .access_token)
Une application n'entre dans son tenant sans policy que si elle est enregistrée comme machine par la plateforme ; une IAMApplication que vous déclarez ne l'est pas dans cette version. Sinon elle est traitée comme une identité sans tenant d'origine : attachez-lui assume-role (sng tenant policy attach assume-role --app <nom SSO>) et précisez toujours tenant. Voir Machines et comptes de service.
Application qui tourne sur la plateforme
Une application déployée dans votre tenant présente le jeton d'application que la plateforme met à sa disposition, avec l'audience stackngo-api. Le corps est facultatif : sans tenant, la session est ouverte sur le tenant de l'application.
APP_TOKEN=$(cat /var/run/secrets/stackngo/token) # fichier fourni à l'application, audience stackngo-api
SESSION=$(curl -fsS -X POST https://api.dev.stackngo.fr/v1/iam/session \
-H "Authorization: Bearer $APP_TOKEN" | jq -r .access_token)
Un jeton sans l'audience stackngo-api, ou expiré, est refusé : 401 invalid identity token.
Fonctionnalité en cours de déploiement. L'interface décrite ici est celle de la prochaine livraison : elle peut encore évoluer, et n'est pas forcément activée sur votre tenant.
La fédération des pipelines CI est activée progressivement.
Job CI : jeton OIDC
GitLab CI (id_tokens) et GitHub Actions délivrent à chaque job un jeton OIDC signé. Si un IAMOIDCTrust du tenant le reconnaît, il s'échange contre une session. L'audience à demander est l'URL de l'API, https://api.dev.stackngo.fr, sauf si le trust en déclare une autre.
deploy:
id_tokens:
STNG_OIDC_TOKEN:
aud: https://api.dev.stackngo.fr
script:
- |
SESSION=$(curl -fsS -X POST https://api.dev.stackngo.fr/v1/iam/session \
-H "Authorization: Bearer $STNG_OIDC_TOKEN" -H "Content-Type: application/json" \
-d '{"tenant":"acme"}' | jq -r .access_token)
Pour un job CI, tenant est obligatoire. trust désigne le trust à utiliser quand plusieurs trusts du tenant pourraient correspondre ; sans lui, exactement un trust doit correspondre. La session dure au plus la maxSessionDuration du trust (1 h par défaut). Avec la CLI, rien de tout cela n'est nécessaire : voir Déployer sans secret (OIDC).
2. Ouvrir une session : POST /v1/iam/session
POST /v1/iam/session
Authorization: Bearer <jeton d'identité>
Content-Type: application/json
Corps (facultatif pour une application de la plateforme ou une machine qui entre dans son propre tenant) :
| Champ | Type | Description |
|---|---|---|
tenant | string | tenant visé ; requis pour un utilisateur et pour un job CI |
trust | string | jeton OIDC de CI uniquement : nom du trust à utiliser |
Tout autre champ est refusé (400 invalid JSON body).
curl -sS -X POST https://api.dev.stackngo.fr/v1/iam/session \
-H "Authorization: Bearer $ID_TOKEN" -H "Content-Type: application/json" \
-d '{"tenant":"acme"}'
Réponse 200 :
{
"tenant": "acme",
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9…",
"token_type": "Bearer",
"expires_in": 3599
}
| Champ | Type | Description |
|---|---|---|
tenant | string | tenant de la session |
origin_tenant | string | tenant d'origine du sujet ; absent pour un utilisateur |
access_token | string | la session, JWT RS256 |
token_type | string | toujours Bearer |
expires_in | integer | secondes avant expiration (3600 au plus) |
Tenant tout juste créé : 409 policies_pending
Pendant les 5 minutes qui suivent la création d'un tenant, ses policies par défaut ne sont peut-être pas encore appliquées. Si votre droit d'entrée manque encore, l'API répond :
HTTP/1.1 409 Conflict
Retry-After: 10
Content-Type: application/json
{"error":"policies_pending"}
Attendez Retry-After secondes et rejouez la requête. sng tenant use le fait tout seul, jusqu'à 6 minutes. Passé 5 minutes, un droit absent donne 403.
Codes de réponse
| Code | error | Cause |
|---|---|---|
400 | invalid JSON body | corps illisible ou champ inconnu |
400 | tenant required | pas de tenant pour un utilisateur, un job CI ou une application sans tenant d'origine |
400 | invalid tenant | nom de tenant mal formé |
401 | missing or invalid Authorization header | pas d'en-tête Authorization: Bearer |
401 | invalid identity token | jeton expiré, mauvaise audience, compte hors organisation de la plateforme ; pour un job CI : aucun trust (ou plusieurs) ne correspond |
401 | session cannot be issued | session impossible à émettre |
403 | forbidden: no iam:AssumeRole on tenant/acme | aucune policy du tenant visé n'accorde iam:assumerole au sujet |
404 | unknown tenant | le tenant n'existe pas |
409 | policies_pending | tenant créé il y a moins de 5 minutes ; voir Retry-After |
429 | rate limit exceeded | trop de requêtes depuis votre adresse (300 par minute) ; voir Retry-After |
503 | authorization service unavailable | service d'autorisation injoignable : jamais d'accord par défaut, réessayez |
503 | tenant registry unavailable | registre des tenants injoignable |
503 | oidc federation unavailable | émetteur OIDC du CI injoignable |
3. Changer de tenant : POST /v1/iam/session/assume
Échange une session du tenant A contre une session du tenant B, sans rejouer l'identité. C'est sng tenant assume.
POST /v1/iam/session/assume
Authorization: Bearer <session du tenant A>
Content-Type: application/json
{"tenant": "acme-staging"}
| Champ | Type | Description |
|---|---|---|
tenant | string | tenant visé, requis |
curl -sS -X POST https://api.dev.stackngo.fr/v1/iam/session/assume \
-H "Authorization: Bearer $SESSION" -H "Content-Type: application/json" \
-d '{"tenant":"acme-staging"}'
{
"tenant": "acme-staging",
"origin_tenant": "acme",
"access_token": "eyJhbGciOiJSUzI1NiIs…",
"token_type": "Bearer",
"expires_in": 2140
}
- Une policy d'
acme-stagingdoit accorderiam:assumerolesurtenant/acme-stagingau sujet de la session, même s'il s'agit d'une machine. - La nouvelle session n'expire jamais après la session source : enchaîner des
assumene prolonge pas un accès (expires_inci-dessus est plus court que 3600). origin_tenantest conservé pour l'audit.
Codes : 401 missing or invalid Authorization header ; 401 invalid session (session absente, invalide ou expirée) ; 400 tenant required (corps absent ou sans tenant) ; puis les mêmes codes que POST /v1/iam/session (400 invalid tenant, 403, 404, 409 policies_pending, 503).
4. Utiliser la session
curl -fsS https://api.dev.stackngo.fr/v1/secrets -H "Authorization: Bearer $SESSION"
La session ne se rafraîchit pas : à l'expiration (401), rejouez POST /v1/iam/session avec votre identité, ou POST /v1/iam/session/assume avec une session encore valide. La CLI le fait d'elle-même.
401 ou 403 ?
| Réponse | Route | Signification | Que faire |
|---|---|---|---|
401 missing or invalid Authorization header | toutes | pas de Bearer | ajouter l'en-tête |
401 tenant session required: run sng tenant use NAME | routes du tenant | session absente, invalide, expirée ou terminée par une déconnexion, ou jeton d'identité présenté à la place | rouvrir une session |
401 tenant session required | /v1/iam/policies…, /v1/iam/bindings…, /v1/iam/oidc-trusts… | idem | idem |
401 invalid token | routes d'identité | jeton d'identité invalide ou expiré, ou session présentée à la place | sng sso login |
403 organization missing in token | routes d'identité | jeton du SSO sans organisation | se connecter avec un compte de la plateforme |
403 forbidden | routes du tenant | la session est valide, mais aucune policy du tenant n'autorise l'action (ou le {tenant} du chemin n'est pas celui de la session) | voir le catalogue des actions, attacher une policy |
403 forbidden: insufficient permissions | /v1/iam/policies…, /v1/iam/oidc-trusts… | pas de policy:* ou oidctrust:* | policy admin |
503 authorization service unavailable | routes du tenant | décision impossible : refus par défaut | réessayer |
Un retrait de policy prend effet en quelques minutes ; un retrait de rôle SSO, à l'expiration de la session (1 h au plus).
5. Vérifier une session : GET /v1/iam/jwks.json
La session est un JWT RS256. Les clés publiques sont servies sans authentification, avec Cache-Control: public, max-age=300 :
curl -fsS https://api.dev.stackngo.fr/v1/iam/jwks.json
{
"keys": [
{ "kty": "RSA", "kid": "pW3mK1q…", "use": "sig", "alg": "RS256", "n": "xGq…", "e": "AQAB" }
]
}
Claims de la session :
| Claim | Contenu |
|---|---|
iss | URL de l'API du cluster qui a émis la session (ex. https://ovh-gra-0.api.dev.stackngo.fr) ; ses clés publiques sont sous <iss>/v1/iam/jwks.json |
aud | stackngo |
sub | identifiant du sujet : sub SSO d'un utilisateur, <tenant>/<nom> d'une application de la plateforme ou d'un trust, nom SSO d'une application |
kind | user, sa, app ou oidc |
name | nom lisible (pour un job CI : le sub du jeton OIDC) |
org | organisation SSO (utilisateur, application) |
tenant | tenant de la session |
origin_tenant | tenant d'origine, s'il y en a un |
roles | rôles SSO org/nom (utilisateur) |
iat, nbf, exp, jti | émission, validité (nbf = émission − 5 s), expiration, identifiant unique |
Vérifiez la signature avec la clé de même kid, puis aud = stackngo, iss et exp.
Exemple complet : un script qui lit un secret
#!/usr/bin/env bash
set -euo pipefail
API=https://api.dev.stackngo.fr/v1
# Identité : ici une machine externe (client_credentials)
ACCESS=$(curl -fsS -X POST https://auth.dev.stackngo.app/api/login/oauth/access_token \
-d grant_type=client_credentials -d client_id="$CLIENT_ID" -d client_secret="$CLIENT_SECRET" \
| jq -r .access_token)
# Session, en respectant Retry-After si le tenant est tout neuf
for i in 1 2 3 4 5 6; do
RESP=$(curl -sS -D headers.txt -o body.json -w '%{http_code}' -X POST "$API/iam/session" \
-H "Authorization: Bearer $ACCESS" -H "Content-Type: application/json" -d '{"tenant":"acme"}')
[ "$RESP" = 200 ] && break
[ "$RESP" = 409 ] || { cat body.json; exit 1; }
sleep "$(awk -F': ' 'tolower($1)=="retry-after"{print $2+0}' headers.txt)"
done
SESSION=$(jq -r .access_token body.json)
# Lecture d'un secret (ssm:read sur secret/db-password)
curl -fsS "$API/secrets/db-password/value" -H "Authorization: Bearer $SESSION"
6. Se déconnecter : POST /v1/iam/logout
La déconnexion met fin à des sessions avant leur expiration. L'effet dépend du jeton présenté :
| Jeton présenté | Sessions terminées |
|---|---|
| une session de tenant | cette session seule ; vos autres sessions (autre terminal, autre tenant) restent valides |
| le jeton d'identité | toutes les sessions de tenant ouvertes avec ce compte jusqu'ici, où qu'elles soient |
# fermer la session courante seulement
curl -fsS -X POST https://api.dev.stackngo.fr/v1/iam/logout -H "Authorization: Bearer $SESSION"
# fermer toutes les sessions du compte (c'est ce que fait sng sso logout)
curl -fsS -X POST https://api.dev.stackngo.fr/v1/iam/logout -H "Authorization: Bearer $ID_TOKEN"
Réponse : 204, aussi pour une session déjà terminée. L'effet est visible en quelques secondes
sur toutes les routes : une session terminée reçoit 401, comme une session expirée (401 tenant session required: run sng tenant use NAME sur les routes du tenant). Elle ne peut pas
non plus servir à POST /v1/iam/session/assume.
- Le jeton d'identité lui-même n'est pas révoqué : oubliez-le. Tant qu'il n'a pas expiré, il peut encore ouvrir une nouvelle session.
401 invalid token: jeton absent ou invalide.503: déconnexion momentanément impossible, réessayez ; dans ce cas, aucune session n'est acceptée non plus.