Create SSO Configuration

POST /v1/sso/config

Create a new SSO connection for the caller's active account. Once a matching config is enforced and (for custom IdPs) domain-verified, account members whose email addresses match the configured email_domain are forced through your IdP at login instead of using password auth.

Plan-gated: requires the account's sso entitlement (else 403). Permission-gated: requires the settings:write account permission — members without it get 403.

Each email_domain can have at most one SSO config per account. Adding a second one for the same domain returns 409.

Provider types usable at login: google, github, and oidc (custom OIDC — Okta, Microsoft Entra ID, Auth0, or any OIDC-compliant IdP). SAML is not supported: provider_type: "saml" is refused with 400 “SAML sign-in is not supported. Use OpenID Connect (oidc), Google or GitHub instead.” Use oidc.

After creation, run POST /v1/sso/config/{uuid}/test to sanity-check the config. For oidc providers you must also complete domain verification before the config applies at login (see below).

Request body

email_domain string Req
Domain to route via this SSO. Lowercased server-side. Users with email addresses ending in @<email_domain> hit this config. Must be unique within the account.
provider_type string Req
Exactly one of google, github, oidc. For google / github the OAuth endpoints are fixed (see below); oidc is a custom IdP that requires its own endpoint URLs. saml returns 400 (SAML is not supported).
client_id string Req
OAuth/OIDC client ID from your IdP.
client_secret string Req
OAuth/OIDC client secret, as issued by your IdP. Required on create: a missing or empty value is refused with 400 (detail: client_secret is required). Encrypted at rest; never returned by any GET endpoint. On update, sending a blank client_secret keeps the existing secret.
auth_url, token_url, userinfo_url string
Required for provider_type: "oidc". Ignored for google / github — the platform force-locks those to Google's / GitHub's canonical endpoints, overriding any values you send (a fake-IdP guard). Must be https and resolve to a public host; internal/private hosts (link-local, RFC1918, cluster-internal) are rejected with 400.
issuer string Req (oidc)
Required for provider_type: "oidc". The IdP's OIDC issuer identifier (e.g. https://acme.okta.com/oauth2/default). At login, ngris matches the iss claim of the provider's signed id_token against this value exactly. Ignored and cleared for google / github (they use platform-controlled endpoints — GitHub issues no id_token). Must be https and resolve to a public host. Tip: use POST /v1/sso/config/discover to fetch it from your provider's .well-known/openid-configuration.
jwks_url string Req (oidc)
Required for provider_type: "oidc". The provider's JWKS endpoint (e.g. https://acme.okta.com/oauth2/default/v1/keys). At login, ngris fetches these public keys to verify the id_token signature. Ignored and cleared for google / github. Must be https and resolve to a public host. Discover returns this as jwks_uri.
scopes string
Space-separated OAuth scopes. Defaults to openid profile email (user:email for github).
enforced boolean
When true, account members whose email domain matches are forced through SSO — their password login is rejected. Requires the account's plan to include the sso entitlement or the request is refused with 403. Default false (opt-in SSO).

How custom OIDC identity is verified at login

For oidc, ngris does not trust the identity returned by the user-info body. At login it cryptographically verifies the provider's signed id_token before creating a session: the signature is checked against the keys at jwks_url (asymmetric algorithms only), and the token's iss (must equal issuer), aud (must contain your client_id), exp (not expired) and the login nonce are all validated. That is why issuer and jwks_url are required for oidc — a config without them cannot be used at login, so the API rejects it at save. google / github need neither (their identity comes from platform-controlled endpoints; GitHub issues no id_token).

Auto-filling endpoints (Discover)

Rather than looking up each URL by hand, call POST /v1/sso/config/discover with your provider's issuer URL. It reads the IdP's .well-known/openid-configuration and returns authorization_endpoint, token_endpoint, userinfo_endpoint, issuer, and jwks_uri — which map onto auth_url, token_url, userinfo_url, issuer, and jwks_url. The dashboard's Discover button does this automatically, so an admin normally pastes one issuer URL and the rest is filled in.

Domain verification (oidc)

Before a custom oidc config applies at login, the account must prove it owns email_domain. On create (and whenever email_domain changes) the config is returned with domain_verified: false plus a DNS-TXT challenge in domain_verify_record_name / domain_verify_record_value. Publish that TXT record, then call POST /v1/sso/config/{uuid}/verify-domain — on an exact match domain_verified flips to true.

google / github configs are auto-verified (identity is asserted by the provider and the endpoints are locked), so they carry no TXT challenge and apply immediately.

Response (200 OK)

The newly-created SSO config (secret never included). Fields: uuid, account_uuid, email_domain, provider_type, client_id, auth_url, token_url, userinfo_url, issuer (oidc), jwks_url (oidc), scopes, status (active | error), enforced, domain_verified, domain_verify_record_name (omitted once verified / for google+github), domain_verify_record_value (same), last_error_message, last_tested_at, created_at, updated_at.

Deprecated: until the sunset this response also carries the legacy integer id and *_id fields. Don't use them; read uuid and the *_uuid fields.

Errors

400 Bad Request
Missing required field (for example an empty client_secret), invalid provider_type (including saml: “SAML sign-in is not supported. Use OpenID Connect (oidc), Google or GitHub instead.”), missing auth_url/token_url/userinfo_url for oidc, missing issuer/jwks_url for oidc, or a non-https / internal endpoint URL.
401 Unauthorized
Missing auth.
403 Forbidden
No active account, caller lacks settings:write, the account's plan doesn't include SSO, or enforced: true was sent for a plan without the SSO entitlement.
409 Conflict
An SSO config for this email_domain already exists in the account.
Request — Custom OIDC
curl -X POST "https://api.ngris.com/v1/sso/config" \ -H "X-API-KEY: <your_api_key>" \ -H "Content-Type: application/json" \ -d '{ "email_domain": "acme.com", "provider_type": "oidc", "client_id": "0oa1234abc", "client_secret": "your-idp-client-secret", "auth_url": "https://acme.okta.com/oauth2/default/v1/authorize", "token_url": "https://acme.okta.com/oauth2/default/v1/token", "userinfo_url": "https://acme.okta.com/oauth2/default/v1/userinfo", "issuer": "https://acme.okta.com/oauth2/default", "jwks_url": "https://acme.okta.com/oauth2/default/v1/keys", "scopes": "openid profile email", "enforced": true }'
Python
from ngris import Ngris client = Ngris(api_key="<your_api_key>") sso = client.sso.create({ "email_domain": "acme.com", "provider_type": "oidc", "client_id": "0oa1234abc", "client_secret": "your-idp-client-secret", "auth_url": "https://acme.okta.com/oauth2/default/v1/authorize", "token_url": "https://acme.okta.com/oauth2/default/v1/token", "userinfo_url": "https://acme.okta.com/oauth2/default/v1/userinfo", "issuer": "https://acme.okta.com/oauth2/default", "jwks_url": "https://acme.okta.com/oauth2/default/v1/keys", "enforced": True, }) # oidc: publish the returned TXT record, then verify: client._post_raw(f"/sso/config/{sso.uuid}/verify-domain", {})
Response — 200 OK
{ "uuid": "019af662-7814-75fe-ac3c-b767d84325a9", "account_uuid": "019a68b9-1a79-7820-a884-41f554aba6bd", "email_domain": "acme.com", "provider_type": "oidc", "client_id": "0oa1234abc", "auth_url": "https://acme.okta.com/oauth2/default/v1/authorize", "token_url": "https://acme.okta.com/oauth2/default/v1/token", "userinfo_url": "https://acme.okta.com/oauth2/default/v1/userinfo", "issuer": "https://acme.okta.com/oauth2/default", "jwks_url": "https://acme.okta.com/oauth2/default/v1/keys", "scopes": "openid profile email", "status": "active", "enforced": true, "domain_verified": false, "domain_verify_record_name": "_ngris-sso-verify.acme.com", "domain_verify_record_value": "ngris-sso-verify=3f9c...a1", "last_error_message": "", "last_tested_at": null, "created_at": "2026-07-02T10:00:00Z", "updated_at": "2026-07-02T10:00:00Z" }
Was this helpful?
Was this helpful?