Authentication
Deployed and supported. The fields and commands described here are stable within version v1alpha1.
Every request on your tenant carries a tenant session: a token signed by StackNGO, valid for 1 hour, that says who you are and in which tenant you are acting. You obtain it in two steps: an identity token (proof of who you are), then a POST /v1/iam/session exchange.
API=https://api.dev.stackngo.fr/v1
# 1. identity (here, the one sng sso login saved)
ID_TOKEN=$(yq '.access_token' ~/.config/stackngo/config.yaml)
# 2. session on the acme tenant
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. call a tenant route
curl -fsS "$API/secrets" -H "Authorization: Bearer $SESSION"
The model (why two tokens, the tenant boundary) is explained in The identity, session, tenant model.
A single token per request
| Routes | Token expected in Authorization: Bearer |
|---|---|
POST /v1/iam/session | the identity token (one of the four below) |
POST /v1/iam/session/assume | a tenant session |
GET /v1/iam/me, GET /v1/tenants, POST /v1/tenants, GET /v1/templates[/{kind}] | the SSO user identity token |
POST /v1/iam/logout | a tenant session, or the identity token (see Sign out) |
all other /v1/… routes (/v1/apply, /v1/secrets, /v1/iam/policies, /v1/tenants/{tenant}/…, /v1/tier…) | the tenant session |
GET /v1/iam/jwks.json | none (public) |
- A single token, always in
Authorization: Bearer <jeton>. - The tenant of a request comes only from the session. No header or parameter lets you designate another one.
- The former
X-Tenant-Tokenheader is refused on every/v1route, includingPOST /v1/iam/sessionand/v1/iam/policies:400 {"error": "X-Tenant-Token is no longer supported: upgrade sng"}. Remove it: the tenant session inAuthorizationis enough. - Presenting the wrong token (an identity on a tenant route, a session on an identity route) gives
401.
1. Obtain an identity token
Four identities are recognized. Each one becomes a subject to which the tenant's policies apply.
| Identity | Subject | Origin tenant | Entering its origin tenant |
|---|---|---|---|
| SSO user (browser) | user:<sub> + org/nom roles | none | — |
Machine application (client_credentials) | app:<nom SSO> | the tenant of its IAMApplication, if it is registered as a machine | without a policy |
Application running on the platform (application token, audience stackngo-api) | sa:<tenant>/<nom> | its tenant | without a policy |
CI job (GitLab or GitHub OIDC token, declared by an IAMOIDCTrust) | oidc:<tenant>/<trust> | the trust's tenant | without a policy |
Entering another tenant, or entering as a user, requires the iam:assumerole action on tenant/<tenant>, granted by a policy of the targeted tenant (managed policy assume-role).
User: browser (PKCE)
The platform's SSO is https://auth.dev.stackngo.app. This is what sng sso login does; to implement it in a tool of your own:
- Open
https://auth.dev.stackngo.app/login/oauth/authorizewithclient_id,response_type=code,redirect_uri,scope=openid profile email,state,code_challenge, andcode_challenge_method=S256. - On return to
redirect_uri, exchange the 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 }
The client_id is that of an OAuth2 application of the platform's SSO; the CLI's is built into the sng binary. Registering a third-party application on the platform's SSO is not documented in this version: for a tool of your own, the simplest is to reuse the token obtained by sng sso login (access_token in the configuration file).
Only accounts of the platform's organization can open a session: a user of your tenant's directory (sng iam user invite) is refused with 401 invalid identity token. The lifetime of the identity token is set by the SSO; the value above is illustrative.
External machine: client_credentials
Declare an IAMApplication with grantTypes: [client_credentials], read clientId and clientSecret from its <nom>-oidc Secret, then:
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)
An application only enters its tenant without a policy if it is registered as a machine by the platform; an IAMApplication that you declare is not, in this version. Otherwise it is treated as an identity without an origin tenant: attach assume-role to it (sng tenant policy attach assume-role --app <nom SSO>) and always specify tenant. See Machines and technical accounts.
Application running on the platform
An application deployed in your tenant presents the application token that the platform makes available to it, with the audience stackngo-api. The body is optional: without tenant, the session is opened on the application's tenant.
APP_TOKEN=$(cat /var/run/secrets/stackngo/token) # file provided to the 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)
A token without the stackngo-api audience, or an expired one, is refused: 401 invalid identity token.
Being rolled out. The interface described here is the one of the next release: it may still change, and may not be enabled on your tenant yet.
CI pipeline federation is being rolled out gradually.
CI job: OIDC token
GitLab CI (id_tokens) and GitHub Actions issue a signed OIDC token to each job. If an IAMOIDCTrust of the tenant recognizes it, it is exchanged for a session. The audience to request is the API URL, https://api.dev.stackngo.fr, unless the trust declares another one.
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)
For a CI job, tenant is required. trust designates the trust to use when several of the tenant's trusts could match; without it, exactly one trust must match. The session lasts at most the trust's maxSessionDuration (1 h by default). With the CLI, none of this is needed: see Deploy without secrets (OIDC).
2. Open a session: POST /v1/iam/session
POST /v1/iam/session
Authorization: Bearer <jeton d'identité>
Content-Type: application/json
Body (optional for a platform application or a machine entering its own tenant):
| Field | Type | Description |
|---|---|---|
tenant | string | targeted tenant; required for a user and for a CI job |
trust | string | CI OIDC token only: name of the trust to use |
Any other field is refused (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"}'
200 response:
{
"tenant": "acme",
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ii4uLiJ9…",
"token_type": "Bearer",
"expires_in": 3599
}
| Field | Type | Description |
|---|---|---|
tenant | string | tenant of the session |
origin_tenant | string | origin tenant of the subject; absent for a user |
access_token | string | the session, RS256 JWT |
token_type | string | always Bearer |
expires_in | integer | seconds before expiry (at most 3600) |
Tenant just created: 409 policies_pending
During the 5 minutes after a tenant is created, its default policies may not be applied yet. If your right to enter is still missing, the API responds:
HTTP/1.1 409 Conflict
Retry-After: 10
Content-Type: application/json
{"error":"policies_pending"}
Wait Retry-After seconds and replay the request. sng tenant use does this on its own, for up to 6 minutes. After 5 minutes, a missing right gives 403.
Response codes
| Code | error | Cause |
|---|---|---|
400 | invalid JSON body | unreadable body or unknown field |
400 | tenant required | no tenant for a user, a CI job, or an application without an origin tenant |
400 | invalid tenant | malformed tenant name |
401 | missing or invalid Authorization header | no Authorization: Bearer header |
401 | invalid identity token | expired token, wrong audience, account outside the platform's organization; for a CI job: no trust (or several) matches |
401 | session cannot be issued | the session cannot be issued |
403 | forbidden: no iam:AssumeRole on tenant/acme | no policy of the targeted tenant grants iam:assumerole to the subject |
404 | unknown tenant | the tenant does not exist |
409 | policies_pending | tenant created less than 5 minutes ago; see Retry-After |
429 | rate limit exceeded | too many requests from your address (300 per minute); see Retry-After |
503 | authorization service unavailable | authorization service unreachable: never granted by default, try again |
503 | tenant registry unavailable | tenant registry unreachable |
503 | oidc federation unavailable | CI OIDC issuer unreachable |
3. Switch tenant: POST /v1/iam/session/assume
Exchanges a session of tenant A for a session of tenant B, without replaying the identity. This is sng tenant assume.
POST /v1/iam/session/assume
Authorization: Bearer <session du tenant A>
Content-Type: application/json
{"tenant": "acme-staging"}
| Field | Type | Description |
|---|---|---|
tenant | string | targeted tenant, required |
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
}
- A policy of
acme-stagingmust grantiam:assumeroleontenant/acme-stagingto the session's subject, even if it is a machine. - The new session never expires after the source session: chaining
assumes does not extend access (expires_inabove is shorter than 3600). origin_tenantis kept for auditing.
Codes: 401 missing or invalid Authorization header; 401 invalid session (session missing, invalid, or expired); 400 tenant required (no body or no tenant); then the same codes as POST /v1/iam/session (400 invalid tenant, 403, 404, 409 policies_pending, 503).
4. Use the session
curl -fsS https://api.dev.stackngo.fr/v1/secrets -H "Authorization: Bearer $SESSION"
The session is not refreshed: on expiry (401), replay POST /v1/iam/session with your identity, or POST /v1/iam/session/assume with a session that is still valid. The CLI does this on its own.
401 or 403?
| Response | Route | Meaning | What to do |
|---|---|---|---|
401 missing or invalid Authorization header | all | no Bearer | add the header |
401 tenant session required: run sng tenant use NAME | tenant routes | session missing, invalid, expired or ended by a sign-out, or identity token presented instead | reopen a session |
401 tenant session required | /v1/iam/policies…, /v1/iam/bindings…, /v1/iam/oidc-trusts… | same | same |
401 invalid token | identity routes | invalid or expired identity token, or session presented instead | sng sso login |
403 organization missing in token | identity routes | SSO token without an organization | sign in with a platform account |
403 forbidden | tenant routes | the session is valid, but no policy of the tenant allows the action (or the {tenant} in the path is not the session's) | see the action catalog, attach a policy |
403 forbidden: insufficient permissions | /v1/iam/policies…, /v1/iam/oidc-trusts… | no policy:* or oidctrust:* | admin policy |
503 authorization service unavailable | tenant routes | decision impossible: denied by default | try again |
Removing a policy takes effect within a few minutes; removing an SSO role, when the session expires (at most 1 h).
5. Verify a session: GET /v1/iam/jwks.json
The session is an RS256 JWT. The public keys are served without authentication, with 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" }
]
}
Session claims:
| Claim | Content |
|---|---|
iss | URL of the API of the cluster that issued the session (e.g. https://ovh-gra-0.api.dev.stackngo.fr); its public keys are under <iss>/v1/iam/jwks.json |
aud | stackngo |
sub | subject identifier: SSO sub of a user, <tenant>/<nom> of a platform application or a trust, SSO name of an application |
kind | user, sa, app, or oidc |
name | readable name (for a CI job: the sub of the OIDC token) |
org | SSO organization (user, application) |
tenant | tenant of the session |
origin_tenant | origin tenant, if there is one |
roles | SSO roles org/nom (user) |
iat, nbf, exp, jti | issuance, validity (nbf = issuance − 5 s), expiry, unique identifier |
Verify the signature with the key of the same kid, then aud = stackngo, iss, and exp.
Complete example: a script that reads a secret
#!/usr/bin/env bash
set -euo pipefail
API=https://api.dev.stackngo.fr/v1
# Identity: here an external machine (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, respecting Retry-After if the tenant is brand new
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)
# Read a secret (ssm:read on secret/db-password)
curl -fsS "$API/secrets/db-password/value" -H "Authorization: Bearer $SESSION"
6. Sign out: POST /v1/iam/logout
Signing out ends sessions before they expire. The effect depends on the token presented:
| Token presented | Sessions ended |
|---|---|
| a tenant session | that session only; your other sessions (another terminal, another tenant) stay valid |
| the identity token | every tenant session opened with that account so far, wherever they are |
# close the current session only
curl -fsS -X POST https://api.dev.stackngo.fr/v1/iam/logout -H "Authorization: Bearer $SESSION"
# close every session of the account (this is what sng sso logout does)
curl -fsS -X POST https://api.dev.stackngo.fr/v1/iam/logout -H "Authorization: Bearer $ID_TOKEN"
Response: 204, also for a session already ended. The effect shows within a few seconds on
every route: an ended session receives 401, like an expired session (401 tenant session required: run sng tenant use NAME on tenant routes). It can no longer be used for
POST /v1/iam/session/assume either.
- The identity token itself is not revoked: forget it. As long as it has not expired, it can still open a new session.
401 invalid token: token missing or invalid.503: sign-out temporarily impossible, try again; in that case, no session is accepted either.