Skip to main content

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.

Written for whoever runs IT12 min readUpdated

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.
The three OIDC-shaped surfaces, so you configure the right one. Only the middle row is what most readers want.
Console pageWhat it isUse it when
Identity → OIDC clientsQorionix 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 providersQorionix 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 ProvidersThe 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

The OIDC provider surface. Every path is under /api/v1. The auth column is the credential the endpoint itself checks, which is not the same for all of them.
EndpointMethodWhat authenticates the call
/oidc/.well-known/openid-configurationGETNothing. This is the discovery document; point your application's OIDC library at it and it will find everything else.
/oidc/jwksGETNothing. The public keys your application uses to verify a token we signed.
/oidc/authorizeGETA Qorionix session. It issues a code on behalf of the signed-in person, so there has to be one.
/oidc/tokenPOSTThe 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/userinfoGETThe RS256 access token this provider issued. Not a Qorionix session token; those are a different signature family and are refused here.
/oidc/registerPOSTAn 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.

Console-side client administration
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:write

What 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.

The advertised capabilities
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               public

Two 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

  1. Send the registration

    client_name and at least one entry in redirect_uris are required; everything else has a default. grant_types defaults to authorization code plus refresh token, response_types to code, scope to openid profile email, and token_endpoint_auth_method to client_secret_basic. PKCE is required on every client we create, whether or not the application asked for it.

  2. Save the secret immediately

    The response carries client_secret in 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 .../secret endpoint; there is no way to recover the original.

  3. Do not look for a certificate field

    None of these endpoints accepts certificate material. An OIDC client is identified by a client_id plus 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.

  4. 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 http where the registration says https all fail. This is the single most common cause of a first-attempt failure.

Tokens and lifetimes

What each token is for and how long it lives. The lifetimes are not currently configurable per client.
TokenLifetimeNotes
Authorization code60 secondsSingle use. Bound to the client, the redirect URI and the PKCE challenge.
Access token15 minutesAn RS256 JWT carrying iss, sub, aud, iat, exp, scope and org. Present it to /oidc/userinfo.
ID token15 minutesRS256, 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 token30 daysAn 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.

Token endpoint — always HTTP 400 on failure
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 body
Authorize endpoint — returned on the redirect where one is known, otherwise HTTP 400
invalid_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>
Userinfo and registration
401  invalid_token     bearer token required
500  server_error      <the underlying error>
401  invalid_token     org-scoped session required
400  invalid_request   malformed json

One 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.

  1. 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.

  2. 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.

  3. 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 503 rather than keeping it in the clear.

  4. 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.

  5. 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, and Provider is enabled. These fields are real on this surface — they were inert on the older one described below.

The three URLs that matter, in the form the console shows them
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}/start

What the callback verifies

  • PKCE with S256, always. It is sent on every authorization request and verified on every exchange. plain is 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_token is verified against the provider's JWKS, with the key selected by kid, 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 callback failures, verbatim
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 endpoint

There 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.

Fields on an inbound SSO provider. Two of them behave differently from how the name reads, and both are called out.
FieldMeaning
provider_typeMust 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_urlYour 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_secretThe credentials your provider issued for Qorionix. The secret is never returned by any read path.
allowed_domainsA single string, not a list. This is what home-realm discovery matches an email domain against.
is_enabledA disabled provider answers 403 on start rather than being hidden.
auto_provision_users / default_roleStored on this surface, and inert. The code that acts on them lives on the federation surface above.

The endpoints and their errors

Public, rate-limited login surfaces
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

  1. 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.
  2. 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 use S256 PKCE.
  3. To reduce the number of passwords your staff hold: read Apps without SSO support for the applications that will never speak either standard.
  4. To get joiners and leavers right as well as sign-in: read SCIM inbound provisioning. Federation authenticates people; provisioning is what removes them.
  5. 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.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in identity and access