OpenID Connect · OAuth 2.0

Advanced features, explained simply

These mechanisms strengthen the security of the base flow. Each one answers a specific weakness: we start with the problem, then the solution.

The common thread

The basic flow sends information through the browser, where anyone can tamper with it. Advanced features fix that at each step: PAR and JAR protect the request, private_key_jwt protects the application's identity, JARM protects the response, DPoP protects the token once issued, and CIBA covers the case where there is no browser. On top of that come resource (which API), RAR (which precise operation), acr (which authentication level) and dynamic registration of clients.

private_key_jwt · PAR · JAR · JARM · DPoP · CIBA · Resource indicators · RAR · acr and step-up · Dynamic client registration · Pairwise and signed userinfo

private_key_jwt

RFC 7523 / OIDC Core 9

Prove who you are with a signature rather than a shared password.

The problem

A confidential client normally authenticates with a "client_secret", a password that the client and the server both know. If it leaks on one side, the other has no way of knowing, and the secret travels through configuration files.

How it works

The client keeps a private key that never leaves its premises. On each call, it builds a small signed "attestation" (a JWT) saying "I am this client, I am addressing this server, and this is valid for a few minutes only". The server only has the public key: it verifies the signature, that's all.

As a picture

Instead of a password the guard knows, you sign each entry with your personal pen; the guard only has a specimen of your signature.

When to use it

Banks, public administrations, any environment that forbids shared secrets. Mandatory in several security profiles (FAPI).

Pitfalls

Replayed assertion (hence the single-use "jti" identifier), wrong audience, lifetime too long, wrong key.

In ID-Kit

Create a "Confidential with key (private_key_jwt)" client in your test provider, paste your certificate or public JWKS, then call /token with client_assertion. ID-Kit tells you precisely why an assertion is rejected.

PAR: pushed authorization request

RFC 9126

Submit your sign-in request directly to the server, without passing it through the browser.

The problem

Normally, the request (who I am, what I want, where to return) is written in the page address and travels through the browser: the user or malicious software can read or change it, and very long addresses are a problem.

How it works

The application first sends its entire request to the server, server to server, authenticating itself. The server answers with a simple "ticket" (request_uri) valid for one minute. The application then sends the browser to the server with only that ticket. The ticket can only be used once.

As a picture

Instead of shouting your order across the dining room, you hand it to the kitchen and only show the room a ticket number.

When to use it

Whenever you want to prevent a parameter from being tampered with (amount, scopes, redirect_uri): banking, healthcare, payment. Required by FAPI.

Pitfalls

Ticket expired (60 s), already used, or presented by a client other than the one that submitted it.

In ID-Kit

Test provider: call /par. On the test application side, choose the "code_par" flow. The provider's "Require PAR" setting rejects classic requests.

JAR: signed authorization request

RFC 9101

Sign the sign-in request so that nobody can modify it on the way.

The problem

Even with the parameters in the address, nothing proves they really come from the application and have not been changed.

How it works

The parameters are gathered in a JWT signed with the client's key ("request" parameter). The server verifies the signature, issuer, audience and date. The JWT's parameters replace those of the address.

As a picture

A letter in an envelope sealed with wax: if the seal is broken, the server sees it.

When to use it

With PAR in high-security profiles; alone, when there is no server-to-server channel.

Pitfalls

Client public key not registered, "aud" that is not the server's issuer, expired JWT, different client_id.

In ID-Kit

Register the client's public key then send your JWT in "request" or to /par. Bad signatures and audiences are explained.

JARM: signed authorization response

FAPI JARM

The server also signs its response: the application knows it really comes from it and has not been modified.

The problem

When the server sends the user back to the application with "code=…" in the address, anyone in the browser can touch it. This opens the door to attacks where another server impersonates the right one ("mix-up").

How it works

Instead of several plain parameters, the server returns a single "response" parameter: a signed JWT containing code, state, the issuer (iss) and the audience (aud). The application verifies the signature, then reads the code inside. Four variants: jwt, query.jwt, fragment.jwt, form_post.jwt (where the JWT is placed).

As a picture

The response arrives in a sealed envelope rather than on a postcard.

When to use it

FAPI 2.0 profile and regulated sectors. Also useful to detect mix-up attacks between servers.

Pitfalls

Forgetting to verify the signature, or reading plain parameters next to the JWT. query.jwt is forbidden when tokens come back through the browser.

In ID-Kit

Add response_mode=jwt (or a variant) to your authorization request on the test provider: even a denial or an error is signed.

DPoP: key-bound token

RFC 9449

A stolen token is useless without the key of the application that requested it.

The problem

A "Bearer" token works like cash: whoever holds it can use it, even if they stole it from a log, a browser extension or a compromised API.

How it works

The application creates a key pair. On each call, it attaches a short "proof" signed with its private key, saying "here is the method and the address I am calling, at this instant". The server binds the token to the key's thumbprint (cnf.jkt). The API checks that the proof was signed by the same key and that it carries the token's hash (ath). A thief who has the token but not the private key is blocked.

As a picture

A bank card with a PIN, rather than banknotes: the card alone is not enough.

When to use it

Mobile apps and web pages (SPA) where tokens are exposed, sensitive APIs, FAPI 2.0 profiles.

Pitfalls

Replayed proof (single-use jti), wrong method or address (htm, htu), skewed clock (iat ±2 min), token presented with "Bearer" instead of "DPoP", ath missing on the API.

In ID-Kit

Send a DPoP header to the test provider's /token: the token becomes token_type DPoP with cnf.jkt. On /userinfo, you need "Authorization: DPoP" plus a proof with ath, otherwise a message explains what is missing.

CIBA: authentication on another device

OpenID CIBA Core

An application asks the server to authenticate someone on their phone, without a browser or redirect.

The problem

At a counter, a call center, a payment terminal or a voice assistant, the person is not in front of a browser that can redirect.

How it works

The application tells the server "authenticate Alice" (login_hint), optionally with a message to display ("Authorize the payment of 25 €?") and receives an auth_req_id. The server contacts Alice on her phone. Meanwhile, the application polls /token: "authorization_pending" as long as Alice has not answered, then the tokens if she accepts, or "access_denied" if she refuses.

As a picture

The seller calls your bank to say "my customer is here", and it is your bank that sends you a notification to confirm.

When to use it

Payment or sensitive operation validation, call centers, kiosks, telephony. The binding message (binding_message) appears on both screens to check that the right operation is being validated.

Pitfalls

Polling too fast (slow_down), letting the request expire, forgetting that login_hint must designate a user known to the server.

In ID-Kit

Enable the CIBA flow on a client of the test provider, call /bc-authorize. The response contains an "idkit_approval_url" link: it is the "phone" page, where you approve or deny, then you fetch the tokens with /token. ID-Kit only supports poll mode.

Resource indicators: which API the token is meant for

RFC 8707

Say which API you want to talk to, so that the token only works there.

The problem

An access token valid for "any API" is dangerous: if the billing API is compromised, it can replay the token against the payroll API.

How it works

The application adds "resource=https://api.example.com/invoices" to its request (authorization or token). The provider puts this address in the token's audience (aud). The API checks that aud is itself, and refuses otherwise.

As a picture

A named train ticket for a specific line, rather than a ticket valid everywhere.

When to use it

As soon as there are several APIs behind the same provider, and for sensitive tokens.

Pitfalls

API that does not check aud; resource with a fragment (#) or that is not an absolute address (invalid_target).

In ID-Kit

Add resource to /authorize or /token of the test provider: the audience of the JWT token and of introspection changes. The resource is remembered in the code and kept on refresh.

RAR: detailed authorizations

RFC 9396

Ask for a precise right ("pay 25 € to X") rather than a broad scope ("pay").

The problem

A scope like "payments" is too vague: the user does not know what they approve, and the token allows far more than the intended operation.

How it works

The application sends authorization_details: a JSON list of objects with a "type" (for example payment_initiation) and the operation details (amount, beneficiary). The provider displays them, copies them into the token and the response, and the API reads them.

As a picture

A filled-in check (amount, payee) rather than a blank check.

When to use it

Payment, healthcare, fine-grained delegation. Mandatory in some banking profiles.

Pitfalls

Invalid JSON or missing "type" (invalid_authorization_details); API that ignores the token content.

In ID-Kit

Send authorization_details to /authorize or /token: ID-Kit validates it and finds it again in the token, the /token response and introspection.

acr and step-up: authentication level

OIDC Core, RFC 9470

Know how strongly the person authenticated, and require more if needed.

The problem

Signing in with a password is not enough to transfer 10 000 €. The application must be able to say "I want strong authentication".

How it works

The application asks for acr_values (for example the "mfa" level). The provider returns in the ID token the acr value obtained and the amr list of methods used (password, one-time code…). An API can reject a token of too low a level and ask for a "step-up": the person goes back for a stronger authentication.

As a picture

At the bank, you are allowed to view your account with your card, but for a large transfer you are also asked for an ID document.

When to use it

Sensitive operations, compliance (strong authentication).

Pitfalls

Treating acr_values as a requirement when it is a request ("voluntary"); not checking the acr received.

In ID-Kit

Add acr_values=urn:idkit:acr:mfa: the ID token contains acr and amr [pwd, otp], and introspection returns acr and auth_time. Without a request, the level is "pwd".

Dynamic client registration

RFC 7591

An application registers itself with the provider, without going through an administrator.

The problem

Creating clients by hand does not scale: tools, extensions, MCP servers, mobile apps installed by thousands of people.

How it works

The application sends a JSON POST to /register (name, return addresses, wanted flows, authentication method). The provider creates the client and returns client_id and, if needed, client_secret.

As a picture

A self-service registration kiosk instead of an appointment at the front desk.

When to use it

Open ecosystems, tools that plug into unknown providers.

Pitfalls

Unsafe redirects (http outside localhost), grants inconsistent with the authentication method, no limit.

In ID-Kit

Enable "Allow dynamic registration" in the test provider settings, then POST to /register. The limit is 25 clients.

Pairwise and signed userinfo

OIDC Core 8.1 and 5.3.2

A different identifier per application, and signed profile information.

The problem

If all applications see the same identifier (sub) for a person, they can compare their databases and follow them from one service to another.

How it works

In pairwise mode, the provider computes a different sub for each application domain: the same user appears under two different identifiers at two applications. Separately, /userinfo can answer with a signed JWT instead of plain JSON, so the application can prove where the information comes from.

As a picture

A different pseudonym per site instead of a social security number everywhere.

When to use it

Privacy (public administrations, healthcare, education); environments where profile information must be signed.

Pitfalls

Linking accounts across applications with sub (impossible with pairwise); forgetting that sub in userinfo must be identical to the one in the ID token.

In ID-Kit

Two settings on the test provider: "Per-application identifier (pairwise)" and "userinfo as signed JWT".

See also: sequence diagrams · which flow to choose? · the timeline