OpenID Connect Provider Guide
Rakomi as an OIDC Identity Provider
Section titled “Rakomi as an OIDC Identity Provider”Rakomi implements OpenID Connect Core 1.0 on top of OAuth 2.0. Any RP (Relying Party) library that supports OIDC Discovery can integrate with Rakomi.
Discovery
Section titled “Discovery”Every Rakomi environment exposes a discovery document at:
GET {ISSUER_URL}/.well-known/openid-configurationExample response (abbreviated):
{ "issuer": "https://api.rakomi.com", "authorization_endpoint": "https://accounts.rakomi.com/authorize", "token_endpoint": "https://api.rakomi.com/oauth/token", "userinfo_endpoint": "https://api.rakomi.com/oauth/userinfo", "jwks_uri": "https://api.rakomi.com/.well-known/jwks.json", "end_session_endpoint": "https://accounts.rakomi.com/logout", "response_types_supported": ["code"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], "scopes_supported": ["openid", "profile", "email", "org"], "token_endpoint_auth_methods_supported": ["client_secret_post", "none"], "code_challenge_methods_supported": ["S256"], "acr_values_supported": ["aal1", "aal2"], "backchannel_logout_supported": false, "frontchannel_logout_supported": false}acr_values_supported shown above is the unconditional subset — eidas_low, eidas_substantial and
eidas_high additionally appear when EUDI Wallet login is configured for that environment. It is an
unordered array (no ranking/priority semantics — see Step-up authentication
for the assurance-level hierarchy), and it scopes only the values Rakomi can issue in the acr
claim, not a set it honors on an inbound acr_values authorization-request parameter (not supported
today). Compare acr values with an exact, case-sensitive string match — never a prefix/substring check
(eidas_low/eidas_high share a prefix; aal1/aal2 differ by one character). Read this field
dynamically per environment rather than hardcoding a closed set client-side — a new AAL/eIDAS level may
be added later.
The discovery document is cached for 1 hour (max-age=3600). Clients should respect this cache — if
EUDI Wallet login was just enabled for your environment, acr_values_supported may lag up to an hour
for callers on long-lived caches. The example above shows the unconditional subset only; check your own
environment’s live response rather than assuming this example matches it (dev/staging may have
eidas_* values where prod does not).
Node.js integration (openid-client v6+)
Section titled “Node.js integration (openid-client v6+)”The openid-client npm package is the recommended RP library. It fetches the discovery document automatically.
import { discovery, authorizationCodeGrant } from 'openid-client';import * as crypto from 'node:crypto';
// Step 1: Discover the providerconst config = await discovery( new URL('https://api.rakomi.com'), 'your-client-id', 'your-client-secret',);
// Step 2: Generate PKCE and nonceconst codeVerifier = crypto.randomBytes(32).toString('base64url');const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url');const nonce = crypto.randomBytes(16).toString('hex');const state = crypto.randomBytes(16).toString('hex');
// Step 3: Build authorization URLconst authorizationUrl = new URL(config.serverMetadata().authorization_endpoint!);authorizationUrl.searchParams.set('client_id', 'your-client-id');authorizationUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');authorizationUrl.searchParams.set('response_type', 'code');authorizationUrl.searchParams.set('scope', 'openid email profile');authorizationUrl.searchParams.set('code_challenge', codeChallenge);authorizationUrl.searchParams.set('code_challenge_method', 'S256');authorizationUrl.searchParams.set('nonce', nonce);authorizationUrl.searchParams.set('state', state);
// Redirect user to authorizationUrl.toString()
// Step 4: Handle callback — exchange code for tokens// (in your callback route handler)const tokens = await authorizationCodeGrant(config, callbackUrl, { pkceCodeVerifier: codeVerifier, expectedNonce: nonce, expectedState: state,});
const idToken = tokens.id_token; // OIDC id_token (RS256)const accessToken = tokens.access_token; // OAuth access tokenClient registration requirements
Section titled “Client registration requirements”When registering an OAuth client for OIDC use, include openid in the requested scopes:
curl -X POST https://api.rakomi.com/v1/oauth/clients \ -H "X-API-Key: your-api-key" \ -H "Content-Type: application/json" \ -d '{ "name": "My App", "redirect_uris": ["https://yourapp.com/callback"], "scopes": ["openid", "email", "profile"] }'Understanding the id_token
Section titled “Understanding the id_token”The id_token is a signed JWT (RS256) returned alongside the access_token when openid scope is granted.
| Claim | Present when | Description |
|---|---|---|
sub | Always | User’s stable identifier (UUID). Use this as your identity key. |
iss | Always | Issuer URL (= ISSUER_URL config) |
aud | Always | Your client_id |
iat | Always | Token issuance time (Unix seconds) |
exp | Always | Expiration time (15 minutes from issuance) |
at_hash | Always | Access token hash for binding (OIDC §3.1.3.6) |
auth_time | Always | When the user last authenticated |
nonce | If requested | Replay protection value you provided |
email | email scope | User’s email address |
email_verified | email scope | Whether email has been verified |
updated_at | profile scope | Last profile update (Unix seconds) |
acr | Always | Authentication Context Class Reference (OIDC Core §2) — see acr_values_supported above for the full vocabulary and Step-up authentication for what each value means. |
amr | Always | Authentication Methods References (RFC 8176) — the method(s) used. Verified values you may branch on: pwd, otp, mfa (MFA-verified login), webauthn (passkey), mlink (magic link), anon (anonymous session), eudi_wallet (EUDI Wallet login), sso/oidc (federated login), smsotp (a one-time-code flow distinct from step-up, which does not offer SMS). oauth, ciba, device_code and tx reflect the OAuth grant type used rather than a distinct authentication event. May also carry a SAML-derived value mapped from the external IdP’s AuthnContextClassRef, which varies by IdP. See Step-up authentication. |
org_id | org scope, and the user has at least one organization | Identifier of the caller’s currently-active organization |
org_role | org scope, and the user has at least one organization | The caller’s role in that organization |
org_memberships | org scope, and the user has at least one organization | The full list of the caller’s organization memberships |
acr and amr are standard OIDC/RFC claim names (RFC 8176 §2 gives pwd/otp as example amr
values in the spec text itself) — but the specific string values Rakomi puts in them (aal1,
eidas_high, eudi_wallet, …) are Rakomi’s own vocabulary, since neither OIDC Core nor RFC 8176
mandates a value format. acr/amr describe the authentication method, not the subject’s identity
— relevant scoping if you’re doing your own data-protection-impact assessment on what you receive.
org_id, org_role and org_memberships are Rakomi-specific, non-standard claims — they are not
part of OIDC Core. org_id/org_role reflect the caller’s currently-active organization; org_memberships
is the full list of every organization the caller belongs to. See Organization claims
below for the full shape and behavior.
UserInfo endpoint
Section titled “UserInfo endpoint”The /oauth/userinfo endpoint returns claims for the authenticated user:
GET /oauth/userinfoAuthorization: Bearer <access_token>Response:
{ "sub": "usr_01J...", "email": "user@example.com", "email_verified": true, "updated_at": 1713350400}Claims are filtered to the scopes granted at authorization time:
openid(required) →subemail→email,email_verifiedprofile→updated_atorg→org_id,org_role,org_memberships(present only when the user has at least one organization)
Organization claims
Section titled “Organization claims”In short: request the org scope at authorization; claims land where you’d expect, except for the two
documented asymmetries below.
When the org scope is granted and the user belongs to at least one organization, Rakomi adds
org_id, org_role and org_memberships claims. org_memberships is an array of objects, one per
organization the caller belongs to:
{ "org_memberships": [ { "org_id": "org_01J...", "org_slug": "acme-inc", "org_role": "admin", "membership_public_metadata": { "department": "engineering" } } ]}org_slug is a URL-safe organization identifier; membership_public_metadata is optional and only
present when the organization admin has set metadata on that membership.
When these claims are populated, per grant type
Section titled “When these claims are populated, per grant type”- Authorization Code grant (
response_type=code): in Rakomi’s implementation, both theid_tokenand theaccess_tokencarry organization claims when the client requested and the user granted theorgscope. - Device Authorization Grant (
device_code): in Rakomi’s implementation, both theid_tokenand theaccess_tokencarry organization claims whenorgwas granted — matching the Authorization Code and CIBA grants. Adevice_code-issued refresh token is also fully supported (see the Refresh Token grant note below). - CIBA (Client-Initiated Backchannel Authentication — an OpenID Foundation specification, not an
IETF RFC): in Rakomi’s implementation, both the
id_tokenand theaccess_tokencarry organization claims, same as the Authorization Code grant. - Refresh Token grant: in Rakomi’s implementation, the access token’s organization claims are
re-fetched live on every refresh, not carried forward from the original grant — so if the user’s
organization membership or role changed since the original login, the refreshed token reflects the
current state. No
id_tokenis reissued on refresh. This applies uniformly across grants, including adevice_code-issued refresh token, which correctly re-derives organization claims live on refresh. - Client Credentials grant (machine-to-machine / M2M): no organization claims — machine-to-machine tokens have no user context, so the entire custom-claims mechanism (including organization claims) never applies to them. See the Resource Server guide for the full M2M claims picture.
| Grant type | id_token | access_token |
|---|---|---|
| Authorization Code | ✅ | ✅ |
| Device Authorization | ✅ | ✅ |
| CIBA | ✅ | ✅ |
| Refresh Token | — (not reissued) | ✅ (re-fetched live) |
| Client Credentials (M2M) | n/a | — |
Truncation and data minimization
Section titled “Truncation and data minimization”A caller with many organization memberships may see the org_memberships list truncated to fit a
response size limit. Your code should always iterate the list rather than assume a fixed length or
index into it — truncation tends to keep the caller’s more-recently-active organizations, but this is
not a guarantee your code should depend on.
Treat membership_public_metadata as third-party data — it is supplied by the organization admin, not
by Rakomi — and avoid retaining it longer than you need it.
See the oidc-org-scope-claims recipe in the examples gallery for a runnable resource-server example
that verifies a token carrying these claims.
Nonce (replay protection)
Section titled “Nonce (replay protection)”Always send a nonce parameter during authorization. The nonce is:
- Stored on the authorization code
- Echoed in the
id_tokennonceclaim - Verified by your RP library against the value you generated
This prevents id_token replay attacks.
RP-Initiated Logout
Section titled “RP-Initiated Logout”To log users out from Rakomi’s session, redirect them to:
GET https://accounts.rakomi.com/logout ?id_token_hint=<id_token> &post_logout_redirect_uri=https://yourapp.com/logged-out &state=<opaque value>POST with form-encoded parameters is supported as well.
| Parameter | Required | Description |
|---|---|---|
id_token_hint | Recommended | A previously issued id_token for your client. Required for post_logout_redirect_uri to be honored, and for the logout to end your client’s session (rather than the browser’s currently active one) when the user is signed in to more than one organization. An expired id_token is still accepted as a hint. |
post_logout_redirect_uri | Optional | Where to redirect after logout. Honored only when a valid id_token_hint identifies your client and the value exactly matches one of the post-logout redirect URIs registered for that client — otherwise the user is redirected to the Rakomi landing page instead. Must be HTTP/HTTPS with no fragment; matching is exact, with the same RFC 8252 §7.3 loopback any-port exception for public (native) clients as redirect_uri. |
state | Optional | Echoed back as a query parameter on the validated redirect target. Never appended to the fallback redirect. |
Register post-logout redirect URIs on the OAuth client the same way you register redirect_uris — a client with none registered always falls back to the Rakomi landing page (the logout itself still completes).
After logout, the matching session is revoked and its cookie entry removed; other organizations’ sessions in the same browser are left intact.
Common errors
Section titled “Common errors”| Error | Cause | Resolution |
|---|---|---|
invalid_client | Wrong client_id or client_secret | Verify credentials. Ensure you use client_secret_post, not client_secret_basic. |
invalid_grant | Authorization code expired or already used | Codes expire in 10 minutes and are single-use. Do not retry with the same code. |
insufficient_scope | openid scope missing from access token | Add openid to requested scopes. |
redirect_uri_mismatch | Redirect URI not registered | redirect_uri must match exactly (including trailing slash). Loopback exception (RFC 8252 §7.3) for public (native) clients only — check your client’s client_type: on http://127.0.0.1, http://[::1] and http://localhost redirect URIs any port is accepted, but path, query and scheme must still be identical. The host literal is pinned — localhost, 127.0.0.1 and [::1] are not aliases, so register every literal your native/CLI client (e.g. an MCP client) may bind — typically all three; the server deliberately does not reveal which component mismatched. Prefer http://127.0.0.1 over http://localhost (RFC 8252 §8.3). |
unsupported_response_type | Client requested token response type | Rakomi supports code only (Authorization Code flow). |
The prompt parameter
Section titled “The prompt parameter”prompt is a space-delimited, case-sensitive list (OIDC Core 1.0 §3.1.2.1).
| Value | Behaviour |
|---|---|
none | No UI is shown. If the request would require the consent screen, consent_required is returned to your redirect_uri; if an existing grant already covers every requested scope, the authorization code is issued silently. One exception: a request that asks for an agent capability always returns consent_required, whether or not the user has approved that capability before — see below. |
consent | The consent screen is shown even when an existing grant already covers every requested scope — use it to re-confirm before a sensitive operation. If the user denies, access_denied is returned. |
login, select_account | Accepted but ignored today — see Known limitations. |
prompt=none and agent capabilities
Section titled “prompt=none and agent capabilities”Agent capabilities are approved individually by the user, so they cannot be granted without the user
present. A request that asks for one therefore answers consent_required unconditionally — the
answer does not depend on what the user has approved previously, and is byte-identical for a user who
has approved the capability and one who has not.
That uniformity is deliberate and worth relying on: it means prompt=none cannot be used to discover
whether a particular user has authorized a particular capability. Do not build a “check first, then
ask” flow on it — send the authorization request and handle consent_required as the normal path.
none combined with any other value returns invalid_request: the two are contradictory (none means
show no UI, the others mean show it), so the request is rejected rather than guessed at.
Known limitations
Section titled “Known limitations”prompt=login/prompt=select_account: accepted but ignored — the existing session is used. If you need a guaranteed fresh authentication before a sensitive operation, do not rely onprompt=logintoday.max_age/login_hint: These parameters are accepted but ignored. Re-authentication based onmax_ageis deferred to a future story.client_secret_basic: HTTP Basic Auth at the token endpoint is not yet supported. Useclient_secret_post.- Custom profile claims (
name,picture,locale): Not yet available. Onlyupdated_atis returned forprofilescope. - Backchannel / Frontchannel logout: Not yet implemented (
backchannel_logout_supported: false).
Cross-links
Section titled “Cross-links”- OAuth Integration Guide — full OAuth 2.0 PKCE flow
- Test Environment — using Rakomi in CI/CD