Single sign-on with OIDC
Two different things share this name: Qorionix as an identity provider for your applications, and Qorionix as a relying party on yours. Both work now. Endpoints and errors for each.
OpenID Connect is the modern standard for one system vouching for a person to another. It is a thin layer on OAuth 2.0, and it is what most software written in the last decade expects.
Two completely different capabilities share the name in this product, and readers conflate them constantly, so they are separated here before anything else. Decide which one you want before reading further, because the configuration, the permissions and the state of completion are all different.
- Qorionix as an OIDC provider (an identity provider)
- Your other applications let people sign in using Qorionix as the source of identity. You register the application here, it redirects your users to Qorionix, and Qorionix hands it back a signed token saying who they are. This half is finished and verified end to end.
- Qorionix as a relying party (inbound federation)
- Your people sign in to Qorionix using an external OIDC provider — Entra ID, Okta, Google Workspace, or an on-premises AD FS over WS-Federation. You register your provider here and Qorionix sends users to it and receives them back. This half is finished too, and it is a different screen and a different code path from the one this article used to describe.
| Console page | What it is | Use it when |
|---|---|---|
| Identity → OIDC clients | Qorionix as the identity provider. You register your application as a client of us. | An internal tool should let people sign in with their Qorionix account. |
| Identity → Federated providers | Qorionix as a relying party, the finished path. Vendor presets for Entra ID, Google Workspace, Okta and AD FS. | Your staff should sign in to Qorionix with the account they already have. This is the one. |
| Identity → SSO Providers | The older generic provider record. This is the surface the warnings below are about. | Nothing new. It is documented here so you recognise it if you already have records in it. |
Qorionix as an OIDC provider
This is the direction that works. An application — an internal tool, a wiki, a dashboard, anything that speaks OIDC — is registered as a client, and from then on it can send people to Qorionix to authenticate and receive a signed token back. Only the authorization code flow is supported, which is the correct one; the implicit flow and the password grant are not offered and should not be missed.
The endpoints
| Endpoint | Method | What authenticates the call |
|---|---|---|
/oidc/.well-known/openid-configuration | GET | Nothing. This is the discovery document; point your application's OIDC library at it and it will find everything else. |
/oidc/jwks | GET | Nothing. The public keys your application uses to verify a token we signed. |
/oidc/authorize | GET | A Qorionix session. It issues a code on behalf of the signed-in person, so there has to be one. |
/oidc/token | POST | The client, not a user — Authorization: Basic base64(client_id:client_secret) or the same credentials in the form body. The secret is verified with bcrypt, the code must be unused and unexpired, redirect_uri is matched exactly, and PKCE is enforced. |
/oidc/userinfo | GET | The RS256 access token this provider issued. Not a Qorionix session token; those are a different signature family and are refused here. |
/oidc/register | POST | An organisation-scoped Qorionix session. This is the RFC 7591 machine endpoint. |
The console's guided equivalents of registration live under your organisation and are gated on the sso permission family, which is the right level — registering a relying party is establishing a federation trust, not editing a preference.
GET /api/v1/organizations/{orgID}/oidc/clients sso:read
POST /api/v1/organizations/{orgID}/oidc/clients sso:write
DELETE /api/v1/organizations/{orgID}/oidc/clients/{clientID} sso:delete
POST /api/v1/organizations/{orgID}/oidc/clients/{clientID}/secret sso:writeWhat the discovery document advertises
You should not need to type any of this — a competent OIDC library reads it — but it is here because it tells you what your application must be configured to do.
scopes_supported openid, profile, email, offline_access
response_types_supported code
grant_types_supported authorization_code, refresh_token
id_token_signing_alg_values_supported RS256
code_challenge_methods_supported S256, plain
token_endpoint_auth_methods_supported client_secret_basic, client_secret_post, none
subject_types_supported publicTwo honest notes on that list. plain appears as a PKCE method because the specification permits it; use S256 — a plain challenge is equal to its own verifier and protects nothing if the redirect is observed. And there is no end_session_endpoint: registration accepts post_logout_redirect_uris and stores them, but there is no single-logout endpoint, so signing out of your application does not sign the person out of Qorionix or the other way round.
Registering an application
Send the registration
client_nameand at least one entry inredirect_urisare required; everything else has a default.grant_typesdefaults to authorization code plus refresh token,response_typesto code,scopetoopenid profile email, andtoken_endpoint_auth_methodtoclient_secret_basic. PKCE is required on every client we create, whether or not the application asked for it.Save the secret immediately
The response carries
client_secretin plaintext and that is the only time it exists in a readable form. It is stored as a bcrypt hash. If you lose it, rotate it on the.../secretendpoint; there is no way to recover the original.Do not look for a certificate field
None of these endpoints accepts certificate material. An OIDC client is identified by a
client_idplus either a secret or PKCE. A form that demands a certificate is asking a SAML question of an OIDC object — if your application insists on one, it wants SAML, not this.Set the redirect URI exactly
Redirect URIs are compared by exact string match at both the authorize and the token step, per OAuth 2.1. A trailing slash, a different port, or
httpwhere the registration sayshttpsall fail. This is the single most common cause of a first-attempt failure.
Tokens and lifetimes
| Token | Lifetime | Notes |
|---|---|---|
| Authorization code | 60 seconds | Single use. Bound to the client, the redirect URI and the PKCE challenge. |
| Access token | 15 minutes | An RS256 JWT carrying iss, sub, aud, iat, exp, scope and org. Present it to /oidc/userinfo. |
| ID token | 15 minutes | RS256, and the same claims plus email, email_verified, name and your nonce if you sent one. This is the one your application reads to learn who signed in. |
| Refresh token | 30 days | An opaque random string, stored only as a SHA-256 hash. Rotated on every use — the presented token is revoked when it is exchanged. |
Refresh rotation is worth understanding because it changes how a leaked token behaves. Each refresh revokes the whole lineage of tokens it descends from and issues a fresh pair. If a stolen refresh token is used after the legitimate application has already rotated it, the presentation is treated as a replay and the entire family is burned — every token descended from that original grant stops working at once. The user is signed out and has to authenticate again. That is the intended outcome: a noisy failure beats an attacker quietly holding a valid credential for thirty days.
The exact error responses
All of these are JSON with an error and an error_description, per OAuth 2.0. The error value is what your library will branch on.
invalid_client unknown client_id, or the client secret did not verify
invalid_grant the code was already used, expired, issued to a different
client, presented with a different redirect_uri, or failed
the PKCE check; or the refresh token is revoked, expired,
belongs to another client, or was replayed
unsupported_grant_type only authorization_code and refresh_token are supported
invalid_request anything else, including an unparseable form bodyinvalid_request client_id, redirect_uri, and response_type=code are required
invalid_client unknown client
invalid_request redirect_uri mismatch
login_required no authenticated session
server_error <the underlying error>401 invalid_token bearer token required
500 server_error <the underlying error>
401 invalid_token org-scoped session required
400 invalid_request malformed jsonOne historical note, because it explains a symptom you may find in an old support thread. The token endpoint used to sit behind the platform's ordinary session middleware, which expects a Qorionix session bearer rather than client credentials, so every exchange was answered with 401 invalid authorization format and no OIDC client could ever complete a grant. Separately, a bug in how client and token identifiers were generated meant registration failed before writing a row and the code grant failed after consuming the code. Both are fixed and the full flow has been verified against a running server. If you are following instructions written before that, discard them.
Qorionix as a relying party: the working path
The other direction. Your people are sent to your identity provider to authenticate rather than typing a Qorionix password. Set it up under Identity → Federated providers, whose page is headed Federated identity providers.
Press Add provider and pick the product, not the protocol
The first wizard step asks
Which identity provider are you connecting?and offers vendor cards rather than a protocol choice, because Qorionix already knows each product's endpoints, scopes and quirks. The next screen then only asks for what your tenant actually has to supply.Paste the discovery document URL and press Fetch configuration
The result is shown back to you under
This is what the provider published, with the issuer, authorization endpoint, token endpoint, userinfo endpoint, JWKS URI, the scopes offered and the PKCE methods. Read it. This is the document the trust is built from, and it is fetched once, at configure time — there is no background refresh, so a provider that later moves an endpoint has to be reconfigured here.Supply the client id and secret
The secret is stored encrypted and never shown again; afterwards only a short hint is displayed. A deployment with no encryption key configured refuses to store it at all and answers
503rather than keeping it in the clear.Copy the redirect URI into your identity provider
The wizard shows it in a panel titled
Configure this in your identity provider, with a Copy control. It has to match character for character, and it is the same URI for every tenant and every provider on the platform.Decide the account-linking rules on the last step
Just-in-time provisioning,Role for accounts created this way,Link by email address,Require a verified email address, andProvider is enabled. These fields are real on this surface — they were inert on the older one described below.
Redirect URI (OIDC) <your Qorionix URL>/api/v1/federation/callback
Reply URL (WS-Fed) <your Qorionix URL>/api/v1/federation/wsfed/{providerID}/callback
Sign-in URL <your Qorionix URL>/api/v1/federation/{orgID}/{slug}/startWhat the callback verifies
- PKCE with S256, always. It is sent on every authorization request and verified on every exchange.
plainis deliberately not implemented, because a plain challenge equals its own verifier and protects nothing. - State is server-side and single-use. Replay, expiry and an unknown value all produce the same message, deliberately, because distinguishing them tells an attacker which of the three they hit.
- Nonce is bound to the request and checked against the
id_token. - The
id_tokenis verified against the provider's JWKS, with the key selected bykid, the issuer pinned to the discovery document, and the authorized party checked against your client id. The key set is cached for ten minutes. - Responses are size-capped and time-bounded — a one-megabyte ceiling on a provider document and a ten-second timeout — so a slow or hostile provider cannot hold a request open.
federation: authorization state is unknown, expired or already used
federation: id_token nonce does not match the authorization request
federation: could not read the provider's OpenID configuration
: no issuer or discovery URL supplied
: the document is missing issuer, authorization_endpoint or token_endpoint
federation: the provider refused the authorization code
: token endpoint refused: <detail>
: the token endpoint answered HTTP <n>
: HTTP <n>, and the body is neither JSON nor form-encoded
: the response carried neither an access token nor an id_token
federation: id_token validation failed
: no key "<kid>" in the provider's JWKS
: the JWKS contains no usable signing key
: the provider has no jwks_uri, so there is no key to verify against
: the provider has no issuer, so there is nothing to pin the token to
: authorized party "<azp>" is not this client
federation: could not read the user profile from the provider
: the provider has no userinfo endpointThere is deliberately no redirect-URI error in that list. We send our own fixed callback, so a mismatch is your provider's refusal and carries your provider's wording.
The older provider surface, and why not to use it
Identity → SSO Providers is a different, earlier record type. It is the one this article used to describe, and it is where POST /auth/sso/oidc/{providerID}/start reads from. If you already have records there, the table below is what they hold and what to watch for.
| Field | Meaning |
|---|---|
provider_type | Must be exactly oidc, lower case, for the OIDC start path to accept it. saml records are handled by a separate surface — see Single sign-on with SAML. |
issuer_url | Your provider's issuer. The authorize URL is built by appending /authorize to it, not by reading your provider's discovery document. A trailing /.well-known/openid-configuration is stripped first. If your provider's authorize endpoint is not at issuer plus /authorize — Entra ID's is not — the redirect this produces will not work. |
client_id / client_secret | The credentials your provider issued for Qorionix. The secret is never returned by any read path. |
allowed_domains | A single string, not a list. This is what home-realm discovery matches an email domain against. |
is_enabled | A disabled provider answers 403 on start rather than being hidden. |
auto_provision_users / default_role | Stored on this surface, and inert. The code that acts on them lives on the federation surface above. |
The endpoints and their errors
POST /api/v1/auth/discovery takes email or domain
POST /api/v1/auth/sso/oidc/{providerID}/start takes redirect_uri, state, login_hint
400 invalid request body
400 email or domain is required
500 failed to discover auth provider
400 providerID is required
400 redirect_uri is required
400 provider is not oidc
400 oidc provider requires issuer_url and client_id
404 SSO provider not found
403 SSO provider is not enabled/auth/discovery answers with federated and the provider to use when the domain matches an enabled provider, and with local when it does not. Starting a login there writes an audit row with the action auth.oidc_login_started. Do not build on that endpoint. It matches on a free-text domain list with no ownership check at all, and it has a disclosure problem serious enough to have its own section in The identifier-first sign-in flow. Use GET /api/v1/federation/discover instead.
Provider administration on this older surface is served under two path spellings — /organizations/{orgID}/sso-providers and /organizations/{orgID}/sso/providers — because the second one used to return 404 and the console's SSO page showed an empty list rather than an error. Both are the same handlers behind the same sso permissions. There are also three routes at /sso-providers/{providerID} for reading, updating and deleting a single provider that carry no permission check and no organisation in the path, so we cannot currently promise that only an administrator can alter a federation trust through them. That is on the list to fix and is stated here rather than hidden, in line with what we publish at /honesty.
What to do next
- To let your staff sign in with your existing identity provider: Identity → Federated providers, then route their email domain with Federated domains and domain verification.
- To let an application sign in with Qorionix: register a client, point the application's library at
/api/v1/oidc/.well-known/openid-configuration, and useS256PKCE. - To reduce the number of passwords your staff hold: read Apps without SSO support for the applications that will never speak either standard.
- To get joiners and leavers right as well as sign-in: read SCIM inbound provisioning. Federation authenticates people; provisioning is what removes them.
- If you were considering Google or GitHub sign-in for business accounts, read Social and external login first — the answer there is shorter than you expect.