Skip to content
This page is a translation of the authoritative Chinese source and may lag behind.View the original

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 → SettingsSSO 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:

  1. 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.
  2. 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 iss field 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's iss must equal the issuer you fill in here, and only then does the gateway accept that this token belongs to this provider.
  • audience: the aud field 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 is jwks_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):

BoxWhat to fill in
IDAn internal name you give this provider (e.g. corp-azure), used in URLs; don't change it after creation.
Issuer URLUsed 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).
AudienceMust equal the token's aud. Data plane only.
Client IDThe 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_exprData 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_exprConsole 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 in common, organizations, or {tenantid}.
  • v1 / China cloud / sovereign cloud: directly fill in the token's iss (decode the token to read the iss field, or assemble by tenant as https://sts.chinacloudapi.cn/<tenant ID>). This token's iss often 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's aud will carry the api:// prefix, and here you must also fill api://<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.

  1. Enter the Entra portal (China cloud is portal.azure.cn) → left-side App registrations → click your application.
  2. Left menu Endpoints.
  3. 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, not chinacloudapi.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)

jsonc
{
  "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)

jsonc
{
  "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:

bash
# --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 tsv

Okta / 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:

bash
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 / suspicionHow to check / fix
Token is v1 / China cloud, issuer doesn't matchFill 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 matchLook 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 keysIs 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 401name_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_listThe username extracted by the id_token's username_expr has no same-name enabled user in the console user table.
Token expiredDecode the token to read exp; if expired, re-fetch the token.
SSO configured but the token seems not to go through SSOConfirm 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