Skip to main content

OpenID Connect Sign-In

The REST Gateway can let a user sign in through your OpenID Connect (OIDC) provider — Keycloak, or any provider that implements the authorization code flow with PKCE. The user authenticates at the provider; the Gateway validates the provider's ID token, looks the user up in a mapping you maintain, and opens a Gateway session acting as an enrolled key-server principal.

The Gateway serves its own browser login page for this at /oidc/. Applications can also drive the flow themselves; see OpenID Connect Sign-In for applications. For the provider-side setup, see Configuring Keycloak.

What it is, and what it is not​

  • The key server keeps authorizing. A signed-in user acts as an Externally Authenticated (XAuth) principal on the key server, through the Gateway's delegation layer. Every operation is checked against that principal's roles and ACLs by the key server, exactly as for any other session. A denied operation is the key server's own 403 ACL_DENIED.
  • No user is created at login. The Gateway signs in only an identity that an enabled mapping names, onto an XAuth principal that already exists. An unmapped identity is refused. You create the principals and the mappings.
  • The provider proves who the user is; it grants nothing. Group or role claims in the ID token are not read.
  • No session cookie. The session is a bearer credential bound to a key the user's browser made (DPoP). The only cookie is a short-lived transaction cookie on the two login steps that run in the sign-in window. It has no API authority and is cleared when the login completes.
  • Refresh tokens are not used. When a session reaches its end the user signs in again.

Before you start​

You need:

  1. The delegation layer. The Gateway mints OIDC sessions by delegation, under its own certificate identity. The tier provisioners enable this by default. Without it every sign-in is refused at the last step with 503 DELEGATION_NOT_CONFIGURED. The Gateway's certificate principal must carry every operation the mapped principals hold. Otherwise the key server refuses the delegation with 403 DELEGATOR_TOO_NARROW.
  2. An XAuth principal for every user you map. Create them in the Console or through POST /api/v1/principals/xauth. Each mapping's xauthName names one.
  3. The Gateway's public origin: the exact https://host[:port] users reach the Gateway at, for example https://gateway.example.test. Every callback URI must sit under it, and the sign-in proof is bound to it.
  4. HTTPS in front of the Gateway. The Gateway listens on plain HTTP on the loopback interface. Put a TLS-terminating proxy in front of it and serve it at the public origin. Every issuer, callback URI and application origin in the configuration must be https://. There is no exception, not even for testing.
  5. A client registration at your provider, with the redirect URI <public origin>/api/v1/auth/oidc/callback/<providerId>. Configuring Keycloak shows one.

The configuration file​

OIDC sign-in is turned on by one JSON file, named by TE_GATEWAY_OIDC_CONFIG, together with TE_GATEWAY_PUBLIC_ORIGIN. If TE_GATEWAY_OIDC_CONFIG is unset, OIDC is off: the five OIDC routes answer 503 OIDC_NOT_CONFIGURED and the login page is not served. If it is set without a valid TE_GATEWAY_PUBLIC_ORIGIN, the Gateway refuses to start.

The file has four members: providers, bindings, mappings and limits. The Gateway validates it whole, at start and on every reload. A refusal names the JSON path of the fault, such as $.providers[0].issuer. Two rules apply everywhere:

  • An unknown member is refused. A misspelled key is an error, not a silently ignored setting. The one exception is _comment, accepted at any level for your notes.
  • Secrets are files. A client secret is named by clientSecretFile and a signing key by clientKeyFile: absolute paths, read when used. A clientSecret, clientKey or privateKey member is refused by name, so no secret ever sits in this document.

A complete, commented example ships with the Console tier at /opt/pisoft/tricryption/console-gateway/examples/gateway-oidc.example.json on Linux and C:\Program Files\PiSoft\Tricryption\console-gateway\examples\gateway-oidc.example.json on Windows:

{
"providers": [
{
"providerId": "keycloak",
"issuer": "https://idp.example.test/realms/te",
"clientId": "te-gateway",
"clientAuth": "client_secret_basic",
"clientSecretFile": "/var/lib/pisoft/console-gateway/secrets/oidc-keycloak-client-secret",
"allowedAlgs": ["RS256", "ES256"],
"callbackUri": "https://gateway.example.test/api/v1/auth/oidc/callback/keycloak",
"endpointHosts": ["idp.example.test"],
"caFile": "/var/lib/pisoft/console-gateway/secrets/oidc-keycloak-ca.pem"
}
],
"bindings": [
{
"bindingId": "console",
"providerId": "keycloak",
"applicationOrigin": "https://gateway.example.test",
"ksSystemId": 1787189380,
"revision": 1,
"enabled": true
}
],
"mappings": [
{
"bindingId": "console",
"issuer": "https://idp.example.test/realms/te",
"subject": "0b6f3c1e-0000-4000-8000-000000000001",
"xauthName": "alice",
"enabled": true,
"revision": 1
}
]
}

providers[] — the OpenID providers this Gateway is a client of​

MemberTypeDefaultRule
providerIdstringrequireda-z, 0-9 and -, starting with a letter or digit, at most 63 characters; unique. It is part of the callback path.
issuerhttps:// URLrequiredThe provider's issuer, exactly as its discovery document states it. No query, fragment or credentials.
clientIdstringrequiredAt most 255 characters.
clientAuthclient_secret_basic or private_key_jwtrequiredHow the Gateway authenticates to the token endpoint.
clientSecretFileabsolute path—Required with client_secret_basic, refused with private_key_jwt. The file holds the client secret.
clientKeyFileabsolute path—Required with private_key_jwt, refused with client_secret_basic. The client's signing key.
clientKeyIdstring—Required with private_key_jwt: the kid the provider knows that key by.
allowedAlgsarrayrequiredA non-empty subset of RS256 and ES256, no duplicates. none and HS* are never accepted.
callbackUrihttps:// URLrequiredExactly <public origin>/api/v1/auth/oidc/callback/<providerId>. One callback per provider is what lets the Gateway tell which provider answered.
endpointHostsarray of host[:port]requiredEvery endpoint the provider's discovery document names must be on one of these hosts. The issuer's host is not implied: list it.
caFileabsolute pathsystem storeA PEM bundle the provider's TLS certificate is verified against.
scopesarray["openid"]Scope tokens to request; must include openid.
maxAgeinteger, 1–86400noneIf set, requested as max_age and enforced on the ID token's auth_time.
clockToleranceSecondsinteger, 0–6030Allowed clock skew when checking the ID token's times.
jwksCacheSecondsinteger, 10–86400300How long the provider's signing keys are cached.
jwksRefreshMinSecondsinteger, 1–360030The least time between two key refreshes. A token signed with an unknown key inside this window is refused without a fetch.

bindings[] — which application may sign in through which provider​

A binding ties one browser origin to one provider and to the key server this Gateway fronts. It also sets the session's bounds.

MemberTypeDefaultRule
bindingIdstringrequiredSame form as providerId; unique. The login link names it.
providerIdstringrequiredMust name a provider in this file.
applicationOriginhttps:// originrequiredThe exact origin (https://host[:port], no path) the login is started from. For the Gateway's own login page this is the public origin.
ksSystemIdinteger, 1–4294967295requiredMust equal the system id of the key server this Gateway fronts. Otherwise the file is refused.
revisioninteger, 1 or morerequiredMove it forward on every change to the binding.
enabledbooleanrequiredA disabled binding refuses new logins and ends the sessions it issued.
sessionMaxSecondsinteger, 10–86400900The absolute life of a session.
sessionIdleSecondsinteger, 5–86400300A session unused this long ends. Must not exceed sessionMaxSeconds.
transactionSecondsinteger, 30–3600300How long a started login may take at the provider.
redemptionSecondsinteger, 5–60060How long, after the provider answers, the browser has to redeem the login.
nonceSecondsinteger, 5–30030The life of the one-time nonce the browser signs to redeem.
objectScopePolicynone, optional or requirednoneWhether a login may (or must) name the objects its session is limited to.

mappings[] — which identity becomes which principal​

MemberTypeDefaultRule
bindingIdstringrequiredMust name a binding in this file.
issuerhttps:// URLrequiredMust equal the binding's provider's issuer exactly.
subjectstringrequiredThe provider's sub claim for the user: exact and case-sensitive, at most 255 characters. For Keycloak it is the user's id, not the user name.
xauthNamestringrequiredThe XAuth principal the user acts as. At most 255 characters.
enabledbooleanrequiredA disabled mapping refuses the user and ends the sessions it issued.
revisioninteger, 1 or morerequiredMove it forward on every change to the mapping.

A (bindingId, issuer, subject) triple may appear only once.

limits — optional bounds​

MemberTypeDefault
pendingTransactionsMaxinteger, 1–100000256 logins in progress at once; beyond it a new login is refused with 503 LOGIN_CAPACITY_EXHAUSTED
redemptionsPerMinuteinteger, 1–100000120; beyond it 429 LOGIN_RATE_LIMITED
networkTimeoutMsinteger, 100–600005000 for every call to the provider

Changing the configuration​

The Gateway re-reads the file every TE_GATEWAY_OIDC_CONFIG_POLL_MS milliseconds (default 2000), and does nothing unless its contents changed. You change it by writing a new file; no restart and no signal are needed.

  • A file that fails validation is refused whole. The configuration already in force stays, and the Gateway logs oidc.config_reload_refused with the reason and the time the configuration in force was loaded.
  • A changed binding or mapping must move its revision forward. A change without a new revision, or a revision that moves backwards, is refused. The revision is what a session records, so a change it cannot see would end nothing.
  • What a reload ends. A binding that changed, was disabled or was removed ends its logins in progress and every live session it issued, at once (oidc.binding_ended in the log). A mapping that changed, was disabled or was removed ends every live session it issued (oidc.mapping_ended). Sessions under bindings and mappings you did not touch continue.

So to cut a user off at once, disable or remove their mapping. To stop all OIDC sign-ins through one application, disable its binding.

Provisioning it​

Both tier provisioners carry the file and the origin. They check that the file is a JSON object with providers, bindings and mappings, copy it into the tier's secrets directory (readable by the service account only), and write TE_GATEWAY_OIDC_CONFIG and TE_GATEWAY_PUBLIC_ORIGIN into the Gateway's environment. The Gateway itself validates everything else when it starts.

On Linux:

sudo tricryption-tier-provision --oidc-config gateway-oidc.json --public-origin https://gateway.example.test

The file is copied to /var/lib/pisoft/console-gateway/secrets/gateway-oidc.json.

On Windows, from an elevated prompt:

powershell.exe -NoProfile -ExecutionPolicy Bypass -File 'C:\Program Files\PiSoft\Tricryption\libexec\console-gateway\tricryption-tier-provision.ps1' -OidcConfig C:\secure\gateway-oidc.json -PublicOrigin https://gateway.example.test

The file is copied to C:\ProgramData\PiSoft\Tricryption\console-gateway\secrets\gateway-oidc.json.

The rules are the same on both:

  • --oidc-config (-OidcConfig) needs --public-origin (-PublicOrigin). The origin must be exactly https://host[:port], with no path and no trailing slash.
  • --public-origin may be given alone. It is also the origin DPoP proofs are checked against behind a TLS terminator.
  • Neither may be combined with --identity-out (-IdentityOut).
  • A later run without --oidc-config removes the copy and the two lines, which turns OIDC sign-in off.

To change the configuration of a running tier, run the provisioner again with the new file, or replace the copy in the secrets directory: the Gateway picks it up at its next poll.

The secret files the configuration names (clientSecretFile, clientKeyFile, caFile) are yours to place. Put them where the Gateway's service account can read them and nobody else can, for example beside the copy in the secrets directory.

Once OIDC is configured the Gateway serves its login page at:

https://gateway.example.test/oidc/?provider=<providerId>&binding=<bindingId>

For the example above: https://gateway.example.test/oidc/?provider=keycloak&binding=console. The binding's applicationOrigin must be the Gateway's public origin for this page to start a login. A link without both parameters shows PAGE_MISCONFIGURED. What users see on the page is described in OpenID Connect Sign-In for applications.

The refusals you will see​

The Gateway answers every OIDC refusal with one public code per class. The specific check that failed is written to the Gateway's log, never to the response: a response that said which check failed would tell an attacker how far a forgery got.

The five OIDC routes and the sign-in window:

Status and codeMeaning
401 LOGIN_EVIDENCE_REJECTEDThe provider's answer, the ID token, the login state, the transaction cookie, the redemption secret or the browser's key did not check out. The user starts again.
403 IDENTITY_NOT_MAPPEDThe provider authenticated the user, but no enabled mapping names that identity on this binding. Add or enable a mapping.
403 BINDING_NOT_AVAILABLEThe binding or its provider is unknown or disabled. Check the link.
403 ORIGIN_REJECTEDThe login was started from an origin other than the binding's applicationOrigin.
503 IDP_UNAVAILABLEDiscovery, the signing keys or the token endpoint could not be used, or the client credential file could not be read. Answered with Retry-After: 30.
503 LOGIN_CAPACITY_EXHAUSTEDpendingTransactionsMax logins are already in progress (Retry-After).
429 LOGIN_RATE_LIMITEDredemptionsPerMinute was spent (Retry-After).
400 OIDC_REQUEST_MALFORMEDThe request itself was not well formed.
503 OIDC_NOT_CONFIGUREDTE_GATEWAY_OIDC_CONFIG is not set.

The login page shows a refusal from the sign-in window in lower case: identity_not_mapped, login_evidence_rejected, binding_not_available, idp_unavailable and so on, and provider_denied when the provider itself refused the user.

At the last step, when the browser redeems the login for a session:

Status and codeMeaning
503 DELEGATION_NOT_CONFIGUREDThe Gateway has no delegator identity: the delegation layer is not enabled.
403 DELEGATOR_NOT_PERMITTEDThe key server did not accept the Gateway's certificate principal as a delegator.
403 DELEGATOR_TOO_NARROWThe mapped principal holds an operation the Gateway's principal does not.
404 TARGET_NOT_FOUNDThe mapping's xauthName is not an XAuth principal on the key server.
403 TARGET_DISABLEDThe mapped principal, or the Gateway's own principal, is disabled.
403 BINDING_REVOKEDThe binding changed or was disabled while the user was signing in.
503 SSO_CAPACITYThe Gateway's session store is full.

On a session after sign-in, a request past the binding's absolute bound is 401 SSO_SESSION_EXPIRED, and a session the Gateway no longer holds is 401 SESSION_NOT_FOUND.