# Encrypt ID tokens, JARM and UserInfo

M7 signs each selected response, then encrypts the signed JWT for your
application's registered public RSA key. Your application retains the private
decryption key. The supported profile is **RSA-OAEP-256 with A256GCM**; signing
algorithms are configured independently. Access, refresh and session token
formats and their validation requirements remain unchanged.

This feature is deployed and advertised. On 2026-09-16, a dedicated PHP receiver
passed live encrypted ID-token login, `form_post.jwt` JARM, encrypted UserInfo,
strict refresh and authorization cancellation against the production provider.
That campaign used inline JWKS and RS256 signing. Remote JWKS, rotation and
other encrypted transports require acceptance in the consuming application.

## Register the public key and response policies

Use [Dynamic Registration](https://m7.org/docs/api/sso.user.m7.org/registration.md#response-encryption-policy) with
exactly one standard public `jwks` or `jwks_uri` source. For an existing app,
use the [application management API](https://m7.org/docs/api/api.user.m7.org/oauth-clients#response-encryption)
or [application registration settings](https://user.m7.org/members/settings/app-registration).
Management keeps response-encryption keys separate from authentication and JAR keys.

| Response | Algorithm field | Content-encryption field | Delivery |
| --- | --- | --- | --- |
| ID token | `id_token_encrypted_response_alg` | `id_token_encrypted_response_enc` | Compact JWE in `id_token` after authorization-code, refresh or device issuance |
| JARM | `authorization_encrypted_response_alg` | `authorization_encrypted_response_enc` | Compact JWE in the sole `response` field for `query.jwt`, `form_post.jwt`, `fragment.jwt` or `jwt` |
| Signed UserInfo | `userinfo_encrypted_response_alg` | `userinfo_encrypted_response_enc` | Compact JWE with `Content-Type: application/jwt` and no-store headers |

Both fields absent leaves that artifact's encryption off. Set the pair explicitly
to `RSA-OAEP-256` and `A256GCM` to require it. Partial or unsupported pairs fail;
there is no implicit content-encryption default in this profile. UserInfo also
requires `userinfo_signed_response_alg=RS256` or `RS512`. JARM requires an explicit
registered JWT response mode; a plain mode or omitted `response_mode` is rejected.

For example, add this metadata to a complete Dynamic Registration request:

```json
{
  "jwks_uri": "https://app.example.com/.well-known/encryption-jwks.json",
  "id_token_encrypted_response_alg": "RSA-OAEP-256",
  "id_token_encrypted_response_enc": "A256GCM",
  "authorization_encrypted_response_alg": "RSA-OAEP-256",
  "authorization_encrypted_response_enc": "A256GCM",
  "userinfo_signed_response_alg": "RS256",
  "userinfo_encrypted_response_alg": "RSA-OAEP-256",
  "userinfo_encrypted_response_enc": "A256GCM",
  "response_modes": ["form_post.jwt"]
}
```

The registered policy is authoritative. Authorization, token and UserInfo requests
cannot replace recipient keys, change algorithms or disable required encryption.
Register only public keys; encryption does not change how the client authenticates.
Every recipient, including a public client registered with `none`, must control
and protect the matching private decryption key. Registering an encryption key
does not establish confidential-client authentication. Do not embed an
application-wide private key in browser assets or distributed native binaries.
Browser/native recipient provisioning and custody require their own acceptance;
the tested receiver is server-side PHP.
The PHP web session/profile bridge requires confidential client authentication
for its introspection calls; a public client with `none` cannot use that path.

### Choose a key source

- Use `jwks_uri` when your application can operate a reliable public HTTPS key
  endpoint. It lets you publish replacement public keys at the registered URL.
  OpenID Connect registration directs clients that can use `jwks_uri` to use it
  instead of inline `jwks`. See [client metadata](https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata).
- Inline `jwks` is supported when hosting that endpoint is unavailable. Replacing
  its keys requires an explicit registration update through app management.
  The live inline-key fixture establishes coverage, not a product-wide default.

For a remote source, endpoint availability becomes an issuance dependency:
M7 fetches it for each issuance and fails closed on retrieval or validation
failure. Test the hosted endpoint, key transition and outage behavior before
requiring encryption. Both source types need deliberate key-ID selection and
retained private keys, as described below.

## Key requirements and rotation

A JWKS contains 1–32 keys and is limited to 64 KiB. Encryption recipients use
ordinary RSA public keys with 2048–8192-bit moduli and unique, nonempty `kid`
values. Public `n` and `e` use canonical unpadded base64url integers. Optional
`use` must be `enc`, `alg` must be `RSA-OAEP-256`, and `key_ops` may contain
`encrypt` and/or `wrapKey`. Private or symmetric material is rejected, including
in keys that would not be selected. Duplicate IDs are rejected across the set.

For supported combined authentication/encryption registration, mark signing keys
`use=sig` and recipients `use=enc`; each subset must satisfy its own registration
rules. A signing key is never an implicit encryption fallback. Every encryption
candidate must be valid; a set with no usable recipient fails.

Among eligible recipients, the **bytewise lexicographically smallest `kid` wins**,
regardless of array order. Choose IDs deliberately when changing the preferred
recipient. Retain old private keys until all responses and outstanding
authorization/cancellation transactions have expired. Outstanding cancellation
transactions may retain their previous policy and source for up to ten minutes.

Remote JWKS must use a public DNS HTTPS hostname on port 443, without credentials,
fragments, IP literals or local names. Registration checks URL syntax; issuance
fetches and validates the actual key set. Fetches require verified TLS and public
destination addresses, disallow redirects, and have a 3-second connection and
5-second total timeout. Only successful JSON/JWKS responses are accepted. Remote
keys are fetched for each issuance; an unavailable host causes issuance failure.

## Receive and validate

1. Select local private keys and expected algorithms from trusted application
   configuration. Never accept key paths or URLs from the response.
2. Authenticate and decrypt the five-part compact JWE. Check its protected
   algorithm, content-encryption algorithm, recipient key ID and nested-JWT type.
3. Verify the inner signed JWT using the provider's trusted signing-key contract.
4. Validate issuer, audience, time and artifact-specific claims before acting:
   saved state for JARM, saved nonce for the ID token, and the established subject
   for UserInfo. Decryption alone does not authenticate the identity.

Use a receiving library that supports this nested profile. The
[PHP SDK encryption guide](https://m7.org/docs/sdk/m7-identity/response-encryption)
distinguishes tested candidates from stable downloadable versions and explains
its optional native runtime. A client that only parses three-part JWTs must be
upgraded before encryption is required.

## Confidentiality boundary

Encrypted JARM conceals the code, state and error fields carried through the
browser from parties that lack the recipient key. `form_post.jwt` carries the
response in a POST body rather than a callback URL; ciphertext and POST bodies
can still be captured by diagnostics. Continue to exclude responses from logs.
In the supported authorization-code flow, ID tokens arrive from the token
endpoint and UserInfo is fetched separately; those are distinct from the
front-channel JARM response.

Keep HTTPS, PKCE, redirect validation and all signature/claim checks enabled.
Encryption does not protect plaintext after the recipient decrypts it or
compensate for a compromised receiving application. Inbound encrypted Request
Objects remain unsupported. No OpenID or FAPI conformance claim follows from
this feature or its scoped live tests; profile validation and certification
remain separate.

## Failure behavior and size limits

Invalid policy, unusable recipients, unsafe/unavailable JWKS and encryption
failures fail closed. Required encryption never retries as readable JWS, JSON
UserInfo or a plaintext authorization redirect. Encrypted JARM carries no
plaintext code, state or error siblings; success and eligible cancellation/error
results are handled only after the envelope and normal JARM validation pass.
If safe delivery is impossible, the failure stays on M7.

SSO limits encrypted JARM to 32 KiB, compared with 16 KiB for signed-only JARM.
Encrypted ID tokens and UserInfo are limited to 256 KiB. Receivers, proxies and
browsers can impose lower limits. Prefer `form_post.jwt` for larger authorization
responses and check the receiver's complete transport budget.

## Application acceptance

Enable one artifact at a time on the provider and receiver, then exercise all
three together. Check login, refresh, UserInfo subject binding and cancellation.
For the paths your app uses, also test wrong keys, tampering, plaintext downgrade,
rotation, remote-key outages, other JWT response modes and device/ACK lifecycles.
Retain only safe status evidence; never log tokens, decrypted claims or private
keys. The scoped production campaign above does not certify these additional
application-specific cases or OpenID conformance.
