Sign in with Ubikap
This recipe uses openid-client v6 for Node.js. Each step also gives the raw HTTP exchange, for any other library.
npm install openid-client1. Discover the provider
Section titled “1. Discover the provider”import * as client from 'openid-client';
const config = await client.discovery( new URL('https://auth.external-integration.ubikap.dev'), CLIENT_ID, { client_secret: CLIENT_SECRET, // Ubikap signs ID tokens with ES512; openid-client expects RS256 unless told otherwise id_token_signed_response_alg: 'ES512', }, client.ClientSecretBasic(CLIENT_SECRET),);Raw HTTP: GET https://auth.external-integration.ubikap.dev/.well-known/openid-configuration.
2. Build the authorization URL
Section titled “2. Build the authorization URL”const codeVerifier = client.randomPKCECodeVerifier();const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);const state = client.randomState();const nonce = client.randomNonce();
// Keep these three in the member's server-side session until the callbacksession.oidc = { codeVerifier, state, nonce };
const authorizationUrl = client.buildAuthorizationUrl(config, { redirect_uri: REDIRECT_URI, scope: 'openid office offline_access', prompt: 'consent', code_challenge: codeChallenge, code_challenge_method: 'S256', state, nonce,});
response.redirect(authorizationUrl.href);| Parameter | Value |
|---|---|
client_id | Your client id. |
response_type | code |
redirect_uri | One of your registered redirect URIs, exactly. |
scope | openid office offline_access — add email profile if your client is allowed them. |
prompt | consent — required to receive a refresh token. |
code_challenge | Base64url of the SHA-256 of the code verifier. |
code_challenge_method | S256 — the only accepted method. |
state | Random value, checked at the callback. |
nonce | Random value, checked in the ID token. |
login_hint | Optional: the member’s email, to pre-fill the login page. |
The resulting URL looks like:
https://auth.external-integration.ubikap.dev/auth?client_id=…&response_type=code&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&scope=openid+office+offline_access&prompt=consent&code_challenge=…&code_challenge_method=S256&state=…&nonce=…3. The member signs in and consents
Section titled “3. The member signs in and consents”- The member enters their email, then their password — or is sent to their office’s SSO.
- A consent page asks the member to open their office to your application. Only a member allowed to manage the office’s connectors can accept; others land on the Ubikap error page.
- The provider redirects to your
redirect_uriwith?code=…&state=…, or with?error=access_denied&state=…when the member refuses.
4. Exchange the code
Section titled “4. Exchange the code”const tokens = await client.authorizationCodeGrant(config, new URL(request.url, APP_ORIGIN), { pkceCodeVerifier: session.oidc.codeVerifier, expectedState: session.oidc.state, expectedNonce: session.oidc.nonce,});
const { sub } = tokens.claims()!;Raw HTTP:
POST /tokenAuthorization: Basic base64(client_id:client_secret)Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=…&redirect_uri=…&code_verifier=…The response holds access_token (opaque, 1 hour), id_token (only sub), refresh_token and expires_in.
5. Read the office with /me
Section titled “5. Read the office with /me”const userInfo = await client.fetchUserInfo(config, tokens.access_token, sub);
const officeKey = userInfo.office_key;Raw HTTP: GET /me with Authorization: Bearer <access_token>. Response:
{ "sub": "jane.doe@example.com", "office_key": "OFC_1234", "email": "jane.doe@example.com", "given_name": "Jane", "family_name": "Doe", "name": "Jane Doe"}email, given_name, family_name and name are present only with the email and profile scopes.
6. Refresh
Section titled “6. Refresh”const refreshed = await client.refreshTokenGrant(config, storedRefreshToken);
// The previous refresh token is now invalid: persist the new one before anything elseawait saveRefreshToken(memberId, refreshed.refresh_token);Run at most one refresh per member at a time: reusing a refresh token revokes the whole sign-in. See OIDC tokens and refresh.
7. Sign out
Section titled “7. Sign out”await client.tokenRevocation(config, storedRefreshToken, { token_type_hint: 'refresh_token' });
const endSessionUrl = client.buildEndSessionUrl(config, { id_token_hint: storedIdToken,});
response.redirect(endSessionUrl.href);The member confirms the logout on a Ubikap page, then lands on the Ubikap login page. Do not send post_logout_redirect_uri: no client has one registered, so the provider would reject the request.
Revoking the refresh token is enough to sign the member out of your application only; ending the session also signs them out of Ubikap in this browser.
Checklist
Section titled “Checklist”- PKCE with
S256on every authorization request. officein the scope, andoffice_keyread from/me— never from the ID token.offline_accessandprompt=consentto get a refresh token.- ID tokens verified as
ES512. - One refresh at a time per member, with the rotated refresh token persisted before use.