PAR, JAR and Hardened Profiles
|
This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against. OAuth 2.1 is still an Internet-Draft ( This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
The authorization request that starts the authorization
code flow normally travels as a query string on a browser redirect — the front channel. That is convenient,
but it puts every parameter (including, for public clients, the PKCE code_challenge) in a URL a browser can
truncate, log, or leak through history and Referer headers, and it gives an attacker sitting in front of the
browser a chance to tamper with parameters before the authorization server ever sees them. This page covers two
IETF mechanisms that hardened deployments use to remove that exposure — PAR (RFC 9126), which moves the
request off the front channel entirely, and JAR (RFC 9101), which cryptographically protects the request’s
integrity wherever it travels — and the FAPI profiles that mandate combinations of both for regulated use
cases such as open banking and health-data access.
PAR: Pushed Authorization Requests (RFC 9126)
The front-channel problem
An ordinary authorization request looks like this — every parameter sitting in a URL the browser will navigate to, log, and potentially truncate if the parameter list gets long (a real constraint once Rich Authorization Requests parameters are added):
GET /authorize?response_type=code
&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&scope=payments.read%20payments.initiate
&state=af0ifjsldkj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: as.example.com
Every one of these parameters passes through the user’s browser, unauthenticated as coming from the client
(the authorization server has no way to know this GET actually originated from the client rather than from
something that merely knows the client’s public client_id), and is fully visible in browser history, in any
proxy log along the path, and to any script running on a page that constructs the redirect.
Pushing the request server-to-server
RFC 9126 adds a new endpoint, the pushed authorization request endpoint, where the client sends the entire
authorization request as an authenticated, direct, server-to-server POST before the user’s browser is
involved at all:
POST /par HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0Mzpnb0dvMjNPU2VU...
response_type=code
&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&scope=payments.read%20payments.initiate
&state=af0ifjsldkj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Because this call is direct and authenticated with the client’s own credentials — exactly the same client
authentication methods covered on
Client Credentials and Client
Authentication, including private_key_jwt and mTLS — the authorization server can trust it came from the
client, not merely from someone who copied a client_id. The authorization server stores the request and
responds with a short-lived, single-use reference:
{
"request_uri": "urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc014b459f7",
"expires_in": 90
}
The browser redirect that follows then carries almost nothing:
GET /authorize?client_id=s6BhdRkqt3
&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A6esc_11ACC5bwc014ltc014b459f7 HTTP/1.1
Host: as.example.com
The authorization server looks up the stored request by request_uri, applies it as if every parameter had
arrived on this GET, and proceeds with the normal consent and redirect flow. Because request_uri values are
short-lived (RFC 9126 recommends a lifetime on the order of tens of seconds to a few minutes) and single-use,
even if one is captured in a browser history entry or a log, it is worthless shortly afterward and cannot be
replayed to start a fresh, differently-scoped flow.
What PAR buys
-
Every authorization parameter, PKCE challenge included, is never on the front channel at all — nothing meaningful for an attacker to intercept, tamper with, or read out of browser history.
-
The authorization server authenticates the client before it will accept a request, closing the gap where an attacker who merely knows a public
client_idcould otherwise construct arbitrary front-channel requests. -
URL length limits stop mattering, since the browser-facing URL is now a fixed, short reference regardless of how many parameters (or how large a JAR request object, see below) the actual request carries.
-
The authorization server can validate the request — reject malformed parameters, unknown scopes, or an unregistered
redirect_uri— before ever redirecting the user’s browser anywhere, giving a cleaner error experience than a redirect to a generic error page.
Some deployments (and, as covered below, FAPI 2.0) go further and mandate PAR for every client, making the
request_uri indirection the only path to the authorization endpoint at all — a direct front-channel
authorization request with inline parameters is then rejected outright.
Error handling
The pushed authorization request endpoint returns ordinary OAuth error responses (RFC 6749 §5.2) with an HTTP error status, before any browser redirect has happened — itself an improvement over a malformed front-channel request, which historically had to be reported by redirecting the browser to an error page or, worse, silently failing:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_request",
"error_description": "redirect_uri does not match any registered value"
}
The subsequent GET /authorize?client_id=…&request_uri=… call can itself still fail — with
invalid_request_uri if the reference has expired, has already been consumed, or does not belong to the
client_id presented alongside it. Because a request_uri is deliberately short-lived and single-use, a
client must treat invalid_request_uri as "start over from the PAR call," never as something to retry with the
same reference.
JAR: JWT-Secured Authorization Requests (RFC 9101)
The signed/encrypted request object
Where PAR removes the request from the front channel, JAR instead protects the request’s integrity and,
optionally, confidentiality wherever it travels, by packaging the whole set of authorization parameters as
claims inside a JWT (see JWT and JOSE for the underlying format) — signed with the client’s key, and optionally also encrypted to the authorization server’s public key — and
sending that JWT as a single request parameter instead of a flat parameter list:
{
"iss": "s6BhdRkqt3",
"aud": "https://as.example.com",
"response_type": "code",
"client_id": "s6BhdRkqt3",
"redirect_uri": "https://client.example.org/cb",
"scope": "payments.read payments.initiate",
"state": "af0ifjsldkj",
"code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
"code_challenge_method": "S256",
"exp": 1893456300,
"nbf": 1893456000
}
signed (typically RS256, ES256 or PS256) into a compact JWS, and sent as one opaque parameter:
GET /authorize?client_id=s6BhdRkqt3
&request=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzNkJoZFJrcXQzIiwi... HTTP/1.1
Host: as.example.com
The authorization server verifies the JWS signature against the client’s registered key (published via its own
JWKS, the same mechanism JWT and JOSE describes for resource servers
verifying access tokens) before honouring anything inside it. Because the whole parameter set is inside a
signed structure, tampering with any single parameter along the way — a scope, the redirect_uri, the PKCE
challenge — invalidates the signature and the authorization server rejects the request outright. Where the
request also needs confidentiality (parameters the browser or an intermediate party should not be able to
read at all, not merely be unable to tamper with), the client additionally encrypts the JWT to the
authorization server’s public key, producing a nested JWS-inside-JWE structure.
A request object can also be referenced rather than inlined, via request_uri pointing at a URL the client
hosts and the authorization server fetches (its own, separate request_uri mechanism, distinct from — and
predating — the one PAR defines) — useful when the signed JWT itself is too large for a comfortable URL, but
this variant still leaves the fetch-time metadata (which URL was requested, when) on a channel the PAR
request_uri avoids entirely.
How PAR and JAR compose
PAR and JAR solve different problems and combine cleanly: a client can build a JAR request object (protecting the integrity of every parameter with its own signature) and then push that object through the PAR endpoint (removing it from the front channel and getting it authenticated by the client-authentication credential used on the PAR call itself):
POST /par HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0Mzpnb0dvMjNPU2VU...
client_id=s6BhdRkqt3&request=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzNkJoZFJrcXQzIiwi...
followed by the same short request_uri redirect PAR always produces. The two are complementary defence in
depth rather than redundant: PAR protects the transport hop to the authorization server and hides the request
shape from the browser; JAR proves the request’s authenticity and integrity end to end, including against a
party that might see the pushed request in transit or in a server-side log at the authorization server itself.
A deployment can adopt PAR without JAR (removing front-channel exposure, without object-level signing) or JAR
without PAR (signed requests still sent inline on the front channel, protecting integrity but not
confidentiality of the parameter values) — FAPI 2.0, covered next, is the profile that requires both together.
FAPI: the profiles that mandate them
FAPI — the Financial-grade API security profile suite, published by the OpenID Foundation’s FAPI working group — takes OAuth 2.0 and OpenID Connect and tightens them into a set of mandatory choices suited to high-value, regulated APIs: open banking (PSD2 and equivalent regimes worldwide), health-record access, and other domains where the consequences of a successful attack are severe enough to justify removing every optional weakening the base specifications otherwise allow.
FAPI 1.0 Baseline and Advanced
FAPI 1.0 ships as two related profiles:
-
FAPI 1.0 Part 1 — Baseline mandates: the authorization code flow with PKCE; TLS 1.2 or higher with a restricted cipher suite list;
client_secret_basicor a stronger client authentication method (no unauthenticated public clients for the flows it covers); short access-token lifetimes; and strict validation ofredirect_uriandstate, effectively making mandatory several of the practices Security Best Practices recommends more broadly as best-current-practice rather than a hard profile requirement. -
FAPI 1.0 Part 2 — Advanced builds on Baseline and adds: JAR (a signed request object is mandatory, not optional); one of holder-of-key sender-constraining — mTLS-bound tokens (RFC 8705) or, in later Advanced guidance, DPoP — so a stolen access token is unusable, exactly the mechanisms covered on Sender-Constrained Tokens; and stronger requirements around detached signatures for response integrity in some deployment variants. Advanced is the profile most open-banking regimes actually require for account-information and payment-initiation APIs.
FAPI 2.0
FAPI 2.0 is a substantial simplification relative to FAPI 1.0’s two-tier structure: rather than a Baseline/ Advanced split with several optional combinations, it defines one Security Profile with a smaller, more opinionated set of mandatory mechanisms, explicitly designed around the lessons learned deploying FAPI 1.0 at scale. FAPI 2.0 mandates:
-
PAR for every authorization request — no direct front-channel request is permitted at all.
-
PKCE, unconditionally, on top of PAR.
-
Sender-constrained access tokens — DPoP or mTLS — unconditionally; a plain bearer token is never compliant.
-
Strict client authentication (no unauthenticated clients;
private_key_jwt,tls_client_auth, orself_signed_tls_client_auth). -
issin the authorization response (RFC 9207), the same mix-up-attack defence covered on Security Best Practices.
FAPI 2.0 treats JAR as a companion message signing profile (FAPI 2.0 Message Signing) rather than a baked-in mandatory requirement of the core Security Profile itself, reflecting that PAR alone — being an authenticated, direct server-to-server call — already removes most of the front-channel exposure JAR exists to prevent; deployments needing end-to-end request-integrity proof beyond the PAR hop still layer JAR on top, exactly as described above.
Comparing the profiles
| Requirement | FAPI 1.0 Baseline | FAPI 1.0 Advanced | FAPI 2.0 Security Profile |
|---|---|---|---|
Authorization code + PKCE |
Mandatory |
Mandatory |
Mandatory |
PAR (RFC 9126) |
Not required |
Recommended in later guidance |
Mandatory |
JAR (RFC 9101) |
Not required |
Mandatory |
Separate Message Signing profile |
Sender-constrained tokens (mTLS or DPoP) |
Not required |
Mandatory |
Mandatory |
|
Not required |
Not required |
Mandatory |
Unauthenticated public clients |
Permitted for some flows |
Not permitted |
Not permitted |
Structure |
Single profile |
Builds on Baseline |
Single unified profile (no tiers) |
What conformance actually requires
FAPI is not a suggestion a deployment can partially adopt and still call itself "FAPI-compliant" — conformance
is verified against the OpenID Foundation’s published certification programme
(https://openid.net/certification/), which runs an automated conformance test suite against a live deployment
of the authorization server (and, for some profiles, the client) and checks every mandatory behaviour: PAR and
JAR wire format, sender-constraining actually rejecting an unconstrained token, iss present and correct,
TLS configuration, and the full set of profile-specific error handling. Passing certification produces a
listed, publicly verifiable certification record; regulated deployments in open banking and similar regimes
frequently require this certification as a condition of participation, not merely as a best-practice
recommendation. A deployment that has "implemented FAPI-like controls" without running the conformance suite
has not established FAPI conformance in the sense a regulator or an open-banking scheme operator will accept.
References
-
RFC 9101 — The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)
-
RFC 9207 — OAuth 2.0 Authorization Server Issuer Identification
-
RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
-
RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)