Advanced

Authorizing a single payment with PAR, RAR, and DPoP

Combine pushed requests, rich authorization details, strong client authentication, and sender-constrained tokens for a high-value transaction.

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

MechanismThreat addressed
private_key_jwt client authenticationImpersonation of the app; no shared secret to steal.
PARTampering with the amount or creditor in the browser; early client authentication.
RAR authorization_detailsOver-broad consent; the token authorizes exactly one payment.
PKCE and issCode interception, injection, and mix-up.
Passkey authenticationPhishing of Maya’s credentials.
DPoP-bound tokenReplay of a stolen access token.
Short token lifetimeLimited exposure if everything else fails.

What the payments API checks

  1. DPoP proof: signature, htm, htu, iat, jti, ath, and key thumbprint equal to cnf.jkt.
  2. Token: issuer, audience (https://payments.example.test), expiry.
  3. authorization_details: type payment_initiation, and that the submitted payment matches amount, currency, and creditor exactly.
  4. 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 stronger acr_values.
  • Recurring payments: a different type with 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.

References

Knowledge is better when it is shared.Improve this page

Type to search the entire handbook.