Getting Started with OIDC
OpenID Connect (OIDC) is an identity layer on top of the authorization-code flow. When enabled, the server issues a signed ID token alongside the access token, exposes a UserInfo endpoint, publishes its public keys at a JWKS endpoint, and advertises its capabilities through discovery.
OIDC is opt-in. The non-OIDC token flows are unchanged when the oidc block is absent.
Prerequisites
- An RSA signing key. OIDC mandates RS256, so the
JwtServicemust be constructed with an asymmetric key, not a shared secret. See Keypair Lifecycle. - An
issuer. The top-levelissueroption is reused as the OIDC issuer and becomes mandatory under OIDC — it is theissof every access token and ID token, and theissuerin the discovery document.
Configuration
OIDC is configured with the top-level issuer plus a nested oidc block on AuthorizationServerOptions:
import { AuthorizationServer, JwtService } from "@jmondi/oauth2-server";
const authorizationServer = new AuthorizationServer(
clientRepository,
accessTokenRepository,
scopeRepository,
new JwtService({ key: process.env.RSA_PRIVATE_KEY_PEM }), // RS256
{
issuer: "https://auth.example.com",
oidc: {
authorizationEndpoint: "https://auth.example.com/authorize",
tokenEndpoint: "https://auth.example.com/token",
userinfoEndpoint: "https://auth.example.com/userinfo",
jwksUri: "https://auth.example.com/jwks",
getUserClaims: async subject => {
const user = await db.users.findById(subject);
return { sub: subject, name: user.name, email: user.email, email_verified: true };
},
},
},
);
authorizationServer.enableGrantType({
grant: "authorization_code",
authCodeRepository,
userRepository,
});The library does not own routing, so the endpoint URLs are supplied explicitly — they appear verbatim in the discovery document. See the full options table.
Wire the endpoints
The three new OIDC endpoints each return a plain ResponseInterface, so every adapter handles them unchanged:
// JWKS — the relying party fetches the public verification keys here.
app.get("/jwks", (req, res) => handleExpressResponse(res, authorizationServer.jwks()));
// Discovery — .well-known/openid-configuration
app.get("/.well-known/openid-configuration", (req, res) =>
handleExpressResponse(res, authorizationServer.openidConfiguration()),
);
// UserInfo — returns scope-derived claims for a presented access token.
app.get("/userinfo", async (req, res) => {
try {
handleExpressResponse(res, await authorizationServer.userInfo(req));
} catch (e) {
handleExpressError(e, res);
}
});The /authorize and /token endpoints are unchanged — when the openid scope is granted, /token adds an id_token to the response body automatically.
Call the flow
Request the openid scope (plus any of profile, email, address, phone) at /authorize. The standard OIDC scopes are auto-recognized for the authorization code flow when OIDC is enabled (other grants are unaffected). After exchanging the code at /token, the response carries both an access_token and an id_token; the access token also drives UserInfo.
To confirm the whole surface works against a real relying party, run the OIDC conformance smoke test.
Access token format
The access token is a JWT tagged typ: at+jwt (RFC 9068), but two claims intentionally deviate from the strict profile for backward compatibility:
cid, notclient_id. The library has always identified the client with the non-standardcidclaim; RFC 9068 §2.2 specifiesclient_id. Resource servers reading the access token should look forcid.audis conditional. RFC 9068 §2.2 listsaudas required, but the access token carriesaudonly when anaudience(oraud) parameter is supplied on the request. With no audience requested, noaudclaim is emitted.
The ID token is unaffected and follows OpenID Connect Core 1.0.
Known limitations (v1)
- No ID token on refresh. ID tokens are issued only in the authorization-code exchange.
- No
offline_accessauto-recognition. Refresh-token issuance remains consumer-owned. - RS256 only. ES256 is deferred — a single-key model cannot satisfy OIDC Discovery §3 with an ES256 key. See Keypair Lifecycle.
- Plain JSON UserInfo only. Signed/encrypted UserInfo responses are not yet supported.
Next steps
- Keypair Lifecycle — generating, storing, and rotating the RSA key.
- Hooks reference —
getUserClaimsvsgetIdTokenClaims. - UserInfo · Discovery — per-endpoint detail.