Receiving encrypted PHP SDK responses

Included in token-php 0.1.3 and web-php 0.1.4. Earlier immutable SDK archives do not include this feature. Native crypto is optional for installing and using the SDK; enable encryption only with the runtime below. The separate native components have their own installation and distribution lifecycle.

On 2026-09-16, an isolated Apache/PHP receiver using the web candidate passed live encrypted ID-token login, form_post.jwt JARM, UserInfo, strict refresh and cancellation against the production provider. The campaign used inline RSA JWKS, RS256 signing and RSA-OAEP-256/A256GCM; it does not establish every transport, remote JWKS, rotation, device or ACK combination.

The broader September 20 acceptance covered the remaining encrypted JARM transports, HTTPS recipient JWKS, retained-key rotation and negative paths. Those provider/browser results do not substitute for checking the exact SDK release artifacts or broaden their runtime requirements.

The PHP SDK supports nested signed JWTs encrypted with RSA-OAEP-256/A256GCM for ID tokens, web JARM authorization responses, and signed UserInfo. Decryption is followed by signature and artifact-specific claims verification. Signing algorithms remain separately configured. Required encryption rejects readable JWS/JSON, unsupported algorithms, unknown recipients, and authentication failures. There is no plaintext retry or cryptographic fallback.

Runtime and installation

Ordinary supported signing flows keep PHP 8.1+ and existing dependency rules. RS256/RS512 use PHP OpenSSL without M7 Crypto. Existing native-only signing, including token-php HMAC verification, still requires its native dependency. Encryption alone requires PHP 8.4+, M7 PHP Crypto 0.3+, its matching M7 C Crypto 0.3+ backend, and PHP OpenSSL with aes-256-gcm. ext-m7crypto is suggested, never a mandatory Composer dependency.

The AlmaLinux C/PHP walkthrough provides a complete build, correctness/memory-test, installation and service activation sequence. Once loaded in the receiving PHP worker, the extension is detected by the SDK's runtime checks; recipients and encryption policy still require explicit configuration. The current receiver has no PHP 8.5 OpenSSL replacement backend; the guide's custom-port note describes possible future or local adaptations rather than a supported automatic fallback.

Follow the maintained public component guides in dependency order:

  1. Build and install M7 C Crypto, checking its release status and integrity.
  2. Build and load M7 PHP Crypto against that exact C library, checking its release status and integrity.

The initial validated extension target is PHP 8.4 NTS; other PHP versions and ZTS builds need separate validation. Broader/lower PHP support is later work. The current native 0.3.0 bundles are verified development candidates, with final release and public download verification pending. Their status is independent of the published Identity SDK ZIPs and base PHP 8.1+ installation.

The C encryption API and PHP encryption API describe primitive operations, ownership and failure behavior. M7 Crypto supplies RSA-OAEP-256; the SDK authenticates AES-GCM through PHP OpenSSL. No additional JOSE dependency is required.

Check the actual interpreter used by your receiving application:

php --ini
php --ri m7crypto
php -r 'echo PHP_VERSION, "\n", OPENSSL_VERSION_TEXT, "\n"; var_export(in_array("aes-256-gcm", openssl_get_cipher_methods(), true));'

For HTTP use, run an access-restricted temporary health probe in the application's PHP runtime. Call Recipient::prepare($policy, 'id_token', $recipient) there, then perform a test login through that same runtime. Report only runtime version and success/failure. Remove the probe afterward. CLI checks cannot certify Apache or FPM. An incompatible/missing C library may prevent the extension loading at PHP startup; SDK exceptions cannot intercept a PHP loader failure.

Public policy and local keys

Use exactly the existing registration field names:

Artifact Algorithm field Content-encryption field
ID token id_token_encrypted_response_alg id_token_encrypted_response_enc
JARM authorization_encrypted_response_alg authorization_encrypted_response_enc
Signed UserInfo userinfo_encrypted_response_alg userinfo_encrypted_response_enc

Omit both fields for an artifact to leave encryption off. Set both to RSA-OAEP-256 and A256GCM to require it. Partial, null, misspelled, and unsupported policy values fail closed. Configuration must come from the application, never from token headers, query parameters, discovery or a response body.

use M7\Identity\ResponseEncryption\Recipient;

$recipient = new Recipient([
    ['kid' => '2026-09-primary',
     'private_pem' => file_get_contents('/srv/private/recipient-2026-09.pem')],
    ['kid' => '2026-08-retained',
     'private_pem' => file_get_contents('/srv/private/recipient-2026-08.pem')],
]);
$policy = [
    'id_token_encrypted_response_alg' => 'RSA-OAEP-256',
    'id_token_encrypted_response_enc' => 'A256GCM',
];
Recipient::prepare($policy, 'id_token', $recipient);

Alternatively pass a local callable with no arguments returning that same list of {kid, private_pem} entries. It can read from your application's secret store. The incoming token never supplies a file path, URL or provider callback. The complete list must contain 1–32 unique, nonempty IDs and unencrypted ordinary RSA private PEMs with 2048–8192-bit keys. Public-only, weak, non-RSA, restricted RSA-PSS and unusable keys cannot decrypt. The entire list is validated, including retained keys; errors never reveal the key ID, PEM or backend exception.

Keep private keys outside the public document root with access limited to the application runtime. The recipient object redacts debug output and rejects serialization. Do not log configuration, tokens, exception arguments or claims; do not export your source key array. The SDK never sends private recipient keys in registration, requests, cookies, transactions or browser configuration. Signature-verification certificates/JWKS remain separate from recipient keys.

Register only the public RSA JWK/JWKS through the existing SSO registration facility (jwks or jwks_uri) and set the matching policy fields there. This is not a new registration API. UserInfo encryption requires registered RS256/RS512 signing. JARM requires an explicitly registered JWT response mode.

SSO chooses the bytewise lexicographically smallest eligible public kid. Choose IDs deliberately when rotating; retain old private keys until all responses and outstanding authorization/cancellation transactions have expired. Changing local keys must not remove a recipient still needed by an in-flight response.

token-php API

Use normal Composer autoloading, or the package's autoload.php for a ZIP install. The ordinary TokenParser still only accepts three-part JWS and establishes no trust by itself. Encrypted responses enter the response verifier first.

use M7\Identity\Token\IdToken\IdTokenVerifier;

$idOptions = [
    'issuer' => 'https://sso.user.m7.org',
    'client_id' => '<YOUR_CLIENT_ID>',
    'nonce' => $savedAuthorizationNonce,
    'response_encryption' => $policy,
    'recipient' => $recipient,
    'validation' => [
        'allowed_algs' => ['RS512'],
        'trusted_x5u_hosts' => ['sso.user.m7.org', 'id.m7.org'],
    ],
];
$verified = (new IdTokenVerifier())->verify($tokenResponse['id_token'], $idOptions);
$claims = $verified->claims();

IdTokenOptions::fromArray() also accepts expected_sub. validation uses the existing token validation/key-resolution API, including trusted cert_pem and native signing profiles. Issuer and audience are always bound to issuer and client_id, regardless of generic validation overrides. ID tokens require sub, integer iat/exp, valid issuer/audience/times, appropriate azp, and a matching saved nonce/subject when supplied. Supply the nonce you saved when starting authorization or device authorization; never take the expected nonce from the returned ID token. Refresh may omit a nonce; supply expected_sub from the established identity when available.

Pass the same array under id_token to RefreshTokenClient::exchange() or DeviceCodeClient::poll() (and the existing SDK facade methods forwarding those options). Verification runs before the successful grant package is released. Its client ID must match the grant's configured authentication client. Reports retain the original encrypted id_token; use IdTokenVerifier to obtain trusted claims. Access/refresh/session values and ACK state are unchanged. id_token options require a returned ID token, so use them only for OIDC grant contexts. Token-php has no authorization-code exchange client; use the standalone verifier for an application-owned code exchange. Without id_token options, historical signed ID-token passthrough remains; an unexpected JWE is rejected.

For UserInfo, pass response_encryption with the userinfo pair and recipient in UserInfoOptions/UserInfoClient::fetch() options. Preserve issuer, client_id and expected_sub. Successful application/jwt responses are unwrapped and verified through the existing issuer JWKS provider. Required UserInfo encryption rejects successful JSON responses. JwtUserInfoVerifier also accepts the same options for application-owned HTTP transport.

web-php setup

Register a confidential server-side client for the complete Web SDK session and profile flow, for example client_secret_basic. Its introspection calls reject public-client none even though encryption itself permits public clients. Keep the authentication secret separate from the private decryption key.

Install the package using bin/m7-identity-web-install as documented by the web package. The installed tree contains its own copy of the shared adapter and does not need token-php, provider repositories, or test files at runtime.

Set M7_RESPONSE_ENCRYPTION_CONFIG_FILE to an absolute PHP config file outside the public document root, readable by the server. It returns only registration policy fields and an optional recipient object. The SDK loads Recipient before requiring this file. Example file:

<?php
use M7\Identity\ResponseEncryption\Recipient;
return [
    '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_encrypted_response_alg' => 'RSA-OAEP-256',
    'userinfo_encrypted_response_enc' => 'A256GCM',
    'recipient' => new Recipient(static function (): array {
        return [['kid' => '2026-09-primary',
            'private_pem' => file_get_contents('/srv/private/recipient-2026-09.pem')]];
    }),
];

Set M7_RESPONSE_MODE=form_post.jwt explicitly and set M7_AUTHORIZATION_SIGNED_RESPONSE_ALG=RS256 for the registered JARM policy. M7_ID_TOKEN_SIGNED_RESPONSE_ALG is an optional ID-token/logout pin: omit it to verify the incoming supported algorithm, or set it to require that exact algorithm. There is no default ID-token/logout pin. The web ID-token and logout verifiers support RS256/RS512 through PHP OpenSSL and Ed25519 through ext-m7crypto with issuer-discovered OKP keys. JARM and UserInfo remain RS256/RS512; token-php retains its broader signing verification API. The login and signup routes check dependencies, then save public encryption policy, issuer/client/signing context and nonce in the server session. It never saves the recipient/private PEM. Callback JARM uses the saved policy and only releases code/state/errors after complete verification. The code exchange consumes the saved ID-token context once. Refresh uses current trusted configuration. Required encrypted ID tokens are verified before session cookies or pending ACK packages are written. They are not added to cookies or returned to browser code. Signed-only web ID-token behavior remains unchanged.

m7_bff_verify_id_token(), m7_callback_verify_jarm() and m7_bff_verify_userinfo_jwt() accept local response_encryption and recipient options for embedding/testing. The installed routes derive these from server configuration and transaction state, never browser input.

Limits and failures

The adapter caps outer JWE at 32 KiB for JARM and 256 KiB for ID token/UserInfo, protected-header encoding at 2048 bytes, and recovered signed JWT at 128 KiB (16 KiB for web JARM). Base64url must be canonical; protected metadata cannot contain duplicate names, compression, critical extensions, key URLs or embedded keys. CEKs must be 32 bytes, IVs 12 bytes, authentication tags 16 bytes.

The SDK intentionally retains stricter existing transport limits:

  • Web callback original encoded query/form/fragment: 16 KiB total, including the response= field. Same-origin relay wrapper: 64 KiB. Thus some provider JWEs below its 32 KiB ceiling will be rejected. Prefer form_post.jwt; do not enlarge only one browser/server limit independently.
  • Web UserInfo body: 128 KiB. Token-php UserInfo defaults to 64 KiB and has configurable max_response_bytes. Set it to 262144 to receive the provider ceiling; recovered JWS still cannot exceed 128 KiB.
  • Token-php grant JSON defaults to 128 KiB, configurable through max_response_bytes. Set 524288 to allow a 256 KiB ID token plus other fields. Web encrypted token exchanges cap the complete HTTP response at 512 KiB.
  • Access/refresh cookie and pending-envelope limits do not change. ID tokens are verified on the server without being added to cookie storage.

Preflight errors use response_encryption_unavailable, response_encryption_recipient_missing, response_encryption_policy_invalid, or response_encryption_keys_invalid. Invalid encrypted messages use encrypted_response_invalid. ID verification exposes id_token_validation_failed; web JARM/UserInfo retain their existing fixed failure results. Backends and local key providers are not chained into these errors. No decrypted claims are released on failure. Do not disable encryption to recover from an error: repair the key, runtime or registration mismatch and begin a new authorization if needed.

Verify your integration

Use a dedicated application and keep only status/validation outcomes as evidence. Confirm the PHP runtime used by actual HTTP requests, not only the CLI.

  1. Match issuer, client ID, exact redirect, registered signing algorithms and public recipient keys with protected local configuration.
  2. Require ID-token encryption on both sides; verify a fresh login and refresh. Validate the saved nonce, issuer and client audience before accepting identity.
  3. Require JARM, select form_post.jwt, and test success and cancellation. Check wrong-state, replay and tampered-envelope rejection before code exchange.
  4. Require signed UserInfo encryption, verify the established subject, and test that JSON or readable JWS cannot satisfy required encryption.
  5. Test all three together, then cover the transports and device/ACK lifecycle your application uses. Preserve complete pending packages where ACK applies.
  6. Rotate with old and new private recipients retained. Test unknown recipients, altered ciphertext/tags, remote-key outages and complete proxy/body limits.
  7. In an isolated runtime without M7 Crypto, ordinary supported signing remains usable; configured encryption must fail with response_encryption_unavailable. Restore the runtime instead of disabling a required policy.

Local interoperability and negative tests cover additional algorithm, key, runtime and protocol failures. The scoped live campaign above proves the stated path only. Candidate success does not add encryption to an older immutable ZIP or publish a new package version. The dedicated CLI executable does not acquire new recipient options merely because the Token/PHP library supports them.