Configure SSO enterprise login
SSO (Single Sign-On) lets you call the gateway with a token issued by your enterprise identity system (IdP, Identity Provider, such as Azure Entra / Okta), or log into the console with an enterprise account — without issuing a gateway key to each person/program separately.
Entry point: Console → Settings → SSO tab. The configuration is stored in the database and takes effect immediately after changes, with no restart and no environment variables needed.
SSO does two mutually independent things in the gateway; you can enable just one, or both for the same provider:
- Data-plane authentication: your program calls the proxy endpoints like
/v1/...with an IdP-issued Bearer token; after the gateway verifies the signature, it takes a name from the token, looks up a real key with the same name in the access key table, and applies that key's permissions. - Console login: a person clicks "Log in with enterprise account" on the login page, jumps to the IdP to authenticate, and on return the gateway takes the username from the id_token and looks up a user with the same name in the console user table; only a match is let in.
Core principle: the gateway never fabricates an identity out of thin air. For the data plane you must pre-create an access key with the same name; for the console you must pre-create a console user with the same name; if not found it always rejects (401) and never synthesizes one on the fly.
Don't confuse this with Upstream SSO credential login: this page covers "authenticating who calls the gateway with an enterprise IdP"; that page covers "the admin logging into an upstream vendor (e.g. Kiro) to obtain a refresh credential for the gateway to forward with". The directions are opposite.
Understand the four words in 30 seconds
They recur below; get familiar with them first:
- issuer: the
issfield in the token — a sentence the IdP writes meaning "I issued this token"; it is a URL. The gateway uses it to match the passphrase — the token'sissmust equal the issuer you fill in here, and only then does the gateway accept that this token belongs to this provider. - audience: the
audfield in the token — "which application this ticket is issued to". The gateway uses it to confirm the token is issued to your gateway, not to another application. - discovery document (the IdP's self-introduction): every IdP has a public JSON self-introduction that spells out four things — ① who I am
issuer, ② where my public key list isjwks_uri, ③ the login URL, ④ the token-exchange URL. The gateway prefers to read this self-introduction to get those URLs, rather than guessing. - JWKS / public key list: the IdP's public key list, which the gateway uses to verify "the token hasn't been tampered with".
Three provider types, what to fill in each box
On the page, each provider has these core boxes (common to all three types):
| Box | What to fill in |
|---|---|
| ID | An internal name you give this provider (e.g. corp-azure), used in URLs; don't change it after creation. |
| Issuer URL | Used exactly as you typed; the gateway will not assemble it from a domain/tenant. The token's iss must equal it (trailing slash is fine either way, the gateway normalizes it when comparing). |
| Audience | Must equal the token's aud. Data plane only. |
| Client ID | The application ID you registered with the IdP. Console login uses it to exchange tokens + validate the id_token audience; SPA mode also uses it for exact client matching on the data plane. |
| Attribute mapping name_expr | Data plane: which claim of the token to take the "name" from, to find a key with the same name in the access key table (JSONPath or expression, e.g. $.oid, $.upn). |
| Console username username_expr | Console login: which claim of the id_token to take the username from, to find a user with the same name in the console user table. |
For Type choose azure / okta / generic: the first two the gateway knows the URL conventions of and can derive automatically; generic is for any standard OIDC, where the login/token-exchange URLs must be filled in by hand.
The advanced collapse section also has: jwks_uri (hand-fill the public key URL, usually not needed), the Use OIDC discovery toggle, Discovery URL (optional), clock tolerance, and the public key refresh interval. The Azure section below covers when discovery_url must be filled in.
Azure section (focus)
Azure is the easiest to misconfigure, because its URL system is split and the v1/v2 token shapes differ. Copy the following and it should work on the first try.
What to fill in for issuer
- Global v2: go to the Entra overview page and copy the Directory (tenant) ID (a GUID), fill it into the template
https://login.microsoftonline.com/<paste tenant ID>/v2.0, and the whole string is the issuer. Do not omit the trailing/v2.0, and don't fill incommon,organizations, or{tenantid}. - v1 / China cloud / sovereign cloud: directly fill in the token's
iss(decode the token to read theissfield, or assemble by tenant ashttps://sts.chinacloudapi.cn/<tenant ID>). This token'sissoften has a trailing slash — no matter, the gateway normalizes it when comparing.
When does audience carry api://
Look at what you set in the portal's Expose an API → Application ID URI:
- Set to a bare GUID (default) → audience is the bare GUID (same as Client ID).
- Set to
api://<client_id>→ the token'saudwill carry theapi://prefix, and here you must also fillapi://<client_id>, not just the bare GUID, otherwise it won't match → 401.
Where to find discovery_url (required for v1 / China cloud / sovereign cloud)
Why you need it: the v1 / China cloud token's iss is on the sts.* domain, but the IdP's self-introduction (discovery document) is on the login.* domain — two different URLs. issuer is only used to match the passphrase and cannot be used to assemble the self-introduction URL (that would 404). So you must tell the gateway separately where the self-introduction is. Global v2 doesn't need it (leave blank), because iss and the self-introduction share the same address.
Preferred: copy from the portal directly, zero guessing.
- Enter the Entra portal (China cloud is
portal.azure.cn) → left-side App registrations → click your application. - Left menu Endpoints.
- In the right panel, the "OpenID Connect metadata document" row — copy the whole row into Discovery URL.
This row is usually the v2 one (with
/v2.0); no problem, it works: the v1 and v2 signing keys of the same application/tenant share one set, so the v2 self-introduction's public keys still verify your v1 token.
Alternative: hand-assemble the template ({tid} is the tid in the token, or the Directory (tenant) ID on the portal overview page):
- China cloud:
https://login.partner.microsoftonline.cn/{tid}/v2.0/.well-known/openid-configuration - Global cloud:
https://login.microsoftonline.com/{tid}/v2.0/.well-known/openid-configuration
⚠️ China cloud's login domain is
login.partner.microsoftonline.cn, notchinacloudapi.cn— the latter is the Microsoft Graph domain, don't confuse them. Take the portal's Endpoints panel as authoritative.
How to confirm you assembled it right: open the discovery_url in a browser or curl — it should return JSON containing jwks_uri; then open the jwks_uri inside it, and seeing the public key list means it's solid.
name_expr: map a claim to the access key's name
- v1 access token has
upn(e.g.alice@corp.example.com): if your access key name is built as the part before@, use an expression to strip the domain:{{ replace(claim:$.upn, "@.*$", "") }}→alice. - v2 access token has no
upn: use$.oid(the full user object ID), and build the access key's name as that OID string. - Prerequisite: there must be a key in the access key table whose name equals the parse result, otherwise 401.
Complete configuration examples
Example A: Azure China cloud v1 (with discovery_url)
{
"id": "azure-cn",
"type": "azure",
"enabled": true,
// Match the passphrase: character-for-character equal to the token's iss
// (trailing slash is fine either way). The tenant/object IDs below are
// example placeholders — replace them with the Directory (tenant) ID /
// Application (client) ID you copied from the portal overview page.
"issuer": "https://sts.chinacloudapi.cn/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
// Who the ticket is issued to: character-for-character equal to the token's
// aud. Here it carries api:// because the portal set the Application ID URI
// to api://<client_id>.
"audience": "api://11111111-2222-3333-4444-555555555555",
"client_id": "11111111-2222-3333-4444-555555555555",
// Data plane: strip upn after @ → alice (the access key table must have name=alice).
"access_key": {
"enabled": true,
"name_expr": "{{ replace(claim:$.upn, \"@.*$\", \"\") }}"
},
// ★ Self-introduction URL: copy the "OpenID Connect metadata document" row
// from the portal's Endpoints panel. The public key URL is derived from it
// automatically; no need to hand-fill jwks_uri.
"discovery_url": "https://login.partner.microsoftonline.cn/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/v2.0/.well-known/openid-configuration"
}Example B: global v2 (no discovery_url, zero changes to old config)
{
"id": "azure-global",
"type": "azure",
"enabled": true,
"issuer": "https://login.microsoftonline.com/<your tenant ID>/v2.0",
"audience": "<your aud>",
"client_id": "<your client id>",
"access_key": { "enabled": true, "name_expr": "$.oid" }
// No discovery_url → the gateway assembles the self-introduction URL from
// issuer (v2 assembles correctly).
}How multiple providers work
You can configure Azure + Okta + generic OIDC all at once. The gateway does not try verifying your token against each provider: it first decodes the token's iss, matches the passphrase to find the unique provider that claims it, and verifies the signature only once. Each provider's issuer is different, so a token is claimed by at most one provider.
A token claimed by no provider (not a JWT issued by an enterprise IdP, or an ordinary sk-... key) falls back to the gateway's built-in access key authentication — SSO will not wrongly reject your original keys.
Verification
Don't wait to hit the 401 table after a mishap; do an end-to-end verification first.
1. Get a test token
Get an access token issued by the IdP whose audience equals the value you configured for the gateway. With Azure you can use the Azure CLI directly:
# --resource is the audience you configured (carry the api:// prefix if you set it)
az account get-access-token \
--resource <your audience> \
--query accessToken -o tsvOkta / generic OIDC use each IdP's own token-issuance flow (OAuth authorization-code flow or client_credentials).
2. Verify data-plane authentication
Use this token as the Bearer to call the model list endpoint, expecting 200:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:7890/v1/models \
-H "Authorization: Bearer <the token from the previous step>"Returning 200 means the signature verified and a key with the same name was found in the access key table. Returning 401 means you should go down the troubleshooting table below item by item.
3. Verify console enterprise login
Open the console login page and click "Log in with enterprise account"; after jumping to IdP authentication and returning, you should be able to enter the console (prerequisite: the console user table already has an enabled user with the same name as the id_token username). After first entry, go to the "Console users" page to confirm the user exists and the role is correct.
Common 401 troubleshooting table
| Symptom / suspicion | How to check / fix |
|---|---|
| Token is v1 / China cloud, issuer doesn't match | Fill issuer with the token's iss as-is; don't fuss over the trailing slash. If the iss domain is sts.*, you must also fill discovery_url (the login domain). |
| audience doesn't match | Look at the portal's Expose an API → Application ID URI: if it's api://<id> fill it with api://; if it's a bare GUID fill the bare GUID. |
| Signature verification fails / can't fetch public keys | Is the discovery_url domain right (China cloud is login.partner.microsoftonline.cn, not chinacloudapi.cn); opening it in a browser should return JSON containing jwks_uri. |
| Signature verified but still 401 | name_expr can't extract a value, or the extracted name has no same-name key in the access key table. When a v2 access token has no upn/azp, don't use them — use $.oid. |
| Console login won't come back / reports not_in_list | The username extracted by the id_token's username_expr has no same-name enabled user in the console user table. |
| Token expired | Decode the token to read exp; if expired, re-fetch the token. |
| SSO configured but the token seems not to go through SSO | Confirm both the provider's enabled and access_key.enabled are on; confirm the SSO plugin is built into and enabled with the image (the official image enables it by default). |
Next steps
- Console login & roles — create console users, understand roles and sessions.
- Access key & key group fields — create the same-name access keys the data plane needs.
- Endpoints & auth — the auth order: how SSO and built-in keys connect.
