# FAPI 2.0 Security Profile

Source: https://permdock.com/docs/standards/fapi-2

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 [#what-it-is]

The [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-security-profile-2_0-final.html) 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 [#534-requirements-for-resource-servers]

A FAPI 2.0 resource server:

1. Accepts access tokens only in the HTTP `Authorization` header, either as a bearer token per RFC 6750 section 2.1 or as a DPoP-bound token per RFC 9449 section 7.1.
2. Does not accept access tokens in query parameters (RFC 6750 section 2.3).
3. Verifies the token's validity, integrity, expiration and revocation status.
4. 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`).
5. Supports and verifies sender-constrained tokens, via mutual TLS (RFC 8705) and/or DPoP (RFC 9449).

### 5.4.1 Cryptography [#541-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](https://www.rfc-editor.org/rfc/rfc9864.html) (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](/docs/standards/jose)).

### The RAR recommendation [#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](/docs/standards/oauth-agent-delegation) and [delegation](/docs/security/delegation).

## Why it matters for PermDock [#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 [#how-permdock-uses-it]

### `permdock/jwt` with `profile: 'fapi2'` [#permdockjwt-with-profile-fapi2]

```ts
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](/docs/adapters/mcp) publishes it where MCP does.

### What PermDock enforces [#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`](/docs/adapters/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()` returning `false` and `assert` producing the 401-class Problem Details the authentication layer defines; the policy never sees a half-verified principal. See [authentication](/docs/concepts/authentication).
* The only row that produces a `denied` Decision is requirement 4. RFC 6750 section 3.1 asks for `insufficient_scope` when the token lacks authority, and PermDock has the information to say what would have sufficed: `Decision.alternatives` becomes the `scope` hint in `WWW-Authenticate` and the `alternatives` member of the [Problem Details](/docs/standards/problem-details) body. The same field drives MCP `scopeChallenge` step-ups ([MCP authorization](/docs/standards/mcp-authorization)), so an OAuth client and an MCP client are told the same thing in their own vocabulary.

### `permdock openapi` [#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.

```yaml
paths:
  /posts/{id}:
    patch:
      security:
        - oauth2: [post:update]
      x-permdock-permissions: [post.update]
      x-permdock-securityProfile: fapi2
```

### In OpenAPI 3.3 output [#in-openapi-33-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](/docs/standards/openapi), [watch list](/docs/standards/watch-list)):

* a `type: profile` security scheme (`permdockFapi2` by default) whose `profileMetadata.name` is `fapi-20-security-profile`, the name the proposal registers for this profile; PermDock never coins a registry name;
* `profileMetadata.supportedParametersSchema` pointing at the FAPI 2.0 parameter schema PermDock publishes, and `profileMetadata.servers` filled from the issuer's metadata URL;
* one `components.securityProfileRequirements` entry per distinct scope set, listing `token_endpoint_auth_methods: [private_key_jwt, tls_client_auth]` (the two client authentication methods FAPI 2.0 permits), the `grant_types` implied by the configured flows, and the operation's scopes;
* `x-permdock-securityProfile: fapi2` on the scheme and every covered operation, exactly as on 3.1 and 3.2 output, so a consumer that ignores the `profile` scheme type still sees the declaration.

```yaml
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]

[Grant Management for OAuth 2.0](https://openid.net/developers/specs/) 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-required` outcome produces when a human approves: an `approval` token bound to permission key, resource id, subject and actor ([approvals](/docs/security/approvals)). A Grant Management `grant_id` is the authorization-server-side twin of that token.
* The contents of a grant (scopes and `authorization_details`) are exactly `delegation`; a `grant_id` on 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](/docs/standards/watch-list).

## Out of scope: Message Signing [#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 [#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 [#sources]

* [FAPI 2.0 Security Profile (final)](https://openid.net/specs/fapi-security-profile-2_0-final.html), 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](https://www.rfc-editor.org/rfc/rfc9864.html), section 2.2 (`Ed25519`, `Ed448`) and the deprecation of polymorphic identifiers.
* [OpenID Foundation specifications index](https://openid.net/developers/specs/) for FAPI 2.0 Message Signing, the Attacker Model, FAPI CIBA and Grant Management for OAuth 2.0.
* [OpenAPI Initiative newsletter, June 2026](https://www.openapis.org/blog/2026/06/09/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 [#related]

* [JWT adapter](/docs/adapters/jwt): the full `subjectFromJwt` option set, of which `profile: 'fapi2'` is one preset.
* [JOSE](/docs/standards/jose) and [OpenID Connect](/docs/standards/openid-connect): the algorithm registry, RFC 9864 and the Discovery step that supplies `jwks` and `issuer`.
* [Authentication](/docs/concepts/authentication): where token verification stops and permissions start.
* [OAuth for agent delegation](/docs/standards/oauth-agent-delegation) and [delegation](/docs/security/delegation): RAR `authorization_details` as `delegation`.
* [Problem Details](/docs/standards/problem-details): the 403 body that carries `alternatives`.
* [OpenAPI](/docs/standards/openapi): where `x-permdock-securityProfile` goes and the native `type: profile` scheme emitted for 3.3 targets.
* [Standards watch list](/docs/standards/watch-list): why the 3.3 draft is emitted before release; Grant Management, rfc8725bis and RAR error remediation.
