Your web application or browser extension can sign end users in through your sign-in gate as an OpenID Connect client. It sends the end user to the hosted sign-in page and gets back an access token that is an ordinary session token: your backend verifies it with verifyToken(), and the Public API accepts it, as it accepts the token of a sign-in on your own pages.
Use it when your application needs a session token but you do not want to build sign-in pages. A hosted sign-in that only sends the end user back to your app hands your application no token (see Where each flow ends); a sign-in your application starts through OpenID Connect does. Google and GitHub sign-in stay available, on the hosted sign-in page.
Turn on the provider
The provider is served on one brand domain of your gate, which must be verified first; see Gates and brand domains. In the operator console, under Settings, Project, Sign-in clients, choose the brand domain and Turn on. Only owners and admins can.
The page then shows the provider's Issuer, https://<brand domain>/oidc, and its Discovery URL. The same is PUT /mgt/v1/identities/{identityId}/oidc-provider with { "host": "<brand domain>" } in the Management API; a host that is not a verified brand domain of the gate is refused with 400 OIDC_ISSUER_INVALID. Turning the provider off keeps the registered clients, which work again once it is on.
Register your application
On the same page, Create client registers one application. The same is POST /mgt/v1/identities/{identityId}/relying-parties:
{
"name": "Web console",
"client_type": "public",
"redirect_uris": ["https://console.example.com/auth/callback", "https://console.example.com/auth/silent"],
"post_logout_redirect_uris": ["https://console.example.com/signed-out"]
}| Field | What to set |
|---|---|
name | 1 to 100 characters, shown in the console |
client_type | public for a browser application or an extension: no secret, the code is protected by PKCE. confidential for a server that keeps a secret |
redirect_uris | 1 to 10 callback addresses |
post_logout_redirect_uris | Optional, 0 to 10 addresses that sign-out may return to |
Each address must be absolute, use https (plain http only on localhost, 127.0.0.1 or [::1]), carry no fragment, wildcard or user name, and be at most 512 characters. The provider compares them character for character, so register exactly what your application sends. Anything else is refused with 400 VALIDATION_ERROR.
The answer carries the client_id. A confidential client also gets its client_secret, shown once; a public client's is null.
The endpoints
All of them are under the issuer, https://<brand domain>/oidc, and listed in its discovery document:
| Endpoint | Path |
|---|---|
| Discovery | /oidc/.well-known/openid-configuration |
| Authorization | /oidc/auth |
| Token | /oidc/token |
| Keys | /oidc/jwks |
| Sign-out | /oidc/session/end |
The provider offers the authorization code flow only. There is no refresh token, no userinfo endpoint and no dynamic client registration.
Send the end user to sign in
Redirect the browser to the authorization endpoint with these parameters:
| Parameter | Value |
|---|---|
response_type | code |
client_id | Your client_id |
redirect_uri | One of the registered addresses, exactly |
scope | openid public; add email for the email claims in the ID token |
code_challenge | The base64url SHA-256 of a random code_verifier that you keep |
code_challenge_method | S256, the only method |
nonce | A random value; required |
state | A random value that you check on the callback |
prompt | Optional: none renews without showing a page; see Renew the token |
Ask for the public scope
public in scope is what makes the access token a session token. A token issued without it is refused by verifyToken() with audience_mismatch, and by the Public API with 401.
The end user signs in on the hosted page, and the browser comes back to redirect_uri with code and your state. A request without nonce comes back with error=invalid_request and no code. An unknown client or an unregistered redirect_uri stops on the provider's error page and never reaches your application.
Exchange the code
Send the code to the token endpoint as a form:
curl -X POST https://auth.example.com/oidc/token \
-d grant_type=authorization_code -d client_id=<client_id> -d code=<code> \
-d code_verifier=<code_verifier> -d redirect_uri=https://console.example.com/auth/callbackA confidential client authenticates with HTTP Basic, its client_id and client_secret, instead of sending client_id. A code is valid for 60 seconds and can be used once. From a browser, the token endpoint accepts a public client's request only from the origin of one of its registered redirect addresses.
| Field of the answer | Value |
|---|---|
access_token | The session token for your backend and the Public API |
token_type | Bearer |
expires_in | 900, that is 15 minutes |
scope | public |
id_token | The ID token, for your application to read who signed in |
Use the access token
Send the access token to your backend as Authorization: Bearer <access_token>. verifyToken() accepts it like any other session token, and its claims name the account, the buyer organization and your application; see How your backend knows who is calling. The Public API accepts it too, so the React components can use it as their getAccessToken().
Do not send the ID token to your backend. Its audience is your client_id, and verifyToken() refuses it with audience_mismatch.
Renew the token
There is no refresh token. Renew by sending the authorization request again with prompt=none. While the end user's sign-in on the provider is still valid, for at most 7 days, the browser comes back to your redirect_uri with a new code and no page is shown. When there is no sign-in to reuse, it comes back with error=login_required and no code; then send the end user through the normal request.
- A web application can load the
prompt=nonerequest in a hidden iframe whose callback, such as/auth/silentabove, is registered too and passes the callback address back to your page. The provider's cookie isSameSite=Lax, so the iframe carries it only when your application and the brand domain are the same site, for exampleconsole.example.comandauth.example.com. Otherwise, renew with a full-page redirect. - A browser extension renews as in the next section.
A browser extension
Use chrome.identity.launchWebAuthFlow with the extension's redirect address, https://<extension ID>.chromiumapp.org/, which chrome.identity.getRedirectURL() returns. Register it like any other https address. A chrome-extension:// address is refused, because redirect addresses must use https.
The token endpoint refuses requests sent from the extension itself: they come from the origin chrome-extension://<extension ID>, which is not the origin of a registered redirect address. Let your backend exchange the code instead. The extension passes it, with its code verifier, to an endpoint of yours:
const base64url = (bytes: Uint8Array) =>
btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
export async function signIn(interactive: boolean): Promise<{ access_token: string; expires_in: number }> {
const redirectUri = chrome.identity.getRedirectURL(); // https://<extension ID>.chromiumapp.org/
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
const state = crypto.randomUUID();
const params = new URLSearchParams({
client_id: CLIENT_ID,
response_type: 'code',
redirect_uri: redirectUri,
scope: 'openid public',
code_challenge: challenge,
code_challenge_method: 'S256',
state,
nonce: crypto.randomUUID(),
});
if (!interactive) params.set('prompt', 'none');
const returned = new URL(await chrome.identity.launchWebAuthFlow({ url: `${ISSUER}/auth?${params}`, interactive }));
if (returned.searchParams.get('state') !== state) throw new Error('state mismatch');
const error = returned.searchParams.get('error');
if (error) throw new Error(error); // login_required: no sign-in to reuse; call signIn(true)
// The token endpoint refuses requests from the extension's own origin: your backend exchanges the code.
const response = await fetch(`${API}/auth/extension/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: returned.searchParams.get('code'), code_verifier: verifier }),
});
if (!response.ok) throw new Error(`sign-in failed: ${response.status}`);
return response.json();
}Your backend exchanges the code for the registered extension address only, and verifies the token before it hands it back:
const ISSUER = process.env.SHIPREV_OIDC_ISSUER!; // https://auth.example.com/oidc
const CLIENT_ID = process.env.SHIPREV_OIDC_CLIENT_ID!;
const EXTENSION_REDIRECT_URI = process.env.EXTENSION_REDIRECT_URI!; // https://<extension ID>.chromiumapp.org/
export async function exchangeExtensionCode(code: string, codeVerifier: string) {
const response = await fetch(`${ISSUER}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
code,
code_verifier: codeVerifier,
redirect_uri: EXTENSION_REDIRECT_URI,
}),
});
if (!response.ok) throw new Error(`token request failed: ${response.status}`);
const { access_token, expires_in } = (await response.json()) as { access_token: string; expires_in: number };
await shiprev.verifyToken(access_token); // throws unless it is a session token of your application
return { access_token, expires_in };
}To renew, call signIn(false) before the token expires. It returns a new token without opening a window while the end user's sign-in on the provider is still valid and the browser presents it to the authorization window, and throws login_required otherwise; then call signIn(true), which shows the hosted sign-in page.
Sign out
Send the browser to the sign-out endpoint with id_token_hint, the ID token, and a registered post_logout_redirect_uri. It ends the end user's sign-in on the provider, so the next prompt=none request comes back with login_required. Access tokens already issued keep verifying until they expire, at most 15 minutes later.
Next
- Sessions and tokens: what the access token carries and why a token is refused.
- Hosted pages, components or your own: the other ways to sign end users in.