Skip to content

OpenID Connect Provider Guide

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.

Every Rakomi environment exposes a discovery document at:

GET {ISSUER_URL}/.well-known/openid-configuration

Example 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).

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 provider
const config = await discovery(
new URL('https://api.rakomi.com'),
'your-client-id',
'your-client-secret',
);
// Step 2: Generate PKCE and nonce
const 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 URL
const 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 token

When registering an OAuth client for OIDC use, include openid in the requested scopes:

Terminal window
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"]
}'

The id_token is a signed JWT (RS256) returned alongside the access_token when openid scope is granted.

ClaimPresent whenDescription
subAlwaysUser’s stable identifier (UUID). Use this as your identity key.
issAlwaysIssuer URL (= ISSUER_URL config)
audAlwaysYour client_id
iatAlwaysToken issuance time (Unix seconds)
expAlwaysExpiration time (15 minutes from issuance)
at_hashAlwaysAccess token hash for binding (OIDC §3.1.3.6)
auth_timeAlwaysWhen the user last authenticated
nonceIf requestedReplay protection value you provided
emailemail scopeUser’s email address
email_verifiedemail scopeWhether email has been verified
updated_atprofile scopeLast profile update (Unix seconds)
acrAlwaysAuthentication Context Class Reference (OIDC Core §2) — see acr_values_supported above for the full vocabulary and Step-up authentication for what each value means.
amrAlwaysAuthentication 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_idorg scope, and the user has at least one organizationIdentifier of the caller’s currently-active organization
org_roleorg scope, and the user has at least one organizationThe caller’s role in that organization
org_membershipsorg scope, and the user has at least one organizationThe 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.

The /oauth/userinfo endpoint returns claims for the authenticated user:

Terminal window
GET /oauth/userinfo
Authorization: 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) → sub
  • emailemail, email_verified
  • profileupdated_at
  • orgorg_id, org_role, org_memberships (present only when the user has at least one organization)

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 the id_token and the access_token carry organization claims when the client requested and the user granted the org scope.
  • Device Authorization Grant (device_code): in Rakomi’s implementation, both the id_token and the access_token carry organization claims when org was granted — matching the Authorization Code and CIBA grants. A device_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_token and the access_token carry 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_token is reissued on refresh. This applies uniformly across grants, including a device_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 typeid_tokenaccess_token
Authorization Code
Device Authorization
CIBA
Refresh Token— (not reissued)✅ (re-fetched live)
Client Credentials (M2M)n/a

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.

Always send a nonce parameter during authorization. The nonce is:

  1. Stored on the authorization code
  2. Echoed in the id_token nonce claim
  3. Verified by your RP library against the value you generated

This prevents id_token replay attacks.

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.

ParameterRequiredDescription
id_token_hintRecommendedA 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_uriOptionalWhere 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.
stateOptionalEchoed 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.

ErrorCauseResolution
invalid_clientWrong client_id or client_secretVerify credentials. Ensure you use client_secret_post, not client_secret_basic.
invalid_grantAuthorization code expired or already usedCodes expire in 10 minutes and are single-use. Do not retry with the same code.
insufficient_scopeopenid scope missing from access tokenAdd openid to requested scopes.
redirect_uri_mismatchRedirect URI not registeredredirect_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_typeClient requested token response typeRakomi supports code only (Authorization Code flow).

prompt is a space-delimited, case-sensitive list (OIDC Core 1.0 §3.1.2.1).

ValueBehaviour
noneNo 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.
consentThe 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_accountAccepted but ignored today — see Known limitations.

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.

  • 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 on prompt=login today.
  • max_age / login_hint: These parameters are accepted but ignored. Re-authentication based on max_age is deferred to a future story.
  • client_secret_basic: HTTP Basic Auth at the token endpoint is not yet supported. Use client_secret_post.
  • Custom profile claims (name, picture, locale): Not yet available. Only updated_at is returned for profile scope.
  • Backchannel / Frontchannel logout: Not yet implemented (backchannel_logout_supported: false).