FAPI 2.0 Security Profile
What the FAPI 2.0 Security Profile requires of a resource server and how permdock/jwt with profile: 'fapi2' and the OpenAPI emitter enforce those requirements before a permission check runs.
permdock/jwt implements the resource-server rules as subjectFromJwt(token, { profile: 'fapi2' }), and permdock openapi emits the x-permdock-securityProfile extension. Grant Management is tracking only.
What it is
The FAPI 2.0 Security Profile is a final specification of the OpenID Foundation's FAPI Working Group. It started in open banking (the "Financial-grade API" name) and is now positioned as a general-purpose, high-security OAuth 2.0 profile: a fixed set of choices for confidential clients, authorization servers and resource servers that removes the options in OAuth 2.0 that have caused real-world breaches. The profile was formally analysed against the FAPI 2.0 Attacker Model, a companion specification that states which attacker capabilities the profile defends against.
Three parts matter for a permissions library, because they are the parts a resource server, and therefore the application PermDock runs in, has to get right:
5.3.4 Requirements for resource servers
A FAPI 2.0 resource server:
- Accepts access tokens only in the HTTP
Authorizationheader, either as a bearer token per RFC 6750 section 2.1 or as a DPoP-bound token per RFC 9449 section 7.1. - Does not accept access tokens in query parameters (RFC 6750 section 2.3).
- Verifies the token's validity, integrity, expiration and revocation status.
- Verifies that the authorization represented by the token is sufficient for the requested resource access, and otherwise returns an error per RFC 6750 section 3.1 (
insufficient_scope). - Supports and verifies sender-constrained tokens, via mutual TLS (RFC 8705) and/or DPoP (RFC 9449).
5.4.1 Cryptography
JWTs adhere to RFC 8725 (JSON Web Token Best Current Practices). Signing algorithms are PS256, ES256 or EdDSA "using the Ed25519 variant"; none is never accepted. RSA keys are at least 2048 bits, elliptic-curve keys at least 224 bits, and credentials carry at least 128 bits of entropy.
The profile's note under 5.4.1 observed that no registered, fully-specified algorithm for "EdDSA using the Ed25519 variant" existed and allowed one once registered. RFC 9864 (Fully-Specified Algorithms for JOSE and COSE) has since registered Ed25519 (and Ed448) as JWS alg values and deprecated the polymorphic EdDSA identifier. The FAPI 2.0 list PermDock enforces is therefore PS256, ES256 and Ed25519, with alg: EdDSA accepted only when the resolved key is an OKP key with crv: Ed25519 (JOSE).
The RAR recommendation
Note 5 of the authorization-server requirements says that OAuth 2.0 Rich Authorization Requests (RFC 9396) are recommended "when the scope parameter is not expressive enough". That is the same conclusion PermDock reached independently: every permission carries both a scope string and an authorizationDetails type, so a FAPI 2.0 deployment that moved to RAR feeds delegation.authorizationDetails directly. See OAuth for agent delegation and delegation.
Why it matters for PermDock
FAPI 2.0 draws the line between authentication and authorization exactly where PermDock does. Requirements 1, 2, 3 and 5 are about establishing who the caller is and that the token really belongs to them; requirement 4 is the permission check. PermDock does not authenticate, but permdock/jwt is where a verified token becomes a subject, and it is the natural place to refuse tokens that a FAPI 2.0 resource server must refuse. Getting that wrong upstream makes every downstream decide call meaningless: a perfectly correct policy evaluated for a subject derived from a replayed bearer token is still a breach.
The profile also gives PermDock a concrete, externally defined bar for "high security" instead of an in-house checklist. When a regulated deployment (open banking, health data, Australia CDR) asks whether the permission layer is FAPI 2.0 compatible, the answer is a table, not an argument.
How PermDock uses it
permdock/jwt with profile: 'fapi2'
import { createPermDock } from "permdock/hono";
import { createJwtSubjectResolver } from "permdock/jwt";
const resolve = createJwtSubjectResolver({
jwks: new URL("https://as.bank.example/.well-known/jwks.json"),
issuer: "https://as.bank.example",
audience: "https://api.bank.example",
claims: { id: "sub", roles: "roles" },
delegation: {
scopes: "scope",
authorizationDetails: "authorization_details",
},
profile: "fapi2", // turns the 5.3.4 / 5.4.1 rules on
sender: "dpop", // or 'mtls'; 'none' is not accepted under this profile
});
export const { permdock, protect } = createPermDock(policy, {
subject: (c) => resolve(bearerFrom(c.req.raw.headers), c.req.raw), // the request carries the DPoP proof
});subjectFromJwt(token, { profile: 'fapi2', sender: 'dpop' }) is the one-shot form of the same resolver. profile: 'fapi2' is a preset over the individual options and cannot be relaxed option by option: a deployment that needs to accept, say, a query-string token is not FAPI 2.0 and should not claim the profile. The sender option names which sender-constraint mechanism the deployment uses, so the adapter knows whether to look for cnf.jkt (DPoP) or cnf.x5t#S256 (mTLS) and which transport evidence (the DPoP proof header, the client certificate the platform exposes) to check it against. The verified binding is attached to the subject as binding so audit can show which key the request was bound to.
A DPoP proof is self-contained, so it reaches the resolver on the Request. For mTLS the application passes certificateThumbprint, the client certificate thumbprint its platform or TLS terminator exposes; PermDock defines no header contract for it. Publishing RFC 9728 Protected Resource Metadata is left to the deployment: the profile does not require it, and the MCP adapter publishes it where MCP does.
What PermDock enforces
| FAPI 2.0 requirement | Behaviour under profile: 'fapi2' | Failure |
|---|---|---|
5.3.4 (1) token in the Authorization header, bearer or DPoP | The resolver reads only the Authorization header; the scheme must be Bearer or DPoP and must match sender | Anonymous subject (principal: null); a missing header produces no audit event, a wrong scheme is logged |
| 5.3.4 (2) no tokens in query parameters | A token found in the query string is rejected, even if it would otherwise verify | Anonymous subject, audit cause token-in-query |
| 5.3.4 (3) validity, integrity, expiration, revocation | Signature against jwks, iss equals issuer, aud contains audience, exp, nbf and iat honoured within clockTolerance; revocation arrives as a CAEP session-revoked event through permdock/ssf, which invalidates the subject's snapshot; a deployment without an SSF transmitter uses short exp or a per-token check through subjectFromIntrospection | Anonymous subject, audit reason invalid-token with a cause naming the failed claim |
| 5.3.4 (4) authorization sufficiency | The decide call. delegation.scopes and delegation.authorizationDetails are filled from the token and intersected with the principal's grants | denied Decision with reason not-delegated; HTTP adapters answer 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="post:update" and a Problem Details body whose alternatives lists what the token would have allowed |
| 5.3.4 (5) sender-constrained tokens | A token without a cnf claim is rejected; with sender: 'dpop' the DPoP proof is verified against cnf.jkt (RFC 9449 section 7.1); with sender: 'mtls' the client certificate thumbprint is compared with cnf.x5t#S256 (RFC 8705); sender: 'none' is not accepted under the profile | Anonymous subject, audit cause sender-constraint-required, dpop-proof-invalid or mtls-binding-mismatch |
| 5.4.1 algorithms | algorithms is restricted to PS256, ES256 and Ed25519 (RFC 9864); EdDSA is accepted only for an OKP / crv: Ed25519 key and reported by permdock doctor; none, RSA1_5 and the HS* and RS* families are refused before signature verification | Anonymous subject, audit cause alg-not-allowed or alg-none |
| 5.4.1 key sizes | JWKS keys below 2048 bits (RSA) or 224 bits (EC) are skipped when the key set is loaded | Tokens signed with a skipped key fail as unknown-kid; permdock doctor warns about the skipped keys |
| RFC 8725 practices | alg comes from the allow-list, never from the header alone; kid selects a key from the configured JWKS only; jku, x5u and jwk headers are ignored | Anonymous subject |
Two consequences follow from the fail-closed rule:
- Every row above that ends in an anonymous subject leaves
can()returningfalseandassertproducing the 401-class Problem Details the authentication layer defines; the policy never sees a half-verified principal. See authentication. - The only row that produces a
deniedDecision is requirement 4. RFC 6750 section 3.1 asks forinsufficient_scopewhen the token lacks authority, and PermDock has the information to say what would have sufficed:Decision.alternativesbecomes thescopehint inWWW-Authenticateand thealternativesmember of the Problem Details body. The same field drives MCPscopeChallengestep-ups (MCP authorization), so an OAuth client and an MCP client are told the same thing in their own vocabulary.
permdock openapi
FAPI 2.0 has no OpenAPI representation of its own. permdock openapi marks operations protected by a FAPI 2.0 subject resolver with x-permdock-securityProfile: fapi2 next to the operation's security requirement, and declares the OAuth 2.0 security scheme with x-permdock-oauth2MetadataUrl pointing at the issuer's RFC 8414 metadata (or the native 3.2 oauth2MetadataUrl field). Gateways and client generators that understand the extension can enforce sender constraint at the edge; those that do not ignore it.
paths:
/posts/{id}:
patch:
security:
- oauth2: [post:update]
x-permdock-permissions: [post.update]
x-permdock-securityProfile: fapi2In OpenAPI 3.3 output
OpenAPI 3.3 is designing a native Security Profile construct with FAPI 2.0 as its worked example (OpenAPI Initiative Discussion #5304). permdock openapi emit --target 3.3 --profile fapi2 emits that pinned draft shape next to the extension (OpenAPI 3.3, watch list):
- a
type: profilesecurity scheme (permdockFapi2by default) whoseprofileMetadata.nameisfapi-20-security-profile, the name the proposal registers for this profile; PermDock never coins a registry name; profileMetadata.supportedParametersSchemapointing at the FAPI 2.0 parameter schema PermDock publishes, andprofileMetadata.serversfilled from the issuer's metadata URL;- one
components.securityProfileRequirementsentry per distinct scope set, listingtoken_endpoint_auth_methods: [private_key_jwt, tls_client_auth](the two client authentication methods FAPI 2.0 permits), thegrant_typesimplied by the configured flows, and the operation's scopes; x-permdock-securityProfile: fapi2on the scheme and every covered operation, exactly as on 3.1 and 3.2 output, so a consumer that ignores theprofilescheme type still sees the declaration.
components:
securitySchemes:
permdockFapi2:
type: profile
profileMetadata:
name: fapi-20-security-profile
supportedParametersSchema: https://permdock.com/schemas/security-profiles/fapi2.json
servers:
- name: default
url: https://auth.example.com/.well-known/oauth-authorization-server
securityProfileRequirements:
permdockFapi2PostUpdate:
securityScheme:
$ref: "#/components/securitySchemes/permdockFapi2"
token_endpoint_auth_methods: [private_key_jwt, tls_client_auth]
grant_types: [authorization_code]
scopes: [post:update]The resource-server rules PermDock enforces come from the Final FAPI 2.0 Security Profile, not from how OpenAPI describes it; where the two differ, the OpenID Foundation document wins and the OpenAPI output follows. When 3.3.0 is released the native construct replaces the draft shape and the extension stays on 3.1 and 3.2 targets.
Grant Management
Grant Management for OAuth 2.0 is a FAPI Working Group implementer's draft, born from PSD2 and the Australian Consumer Data Right, for "managing grants that represent the consent a data subject has given": a grant gets an identifier, a client can query, update, replace or revoke it, and an authorization request can refer to an existing grant instead of asking for consent again.
The shape lines up with two PermDock concepts:
- A grant, as a consent object, is what PermDock's
approval-requiredoutcome produces when a human approves: anapprovaltoken bound to permission key, resource id, subject and actor (approvals). A Grant Managementgrant_idis the authorization-server-side twin of that token. - The contents of a grant (scopes and
authorization_details) are exactlydelegation; agrant_idon an incoming token is therefore a pointer to the delegation the token carries.
PermDock tracks the draft and does not implement it: grant lifecycle is authorization-server work, and PermDock never mints or manages tokens. If the draft reaches final and authorization servers ship it, the candidate integration is recording grant_id in audit events and treating a revoked grant like a CAEP session-revoked event. See the watch list.
Out of scope: Message Signing
FAPI 2.0 Message Signing (signed authorization requests and responses via JAR and JARM, signed introspection responses, HTTP message signing for non-repudiation) is a separate final specification aimed at the client and authorization server. It changes nothing about how a resource server turns a token into a subject, so permdock/jwt neither requires nor verifies it. Deployments that need non-repudiation of API requests handle it in the HTTP layer before PermDock runs.
Mapping table
| FAPI 2.0 concept | PermDock concept |
|---|---|
| Resource server | The application process running createPermDock; permdock/jwt is its token-to-subject step |
Access token (sub, client_id, scope, authorization_details) | principal from sub and issuer claims, actor from client_id when it differs from the subject, delegation from scope and authorization_details |
Sender constraint (cnf.jkt, cnf.x5t#S256) | sender: 'dpop' or 'mtls'; the verified binding is recorded on the subject for audit |
| Authorization sufficiency (5.3.4 item 4) | permdock.decide(permission, data) |
RFC 6750 insufficient_scope | denied Decision rendered as WWW-Authenticate plus Problem Details with alternatives |
| RAR recommendation | delegation.authorizationDetails, one type per permission |
Grant Management grant_id | Approval token and delegation, tracking only |
| 5.4.1 algorithm list | The alg allow-list fixed by the profile: PS256, ES256, Ed25519 |
Sources
- FAPI 2.0 Security Profile (final), sections 5.3.4, 5.4.1 (including its note on a fully-specified Ed25519 algorithm) and note 5.
- RFC 9864, Fully-Specified Algorithms for JOSE and COSE, section 2.2 (
Ed25519,Ed448) and the deprecation of polymorphic identifiers. - OpenID Foundation specifications index for FAPI 2.0 Message Signing, the Attacker Model, FAPI CIBA and Grant Management for OAuth 2.0.
- OpenAPI Initiative newsletter, June 2026 for the 3.3 security-profile investigation.
- RFC 6750, RFC 8705, RFC 8725, RFC 9396 and RFC 9449 are referenced by number.
Related
- JWT adapter: the full
subjectFromJwtoption set, of whichprofile: 'fapi2'is one preset. - JOSE and OpenID Connect: the algorithm registry, RFC 9864 and the Discovery step that supplies
jwksandissuer. - Authentication: where token verification stops and permissions start.
- OAuth for agent delegation and delegation: RAR
authorization_detailsasdelegation. - Problem Details: the 403 body that carries
alternatives. - OpenAPI: where
x-permdock-securityProfilegoes and the nativetype: profilescheme emitted for 3.3 targets. - Standards watch list: why the 3.3 draft is emitted before release; Grant Management, rfc8725bis and RAR error remediation.
Last updated on
SCIM 2.0 (RFC 7643, RFC 7644, RFC 9865)
The SCIM 2.0 core schema and protocol, why it is the write side of PermDock memberships, and the identifier and tenancy rules PermDock takes from it. The receiver, its protocol subset and IdP dialects are on the SCIM adapter page.
MCP authorization
How the Model Context Protocol 2026-07-28 authorization model (OAuth 2.1 resource servers, scope challenges, RFC 8707, MRTR, CIMD, RFC 9207, Enterprise-Managed Authorization) maps onto permdock/mcp.