Skip to main content

OpenID Connect Sign-In for Applications

When an operator has configured OpenID Connect sign-in, a user can sign in to the REST Gateway with their organization's identity provider. The Gateway validates the provider's answer itself and opens a session acting as the key-server principal the operator mapped the user to. Every operation on that session is authorized by the key server, as for any other session.

There are two ways to use it:

  • The Gateway's own login page, at /oidc/ on the Gateway. Nothing to build.
  • Your own page, driving the five routes yourself.

The login page​

The operator gives users a link of this form:

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

The page is plain HTML and one script, served by the Gateway from its own origin under a strict Content Security Policy. It talks to nothing but the Gateway. It holds no secret and sets no cookie.

What the user sees​

The page is always in one of five states (the data-state of its #te-oidc element):

StateThe user sees
idleSign in through your identity provider. and a Sign in button. This is also where the page returns after a sign-out, an ended session, or a sign-in window closed early.
at-providerContinue in the sign-in window. A separate window shows the identity provider's own login page, where the user signs in as usual.
mintingSigned in at the provider. Creating your session. This lasts a moment.
signed-inSigned in., the principal the user is acting as, when the session ends, and a Sign out button.
refusedThe sign-in was refused. and a code, such as identity_not_mapped. The code is the Gateway's public refusal class; the operator can look up the reason in the Gateway's log.

The sign-in window must be allowed to open: if the browser blocks it, the page shows POPUP_BLOCKED. A link without both provider and binding shows PAGE_MISCONFIGURED.

A reload or a closed tab signs you out​

The page keeps the session in its own memory only: not in local or session storage, not in IndexedDB, not in a cookie. Reloading the page, navigating away from it or closing the tab signs the user out of the page. They select Sign in again; if their identity provider still has them signed in, that takes a moment and no password.

The session is bound to a signing key that the page made in the browser and that cannot be exported. Every request on the session carries a proof signed with that key. When the page goes, the key goes with it, so the session can no longer be used by anyone. It does stay on the Gateway until its idle or absolute limit. To end it at once, select Sign out, which deletes it on the Gateway.

A session also ends by itself after the binding's absolute limit, or after it has been idle for the binding's idle limit. The page returns to idle with Your session reached its end.

Calling the API from the page: teOidc.fetch​

The page exposes one object, window.teOidc, to scripts running in its document:

MemberWhat it does
teOidc.fetch(path, init)Sends a request on the session. path is a path on the Gateway, beginning with /. init takes method, body (a string) and headers. It adds Authorization: Bearer and a fresh DPoP proof signed by the page's key over the method, the URL, the bearer and the exact body bytes. It returns the fetch Response.
teOidc.signOut()Deletes the session on the Gateway (DELETE /api/v1/session) and returns to idle.
teOidc.state()The current state, one of the five above.
teOidc.keyThe page's key pair while signed in, otherwise null. Its private key is non-extractable: exporting it is refused by the browser.

For example, to encrypt under a key the user may use:

const r = await teOidc.fetch('/api/v1/crypto/encrypt', {
method: 'POST',
body: JSON.stringify({ data: btoa('hello'), ttag: '<the key>' }),
});

A denied operation is answered by the key server, for example 403 ACL_DENIED, and the page shows the last call and its result.

Writing your own page​

A team that needs its own user interface drives the same five routes the Gateway's page does. They are open routes, each guarded by single-use evidence bound to one browser:

  1. POST /api/v1/auth/oidc/start. Make a non-extractable P-256 key with crypto.subtle.generateKey and a random redemption secret. Send providerId, bindingId, the key's public JWK (kty, crv, x, y) as signingJwk, and the SHA-256 of the secret, base64url, as redemptionChallenge. The answer carries flowId and launchUrl. The request must come from the binding's applicationOrigin (403 ORIGIN_REJECTED otherwise).
  2. GET /api/v1/auth/oidc/launch. Open launchUrl in a new window. Open the window inside the user's click, before any await, or the browser blocks it. The Gateway sets the one transaction cookie and redirects to the provider.
  3. GET /api/v1/auth/oidc/callback/{providerId}. The provider redirects the window here. The Gateway exchanges the code, validates the ID token and maps the user. Its completion page then posts {te: 'oidc', flowId, ok} to the opener, with error set to a public code when ok is false. Accept the message only when event.origin is your origin, event.source is the window you opened and flowId is yours.
  4. POST /api/v1/auth/oidc/challenge. Send flowId and the redemption secret; the answer is a one-time nonce.
  5. POST /api/v1/session/oidc. Send flowId, redemptionSecret and signingJwk. Add a TE-SSO-Mint header: an ES256 JWS signed with your key, header typ: te-sso-mint+jwt with the public JWK, and claims mth: 'oidc', org (your origin), htm: 'POST', nonce, flow (the flowId), iat, jti and bth (the SHA-256 of the exact body bytes you send). The answer carries the session bearer, the principal it acts as (actingAs) and expiresAt.

From then on, send every request with Authorization: Bearer <session> and a DPoP proof signed by the same key, as Integrating via the API describes. Sign out with DELETE /api/v1/session.

Keep the bearer and the key in memory. Do not put either in storage or in a cookie, and do not put the bearer, a proof, the secret, the nonce or the flowId into the page's text, its console or a URL you build.

note

If your page is served with Referrer-Policy: no-referrer, set referrerPolicy: 'strict-origin' on your own fetch calls. Under no-referrer a browser sends Origin: null on a same-origin POST, and /start refuses it with ORIGIN_REJECTED.