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 (draft-ietf-oauth-v2-1-16, 3 September 2026) and is flagged as such everywhere it appears on these pages. It is a working-group consolidation in progress, not a published standard; nothing here should be read as saying otherwise.

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_id could 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.

flowchart TB subgraph plain["Plain front-channel authorization request"] direction TB b1["Browser"] -- "GET /authorize?response_type=code&client_id=...&scope=...&code_challenge=...\n(every parameter visible in the URL, history, logs, Referer)" --> as1["Authorization server"] end subgraph par["With PAR"] direction TB c2["Client backend"] -- "POST /par (authenticated, server-to-server)\nfull parameter set" --> as2["Authorization server"] as2 -- "request_uri (short-lived, single-use)" --> c2 b2["Browser"] -- "GET /authorize?client_id=...&request_uri=urn:...\n(no sensitive parameters visible)" --> as2 end

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_basic or a stronger client authentication method (no unauthenticated public clients for the flows it covers); short access-token lifetimes; and strict validation of redirect_uri and state, 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, or self_signed_tls_client_auth).

  • iss in 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

iss in authorization response (RFC 9207)

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.