Skip to main content

Authentication

Available

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​

RoutesToken expected in Authorization: Bearer
POST /v1/iam/sessionthe identity token (one of the four below)
POST /v1/iam/session/assumea tenant session
GET /v1/iam/me, GET /v1/tenants, POST /v1/tenants, GET /v1/templates[/{kind}]the SSO user identity token
POST /v1/iam/logouta 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.jsonnone (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-Token header is refused on every /v1 route, including POST /v1/iam/session and /v1/iam/policies: 400 {"error": "X-Tenant-Token is no longer supported: upgrade sng"}. Remove it: the tenant session in Authorization is 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.

IdentitySubjectOrigin tenantEntering its origin tenant
SSO user (browser)user:<sub> + org/nom rolesnone—
Machine application (client_credentials)app:<nom SSO>the tenant of its IAMApplication, if it is registered as a machinewithout a policy
Application running on the platform (application token, audience stackngo-api)sa:<tenant>/<nom>its tenantwithout a policy
CI job (GitLab or GitHub OIDC token, declared by an IAMOIDCTrust)oidc:<tenant>/<trust>the trust's tenantwithout 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:

  1. Open https://auth.dev.stackngo.app/login/oauth/authorize with client_id, response_type=code, redirect_uri, scope=openid profile email, state, code_challenge, and code_challenge_method=S256.
  2. 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)
Origin tenant of an application

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.

Beta

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.

.gitlab-ci.yml
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):

FieldTypeDescription
tenantstringtargeted tenant; required for a user and for a CI job
truststringCI 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
}
FieldTypeDescription
tenantstringtenant of the session
origin_tenantstringorigin tenant of the subject; absent for a user
access_tokenstringthe session, RS256 JWT
token_typestringalways Bearer
expires_inintegerseconds 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​

CodeerrorCause
400invalid JSON bodyunreadable body or unknown field
400tenant requiredno tenant for a user, a CI job, or an application without an origin tenant
400invalid tenantmalformed tenant name
401missing or invalid Authorization headerno Authorization: Bearer header
401invalid identity tokenexpired token, wrong audience, account outside the platform's organization; for a CI job: no trust (or several) matches
401session cannot be issuedthe session cannot be issued
403forbidden: no iam:AssumeRole on tenant/acmeno policy of the targeted tenant grants iam:assumerole to the subject
404unknown tenantthe tenant does not exist
409policies_pendingtenant created less than 5 minutes ago; see Retry-After
429rate limit exceededtoo many requests from your address (300 per minute); see Retry-After
503authorization service unavailableauthorization service unreachable: never granted by default, try again
503tenant registry unavailabletenant registry unreachable
503oidc federation unavailableCI 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"}
FieldTypeDescription
tenantstringtargeted 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-staging must grant iam:assumerole on tenant/acme-staging to 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_in above is shorter than 3600).
  • origin_tenant is 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?​

ResponseRouteMeaningWhat to do
401 missing or invalid Authorization headerallno Beareradd the header
401 tenant session required: run sng tenant use NAMEtenant routessession missing, invalid, expired or ended by a sign-out, or identity token presented insteadreopen a session
401 tenant session required/v1/iam/policies…, /v1/iam/bindings…, /v1/iam/oidc-trusts…samesame
401 invalid tokenidentity routesinvalid or expired identity token, or session presented insteadsng sso login
403 organization missing in tokenidentity routesSSO token without an organizationsign in with a platform account
403 forbiddentenant routesthe 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 unavailabletenant routesdecision impossible: denied by defaulttry 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:

ClaimContent
issURL 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
audstackngo
subsubject identifier: SSO sub of a user, <tenant>/<nom> of a platform application or a trust, SSO name of an application
kinduser, sa, app, or oidc
namereadable name (for a CI job: the sub of the OIDC token)
orgSSO organization (user, application)
tenanttenant of the session
origin_tenantorigin tenant, if there is one
rolesSSO roles org/nom (user)
iat, nbf, exp, jtiissuance, 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 presentedSessions ended
a tenant sessionthat session only; your other sessions (another terminal, another tenant) stay valid
the identity tokenevery 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.

See also​