A budgeting app wants to pay 45.00 EUR from Maya’s bank account to a merchant. The bank exposes a payments API protected by OAuth. Classic scopes such as payments are too broad: Maya should approve this payment, the bank must be sure the request came from the registered app, and a stolen token must be useless.
This walkthrough combines PAR, RAR, DPoP, and strong client authentication, similar to what the FAPI 2.0 Security Profile requires.
The flow
Budget app (confidential client, private_key_jwt, DPoP key) Bank AS Payments API
1. POST /par authorization_details=[payment 45 EUR → Merchant A]
+ PKCE + client_assertion + dpop_jkt ───────────────>
<───────── request_uri (60 s)
2. Redirect Maya's browser → /authorize?client_id&request_uri
3. Maya authenticates (passkey) and sees "Pay 45.00 EUR to Merchant A"
4. <──── code + iss
5. POST /token code + code_verifier + client_assertion + DPoP proof ──>
<───────── DPoP-bound access token
(authorization_details, cnf.jkt)
6. POST /payments Authorization: DPoP … DPoP: proof(ath) ──────────────────────────────> executes
Why each piece is there
| Mechanism | Threat addressed |
|---|---|
private_key_jwt client authentication | Impersonation of the app; no shared secret to steal. |
| PAR | Tampering with the amount or creditor in the browser; early client authentication. |
RAR authorization_details | Over-broad consent; the token authorizes exactly one payment. |
PKCE and iss | Code interception, injection, and mix-up. |
| Passkey authentication | Phishing of Maya’s credentials. |
| DPoP-bound token | Replay of a stolen access token. |
| Short token lifetime | Limited exposure if everything else fails. |
What the payments API checks
- DPoP proof: signature,
htm,htu,iat,jti,ath, and key thumbprint equal tocnf.jkt. - Token: issuer, audience (
https://payments.example.test), expiry. authorization_details: typepayment_initiation, and that the submitted payment matches amount, currency, and creditor exactly.- Single use: the authorization has not been consumed already.
If the app tries to submit 450.00 EUR, or a different creditor, the API rejects it even though the token is valid.
Variations
- Step-up: for amounts above a threshold, the API returns
insufficient_user_authentication(RFC 9470) and the app restarts with strongeracr_values. - Recurring payments: a different
typewith limits, frequency, and an end date, plus revocation by the user. - Decoupled approval: with CIBA, Maya approves on her banking app while paying at a shop terminal.
Test cases
- Changing any field of the pushed request after PAR is impossible; a modified front-channel parameter is ignored.
- A token used without a DPoP proof, or with a proof from another key, is rejected.
- A second submission with the same authorization is rejected.
- The consent screen displays the amount and creditor exactly as executed.