HIPsHanzo Proposals
Back to HIPs
HIP-0111FinalStandards TrackInfrastructure

Hanzo IAM Authentication Standard

Hanzo AI Team
Created: 2026-06-16
Requires: HIP-0026, HIP-0068

HIP-0111: Hanzo IAM Authentication Standard

Abstract

This is the one and only way an application authenticates a user, provisions an identity, or validates a token against Hanzo IAM. It defines the canonical IETF/RFC endpoint surface, the single approved client library (@hanzo/iam), the integration pattern for every supported framework, the application-registration rules, and the anti-patterns that are forbidden.

RFC-standard only — no vendor compat. Every wire contract on this surface is an IETF RFC or OpenID Connect standard. There are NO verb aliases (get-users, add-user, get-account, issue-user-token, …), no bespoke "verb" REST, and no backward-compat shims, on iam or on any client. Where a capability has a standard, the standard IS the surface: identity provisioning is SCIM 2.0 (RFC 7644/7643), delegated/on-behalf-of tokens are OAuth 2.0 Token Exchange (RFC 8693), account claims are OIDC UserInfo, token validation is Introspection (RFC 7662) + JWKS (RFC 7517). A client that needs a capability uses its RFC; if no RFC covers it, it is the authorization server's internal concern (§6), never a new public "verb".

HIP-0026 specifies the IAM server — the provider itself. This HIP specifies the wire contract — how everything talks to it. Where the two touch (endpoint paths, discovery, the token exchange), this HIP is authoritative and HIP-0026 follows it.

Hanzo IAM (hanzoai/iam) is a clean-room, standards-based OAuth 2.0 + OpenID Connect + SCIM 2.0 provider — original expression, no upstream fork — deployed once per brand. The Beego/xorm fork it replaced is retired to hanzoai/iam-v1 and is out of every graph; a HIP describing Beego, xorm or a Postgres schema is describing that repository, not this one. IAM is deployed once per brand:

BrandIAM origin (serverUrl)Login UI
Hanzohttps://hanzo.idhanzo.id
Luxhttps://lux.idlux.id
Zoohttps://zoo.idzoo.id
Bootnodehttps://id.bootno.deid.bootno.de
Parshttps://pars.idpars.id

The two columns hold the same host on every row, and that is the rule, not a coincidence: the authorization server answers on the origin it names as issuer, so serverUrl is always the brand's identity host. iam.hanzo.ai is not that host — it serves the 200 text/html SPA on every path, /v1/iam/* included, so a client pointed there resolves the catch-all described below and fails on content type. Discovery is the check: https://hanzo.id/.well-known/openid-configuration returns application/json with issuer: https://hanzo.id, and so does the copy served from api.hanzo.ai.

The library is brand-agnostic. You select the brand by setting serverUrl; nothing else changes.

SDK: @hanzo/iam (npm, v0.11.0+) Source of truth for paths: @hanzo/iam → src/paths.ts → OIDC_PATHS

Motivation

Every authentication regression in the estate has had one of three root causes:

  1. Path drift — a client invented its own OIDC path (/oauth/authorize, /api/login/oauth/access_token, /api/...). IAM serves a 200 text/html SPA catch-all for any unregistered path, so a wrong path returns an HTML body with a 200, not a 404. The OAuth library then dies on content-type must be application/json and the failure looks like a server bug. It is not; it is a client hitting the wrong URL.

  2. Discovery drift — a client used raw better-auth genericOAuth({ discoveryUrl }). Discovery resolution landed on the SPA catch-all HTML and the client wired itself to garbage endpoints.

  3. Hand-rolled OAuth — a team reimplemented PKCE, token exchange, or JWKS validation and got the audience check, the S256 challenge, or the refresh rotation subtly wrong.

All three vanish if there is exactly one library that owns exactly one set of paths, and every application uses it. That is this standard.

Specification

1. The canonical OIDC endpoints

These are the only paths. There is no /oauth/*, no /api/login/*, no /api/ prefix. They are relative to the brand serverUrl.

PurposePathRFC / spec
OIDC discovery/.well-known/openid-configurationOIDC Discovery 1.0
AS metadata/.well-known/oauth-authorization-serverRFC 8414
Authorize/v1/iam/oauth/authorizeRFC 6749 §3.1
Token/v1/iam/oauth/tokenRFC 6749 §3.2
UserInfo/v1/iam/oauth/userinfoOIDC Core §5.3
Introspection/v1/iam/oauth/introspectRFC 7662
Revocation/v1/iam/oauth/revokeRFC 7009
JWKS/v1/iam/.well-known/jwksRFC 7517
Logout/v1/iam/oauth/logoutOIDC RP-Initiated Logout
Provisioning (SCIM)/v1/iam/scim/v2/{Users,Groups,…}RFC 7644/7643 (§8)

The token endpoint (/v1/iam/oauth/token) dispatches ONLY standard grant_types — authorization_code (RFC 6749 §4.1, always PKCE-bound), refresh_token (§6, rotating), client_credentials (§4.4), password (§4.3, confidential first-party only), and urn:ietf:params:oauth:grant-type:token-exchange (RFC 8693, §7 — delegation / on-behalf-of). There is exactly one token endpoint and one spelling of it; the legacy access_token alias is gone (a client posts to token, never access_token).

Mandatory parameters, everywhere:

  • PKCE S256 on every authorization request. plain is not permitted. Public clients (SPAs, native) require it; confidential clients use it too.
  • client_secret_basic for confidential clients. HTTP Basic, not body params.
  • Scopes openid profile email (+ offline_access for a refresh token).
  • resource / audience (RFC 8707) name the resource server a token is minted for; the AS stamps aud accordingly and validators fail closed on a mismatch.
  • iss is pinned per deployment (IAM_ISSUER, e.g. https://hanzo.id) so every token and the discovery document advertise ONE stable issuer regardless of request host — never steerable by X-Forwarded-Host.

The discovery document MUST be self-consistent: issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, and jwks_uri all share one origin (host-relative to the brand). The IAM knob that controls this is originFrontend in app.prod.conf — it MUST be empty so discovery is host-relative. A split-origin discovery document breaks strict OIDC clients (openid-client, NextAuth) that pin the issuer.

2. The only integration: @hanzo/iam

JavaScript and TypeScript applications integrate only through @hanzo/iam. No application writes an OIDC path string. No application calls these endpoints by hand. The SDK holds the paths in one place (OIDC_PATHS) and every entry point reads from it; a failed discovery round-trip degrades to these same hard-coded values, so a client can never resolve to the SPA catch-all.

The SDK is split into per-environment entry points. Import the one that matches your runtime:

SubpathSurfaceUse
@hanzo/iamIamClient, typesconditional Node/browser entry
@hanzo/iam/servervalidateToken, getServerSessionserver-side JWT validation + session
@hanzo/iam/betterauthiamProviderbetter-auth apps
@hanzo/iam/nextauthIamProviderNextAuth / Auth.js apps
@hanzo/iam/reacthooks, OrgProjectSwitcherReact SPAs
@hanzo/iam/browserIAM (PKCE client)browser PKCE login
@hanzo/iam/passportcreateIamPassportStrategyNode/Express + Passport

Server-side token validation (any backend)

import { validateToken } from "@hanzo/iam/server";

const result = await validateToken(accessToken, {
  serverUrl: process.env.IAM_ENDPOINT!, // e.g. https://iam.hanzo.ai
  clientId: process.env.IAM_CLIENT_ID!,
});

if (!result.ok) return unauthorized(result.reason);
const { userId, email, owner } = result; // owner = org slug → scope every query to it

validateToken discovers JWKS from /.well-known/openid-configuration, caches the key set per issuer, and verifies signature, iss, aud, and exp. Scope all multi-tenant data access to owner.

Server session (App Router / RSC)

import { getServerSession } from "@hanzo/iam/server";

const session = await getServerSession({ serverUrl: process.env.IAM_ENDPOINT! });
if (!session) redirect("/login");

better-auth

import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
import { iamProvider } from "@hanzo/iam/betterauth";

export const auth = betterAuth({
  plugins: [
    genericOAuth({
      config: [
        iamProvider({
          serverUrl: process.env.IAM_ENDPOINT!,
          clientId: process.env.IAM_CLIENT_ID!,
          clientSecret: process.env.IAM_CLIENT_SECRET!,
        }),
      ],
    }),
  ],
});

iamProvider() returns a config with explicit authorization, token, and userinfo endpoints (the canonical /v1/iam/oauth/* paths) — it never relies on discovery resolution. The registered redirect URI for this provider is https://<app-host>/api/auth/oauth2/callback/hanzo.

NextAuth / Auth.js

import { IamProvider } from "@hanzo/iam/nextauth";

export default NextAuth({
  providers: [
    IamProvider({
      serverUrl: process.env.IAM_ENDPOINT!,
      clientId: process.env.IAM_CLIENT_ID!,
      clientSecret: process.env.IAM_CLIENT_SECRET!,
      checks: ["state", "pkce"],
    }),
  ],
});

React SPA (PKCE)

import { IAM } from "@hanzo/iam/browser";

const iam = new IAM({
  serverUrl: "https://iam.hanzo.ai",
  clientId: "hanzo-myspa",
  redirectUri: `${location.origin}/auth/callback`,
});

await iam.signinRedirect();              // start
const token = await iam.handleCallback(); // on /auth/callback
const access = await iam.getValidAccessToken(); // auto-refresh
import { IamProvider, useIam } from "@hanzo/iam/react";

<IamProvider serverUrl="https://iam.hanzo.ai" clientId="hanzo-myspa">
  <App />
</IamProvider>;

The browser client uses PKCE S256, holds tokens in memory, and refreshes silently. Never persist access tokens in localStorage.

Node / Express + Passport

import passport from "passport";
import { createIamPassportStrategy } from "@hanzo/iam/passport";

passport.use("iam", createIamPassportStrategy({
  serverUrl: "https://iam.hanzo.ai",
  clientId: "hanzo-myservice",
  clientSecret: process.env.IAM_CLIENT_SECRET!,
  callbackUrl: "https://myservice.hanzo.ai/v1/sso/oidc/callback",
}));

3. Application registration

Every application is registered once per brand in IAM before it can authenticate.

  • client_id naming: <org>-<app> (e.g. hanzo-console, lux-wallet, zoo-research). One ID per app per brand.
  • redirectUris: MUST contain the exact callback the SDK/framework uses. There is no wildcard. Per framework:
FrameworkRegistered redirect URI
better-auth (genericOAuth + iamProvider)https://<app-host>/api/auth/oauth2/callback/hanzo
NextAuth / Auth.jshttps://<app-host>/api/auth/callback/iam
React SPA (@hanzo/iam/browser)https://<app-host>/auth/callback
Passporthttps://<app-host>/v1/sso/oidc/callback

The first two carry an /api/ segment because better-auth and NextAuth mount their own handler there and the path is not ours to choose — it is a route on the app's host, in a third-party framework's namespace. The no-/api/ rule (§4.4, §4.7) is about surfaces we serve; it is not weakened by a callback we merely register. A redirect URI that does not match the framework's actual mount point byte for byte fails the flow, so this table records what the framework does, not what we would prefer.

  • Grant: Authorization Code + PKCE. Implicit grant is not used, and implicit is not a registered grant type on any application.
  • Client secret: KMS-managed (HIP-0027, HIP-0136). Never in Git, init data, env files, or images.
  • Superuser convention: no built-in admin — the seeded superuser z@<domain> is the only privileged account, and it is a member of the reserved admin org (HIP-0118). The password is the org's, not the app's.
  • Machine identity: a service authenticates as itself with client_credentials and RFC 8707 resource naming the resource server it is calling — for example resource=hanzo-egress (HIP-0143). There is no service token, no shared secret and no per-app auth; a bearer that does not come from this endpoint is not an identity.

4. Forbidden anti-patterns

These break in production and are not permitted under any circumstance:

  1. Raw better-auth genericOAuth({ discoveryUrl }) — discovery resolves to the SPA catch-all HTML and the client dies with content-type must be application/json. Use iamProvider(), which pins explicit endpoints.
  2. Hand-rolled OAuth / PKCE / JWKS — use the SDK. Reimplementation gets aud, S256, or refresh rotation wrong.
  3. Any per-app OIDC path string — no application writes /v1/iam/oauth/... (or, worse, /oauth/...) itself. The path lives in OIDC_PATHS inside the SDK; applications pass only serverUrl.
  4. Legacy paths — /oauth/*, /api/login/oauth/*, anything /api/-prefixed. Gone. No backward compatibility.
  5. Non-empty originFrontend in production — produces a split-origin discovery document that breaks strict clients.
  6. Per-app social OAuth clients — an app registering its own Google/GitHub (or Web3) OAuth client. Social providers are configured ONCE per network, org-level, and shared (§7). A per-app client re-creates the shared one N times and drifts.
  7. /api/ on the login entry point too — the IAM's own login UI / portal Worker uses the native login API under /v1/iam/* (§6), never /api/login, /api/get-app-login, /api/signup. The "no /api/" rule is absolute, including the login entry point.
  8. "Verb" aliases / bespoke REST for a standardized capability — get-users, get-user?id=, add-user, update-user, delete-user, get-organizations, get-records, issue-user-token, get-account, mint-user-keys, and every other non-standard verb are gone, on iam and on every client. Each has an RFC that IS the surface: identity provisioning → SCIM 2.0 (§8), delegated/on-behalf-of tokens → Token Exchange (§7), account claims → UserInfo (§1). A client that reaches for a verb is reaching for the wrong contract; there is no compat layer that will answer it.
  9. A duplicate spelling of a standard endpoint — one token endpoint, not token + access_token; one userinfo, not userinfo + get-account. An alias is two ways to do one thing; the standard path is the only one served.

5. Gotchas (call out explicitly)

  • SPA catch-all — IAM returns a 200 text/html page for ANY unregistered path. A wrong path is not a 404; it is silent breakage. Clients MUST hit the exact /v1/iam/* paths. This is why the SDK centralizes paths and degrades discovery to hard-coded canonical values.
  • Discovery self-consistency — issuer/authorize/token/userinfo/jwks share one origin (host-relative). Keep originFrontend empty in app.prod.conf.
  • owner is the tenant — the org slug. IAM emits owner (and the standard-name alias organization) in BOTH the OIDC userinfo response AND the JWT, in every token format, scope-independent — so a consumer reading either claim off either surface gets the tenant. Scope every data query to it. The gateway (HIP-0519) propagates it as X-Org-Id; backends behind the gateway trust that header and do not re-parse the JWT. A consumer that reads org from a non-standard field (e.g. a legacy groups claim) and finds nothing MUST fail closed, never silently fall back to a "default"/"personal" org — that is a tenant-isolation defect.

6. The login entry point — the AS's own concern, not a client surface

OAuth 2.0 / OIDC deliberately do not specify how the authorization server authenticates the end user (the credential-entry step). That is the AS's internal concern. So the hosted login page (the per-brand portal at hanzo.id/lux.id/… and its Worker) has a small first-party API it — and ONLY it — calls, under the canonical /v1/iam/* prefix:

PurposePath
App/org resolution before login/v1/iam/get-app-login
Password login (mints the code)/v1/iam/login
Signup/v1/iam/signup
Verification code/v1/iam/send-verification-code
Logout/v1/iam/oauth/logout (§1)

This is NOT a client integration surface and NOT a set of "verbs" a client may call — it is the AS's own login UI talking to the AS. Account claims are NOT here: there is no get-account and no second userinfo — every consumer (including the gateway admin-guard, HIP-0118) reads the standard OIDC UserInfo (/v1/iam/oauth/userinfo, §1), which carries sub, owner/organization, email, email_verified, and the isAdmin claim the SuperAdmin predicate derives from. One account contract, and it is the RFC one.

Same rule as §1: /v1/iam/* only — no /api/, anywhere, including the login Worker. Client apps use only the standard surface (§1) through the SDK; the login API is internal to the AS.

7. Delegation / on-behalf-of — OAuth 2.0 Token Exchange (RFC 8693)

A trusted first-party backend that must call a downstream API as an end user (the console BFF forwarding a request on the signed-in user's behalf, the keyless AI proxy) obtains that token through RFC 8693 Token Exchange on the token endpoint — never a bespoke issue-user-token verb.

  • grant_type=urn:ietf:params:oauth:grant-type:token-exchange, client_secret_basic (confidential clients only), subject_token naming the target user (or a requested_subject), requested_token_type=urn:ietf:params:oauth:token-type:access_token, and resource/audience (RFC 8707) pinning the downstream resource server.
  • The issued token carries the target user's subject + owner (so a resource server that scopes on the validated owner claim scopes to the user's tenant), an act claim recording the acting client, and the requested aud. It is signed by the same trusted key the JWKS publishes — indistinguishable from a token the user obtained directly, which is the point.
  • Two conditions gate it, both required (internal/oidc/issuetoken.go, mintAllowed): the acting application's owning org must be a reserved platform signing owner (admin/built-in), AND its client_id must appear on IAM_TOKEN_EXCHANGE_APPS. The owner-pin is the decisive one. client_id and secret are body-supplied at registration, so a tenant could register an app whose client_id collides with a listed one and, on a backend whose duplicate-row order is unspecified, have its row resolve and its known secret authenticate — but its owner is its own tenant, never a signing owner, so it acts on nobody. An empty or unset list allows nothing.
  • Acting on behalf of a reserved-org (admin/built-in) subject requires the strictly narrower, separately granted IAM_ADMIN_TOKEN_EXCHANGE_APPS, under the same owner-pin (adminMintAllowed). A leaked general-exchange credential can therefore never reach a SuperAdmin identity (HIP-0118). Every exchange is audit-logged.
  • IAM_KEY_MINT_ALLOWED_APPS is a different authority and MUST NOT be conflated with these. It gates credential administration (authz.CapKeyMint) and is keyed on the application name, not the client_id, so an app permitted to exchange tokens is not thereby granted the credential-administration capability nor its reach across the entity registry. Two capabilities, two lists, two keys.

8. Identity provisioning — SCIM 2.0 (RFC 7644 / RFC 7643)

Creating, reading, updating, and deleting identities is SCIM 2.0 — the IETF standard for cross-domain identity management — under /v1/iam/scim/v2/. There are NO get-users/add-user/get-organizations verbs.

ResourcePathMaps to
Service provider config/v1/iam/scim/v2/ServiceProviderConfigsupported features
Schemas / resource types/v1/iam/scim/v2/{Schemas,ResourceTypes}discovery
Users/v1/iam/scim/v2/Users (+ /{id})the user entity
Groups/v1/iam/scim/v2/Groups (+ /{id})organizations, roles
  • Standard verbs are HTTP: GET (list with filter/startIndex/count, or by id), POST (create), PUT/PATCH (RFC 7644 §3.5.2 patch ops), DELETE. Lists return the SCIM ListResponse envelope (totalResults/Resources), not a {status,data,data2} one.
  • A User is the SCIM core schema (urn:ietf:params:scim:schemas:core:2.0:User) plus a Hanzo enterprise extension for owner/isAdmin/credential metadata. Passwords are write-only (password attribute in), never returned. Secrets never cross a SCIM response (the AS masks on read).
  • Tenant scope: a non-super caller's SCIM view is pinned to its own owner; a SuperAdmin may filter across tenants. Same authorization model as every other surface — bearer-authenticated, owner-scoped, fail-closed.
  • Clients provision through the SDK's SCIM client (or any conformant SCIM library); no client writes SCIM URLs by hand, same as §2/§3.

9. Social & Web3 — one shared provider, never per-app

Google, GitHub, and Web3 are configured once per network as org-level providers in IAM (admin/provider-google, admin/provider-github, …). Every app reuses them via a per-app canSignIn toggle — an application never registers its own social OAuth client (§4.6).

  • The shared social OAuth client's redirect URI is IAM's own callback (https://iam.<brand>/callback); the provider hop happens inside IAM, not in the app. The app only ever sets its own redirect_uri (its /auth/callback).
  • An app selects a method with one knob: startLogin({ provider }) adds &provider=<name> to /v1/iam/oauth/authorize. Omit provider for the IAM login page (password + whatever it offers). One flow; the provider is a parameter, not a separate code path — adding a provider is a config entry plus a button, and every app inherits it.
  • Login buttons are presentation, wired through @hanzo/ui <SignIn providers={…}> to the SDK. A surface that lacks a working button has it disabled in config — it is never deleted from code, because the shared provider is always available.

Conformance status

The prohibitions in §Anti-patterns are met by the deployment. Measured against https://hanzo.id on 2026-09-09; unauthenticated probes, so a 401 would mean the route still exists and demands auth, and 410 Gone means it was removed on purpose and the server says what replaced it:

SurfaceThis HIP saysProduction returns
/v1/iam/get-usersgone410 → {"successor":["/v1/iam/users"]}
/v1/iam/get-usergone410 → {"successor":["/v1/iam/users","/v1/iam/keys/principal"]}
/v1/iam/add-usergone410 → {"successor":["/v1/iam/users"]}
/v1/iam/get-organizationsgone410 → {"successor":["/v1/iam/organizations"]}
/v1/iam/get-applicationgone410 → {"successor":["/v1/iam/applications"]}
/v1/iam/issue-user-tokengone410 → {"successor":["/v1/iam/tokens/issue"]}
/v1/iam/get-recordsgone410 → {"successor":["/v1/iam/audit-logs"]}
/v1/iam/get-accountgone410 → {"successor":["/v1/iam/account"]}

The two further deviations are also closed. /v1/iam/oauth/access_token returns 404; the standard /v1/iam/oauth/token is the only spelling, and it answers a POST with 400 on an empty body rather than a 200 carrying an error envelope. The get-account shape that returned 200 with {"status":"error"} is gone with the rest.

410 rather than 404 is the stronger result, and the retirement order in this section is why it was reachable. A 404 says only that nothing is there; 410 with a successor says the route was removed deliberately and names the RFC route that replaced it, so a client still on an alias gets told where to go instead of guessing. The order held: callers moved to the native routes first, the aliases went second, and no deletion was an outage.

The standard surfaces this HIP mandates remain present and correct (/v1/iam/scim/v2/Users and /v1/iam/scim/v2/ServiceProviderConfig answer 401 as application/json; /v1/iam/.well-known/jwks serves an RS256 key set; /v1/iam/oauth/{authorize,token,introspect} are live routes, not the SPA; discovery is single-origin on https://hanzo.id and advertises RFC 8693 token-exchange and device_code). One surface, one spelling — which is what this HIP asked for.

The catch-all still discriminates, and that is how these probes are read: the uncanonical /scim/v2/Users returns 200 text/html, the canonical /v1/iam/scim/v2/Users returns 401 application/json. Content type, not status, is the test.

Security Considerations

  • PKCE S256 mandatory for all flows. Authorization-code interception is the most common OAuth attack; PKCE eliminates it.
  • Signature + claim validation — validateToken verifies the JWKS signature and iss/aud/exp. Never accept a token without these checks.
  • Token storage — in-memory or httpOnly cookies. Never localStorage.
  • Confidential-client secrets — KMS only (HIP-0027). client_secret_basic over TLS.
  • Refresh rotation — handled by the SDK; refresh tokens rotate on use and the previous token is invalidated.
  • TLS everywhere — IAM rejects plaintext. The gateway and ingress (HIP-0519, HIP-0068) terminate and re-encrypt.

References

  1. HIP-0026: Identity & Access Management Standard — the IAM server
  2. HIP-0519: One Identity Boundary — JWT validation + X-Org-Id minted once at the edge
  3. HIP-0068: Ingress Standard — edge TLS and routing
  4. HIP-0027: Secrets Management Standard — KMS-managed client secrets
  5. HIP-0134: One Process, One Socket, One Identity — how IAM fits the estate
  6. HIP-0118: SuperAdmin & Tenant Isolation Model — the reserved-org predicate IAM_ADMIN_TOKEN_EXCHANGE_APPS defends
  7. HIP-0136: One Secret, One Path — where a client secret is addressed
  8. HIP-0143: Egress — The Outbound Trust Boundary — the machine identity a caller presents in order to spend
  9. @hanzo/iam — the SDK; src/paths.ts is the canonical path source
  10. Standards this surface implements (the wire contract, in full):
  • RFC 6749 OAuth 2.0 — authorize, token (authorization_code / refresh_token / client_credentials / password grants)
  • RFC 7636 PKCE S256
  • RFC 7517 JWK / JWKS
  • RFC 7662 Token Introspection
  • RFC 7009 Token Revocation
  • RFC 8414 Authorization Server Metadata
  • RFC 8693 OAuth 2.0 Token Exchange — delegation / on-behalf-of (replaces issue-user-token)
  • RFC 8707 Resource Indicators (resource/audience)
  • RFC 8628 Device Authorization Grant (optional, for input-constrained clients)
  • RFC 7644 SCIM 2.0 Protocol + RFC 7643 SCIM Core Schema — identity provisioning (replaces the get-users/add-user verbs)
  • OpenID Connect Core 1.0 + Discovery 1.0 — id_token, UserInfo, discovery, RP-initiated logout

Copyright

Copyright and related rights waived via CC0.