High-assurance OAuth does not usually fail because one engineer forgot the name of a standard.
It fails in smaller, more ordinary ways. A client is allowed to send a direct front-channel authorization request even though the request should have gone through PAR. A public client is treated like a confidential one. PKCE is present, but not with S256. Tokens remain bearer tokens because sender constraints were documented as a recommendation rather than enforced as policy.
That is the useful way to read the FAPI 2.0 Security Profile: not as a label, and not as one more protocol feature, but as a profile that removes unsafe degrees of freedom for high-value API access.
In the ProAuth v3 preview, FAPI 2.0 support follows that idea. The profile is opt-in for tenants and client applications, disabled by default, and intended for flows where authorization requests, client authentication, and access tokens need stronger runtime guarantees.
This is preview content for the upcoming ProAuth v3 release. It describes the guardrails available in the preview; it does not claim OpenID certification.
Profiles matter because configuration drifts
Most teams already know the individual controls in the FAPI stack: authorization code flow, PAR, PKCE, strong client authentication, DPoP or mTLS sender constraints, and short-lived protocol handles. The harder part is making those decisions stay true after the first integration workshop.
FAPI helps because it changes the failure mode. Instead of relying on a checklist that says “please configure this client securely”, the authorization server rejects the request when the active policy is not satisfied.
flowchart TD
Start["High-value client request"] --> Policy{"FAPI policy active?"}
Policy -->|"No"| Standard["Normal OIDC/OAuth policy"]
Policy -->|"Yes"| Authz["Authorization request guardrails"]
subgraph AuthzStack["Authorization request"]
Authz --> Par["Require PAR"]
Par --> Code["Require response_type=code"]
Code --> Pkce["Require S256 PKCE"]
Pkce --> Front["Browser request carries only client_id and request_uri"]
end
Front --> Token["Token request guardrails"]
subgraph TokenStack["Token request"]
Token --> Confidential["Require confidential client"]
Confidential --> ClientAuth["Require private_key_jwt or mTLS client authentication"]
ClientAuth --> Sender["Require DPoP-bound or certificate-bound tokens"]
end
Sender --> Issue["Issue tokens only after the guardrails pass"]
Authz -. "Missing PAR, wrong response type, or weak PKCE" .-> RejectAuthz["Reject before user interaction continues"]
ClientAuth -. "Weak client auth or bearer-only issuance" .-> RejectToken["Reject before tokens are issued"]
The diagram is intentionally a stack, not a badge. A client does not become high assurance because a setting exists somewhere. It becomes easier to operate safely when each layer rejects the insecure alternative.
How ProAuth resolves the effective policy
The v3 preview keeps the rollout model deliberately conservative. FAPI mode is disabled by default, and existing tenants and client applications continue to use non-FAPI behavior unless the profile is explicitly enabled.
ProAuth resolves the effective policy in this order:
| Level | Setting | Effect |
|---|---|---|
| Tenant force | RequireFapi2SecurityProfile = true | Forces FAPI 2.0 for every client in the tenant |
| Client application | SecurityProfile | Lets an individual client choose its security profile |
| Tenant default | DefaultClientSecurityProfile | Applies when the client application’s SecurityProfile is null |
The profile values are None for normal OIDC/OAuth behavior and Fapi2Security for FAPI 2.0 enforcement. Migrated existing clients are set to explicit None, which preserves compatibility instead of silently moving old integrations into a stricter policy.
That matters operationally. A tenant can start with one high-value client, learn what breaks, update client libraries and resource-server checks, and only later decide whether tenant-wide enforcement is appropriate.
What a FAPI client must satisfy
For a ProAuth v3 preview client running with Fapi2Security, the required shape is narrow by design:
| Area | Required behavior |
|---|---|
| Authorization flow | Authorization code flow only, with response_type=code |
| Authorization request | PAR for every authorization request |
| Browser-facing request | Only client_id and request_uri after PAR |
| PKCE | code_challenge_method=S256 |
| Client type | Confidential client application |
| Client authentication | private_key_jwt, tls_client_auth, or self_signed_tls_client_auth |
| Access token protection | Sender-constrained access tokens through DPoP or certificate binding |
The token endpoint is similarly constrained for FAPI clients: authorization-code redemption requires the original code to have been issued with S256 PKCE, and the token request must include the matching code_verifier. Refresh-token use still requires the configured FAPI client authentication and sender constraint.
ProAuth also caps the relevant lifetimes in FAPI mode: PAR handles stay below 600 seconds, and authorization codes are limited to at most 60 seconds.
The useful rejection happens early
The best security control is often the boring one that fails early.
In FAPI mode, a direct authorization request without PAR is not an alternative path. A request with implicit or hybrid response types is not an acceptable variation. A missing or non-S256 PKCE challenge is not “close enough”. A token request with weak client authentication, missing sender-constraint configuration, or a missing DPoP proof for a DPoP-bound client fails before token issuance.
Those errors use the normal OAuth/OIDC error model, such as invalid_request, unsupported_response_type, invalid_client, or unauthorized_client. The important point is not that the error names are exotic. They are not. The point is that configuration drift becomes a protocol failure at runtime.
Discovery also follows the same idea. ProAuth discovery stays broad unless the tenant requires FAPI. When RequireFapi2SecurityProfile is enabled at tenant level, discovery advertises only FAPI-compatible response types, grant types, and client authentication methods, and includes require_pushed_authorization_requests.
Sender-constrained refresh tokens change the tradeoff
One subtle difference is refresh-token handling.
For non-FAPI clients, ProAuth uses rolling refresh-token rotation: each successful refresh returns a new refresh token, revokes the used token, and treats reuse of an older token as a replay signal for that user and client.
FAPI clients use stable sender-constrained refresh tokens. A successful refresh returns new access and ID tokens but does not return a replacement refresh_token. The client keeps using the original refresh token until it expires or is revoked, and every refresh request still has to satisfy the configured client authentication and sender constraint. For DPoP-bound clients, that means a valid DPoP proof. For mTLS clients, it means the configured client certificate.
That is not weaker rotation by another name. It is a different replay-control model built around proof-bound use of the refresh token.
Roll out the profile where the risk justifies it
FAPI should not become the new checkbox everyone enables because it sounds serious.
It belongs first on clients where the risk, partner requirements, or regulatory context justify the stricter behavior: payment and delegated-action flows, sensitive APIs, cross-organization integrations, and environments where a configuration mistake should fail closed rather than become a production incident.
It also does not replace application authorization, API-side token and proof validation, key and certificate operations, monitoring, or governance between ecosystem participants. The profile makes the OAuth/OIDC layer stricter. The rest of the system still has to be designed and operated with the same care.
A pragmatic rollout usually looks like this:
- Pick one high-value client application.
- Move authorization requests to PAR.
- Confirm code flow with S256 PKCE.
- Move client authentication to
private_key_jwtor mTLS. - Enable DPoP-bound or certificate-bound access tokens.
- Set the client
SecurityProfiletoFapi2Security. - Consider tenant defaults or
RequireFapi2SecurityProfileonly after upgraded clients and APIs are ready.
The order matters less than the principle: make the profile enforce what the client and resource servers can actually operate.
For implementation details, start with the ProAuth FAPI 2.0 Security Profile documentation. The related ProAuth docs for PAR and JAR, DPoP token binding, and mutual TLS client authentication explain the underlying building blocks. For the standards background, read the FAPI 2.0 Security Profile, RFC 9126, RFC 9449, RFC 8705, and the OAuth 2.0 Security Best Current Practice. The broader ProAuth v3 security expansion is tracked in the ProAuth 3.0.0 release notes.