Skip to content

RFC 8705 Support - #8

Open
ckoval7 wants to merge 8 commits into
maicol07:mainfrom
ckoval7:feat/rfc8705-mutual-tls
Open

RFC 8705 Support#8
ckoval7 wants to merge 8 commits into
maicol07:mainfrom
ckoval7:feat/rfc8705-mutual-tls

Conversation

@ckoval7

@ckoval7 ckoval7 commented Aug 4, 2026

Copy link
Copy Markdown

RFC 8705 - Mutual TLS Client Auth against an IdP.

What was added

Clients can now authenticate with a certificate presented during the TLS handshake instead of a shared secret. Both RFC 8705 methods are supported:

  • ClientAuthMethod::TLS_CLIENT_AUTH - certificate issued by a CA the provider trusts
  • ClientAuthMethod::SELF_SIGNED_TLS_CLIENT_AUTH - provider holds the certificate itself
$oidc = new Client(
    provider_url: 'https://id.example.com',
    client_id: 'ClientIDHere',
    // No client_secret — the certificate authenticates the client
    redirect_uri: 'https://example.com/callback.php',
    token_endpoint_auth_method: ClientAuthMethod::TLS_CLIENT_AUTH,
    mtls_certificate: new MutualTlsCertificate(
        certificate_path: '/path/to/client.crt',
        private_key_path: '/path/to/client.key',
    ),
);
$oidc->authenticate();

Certificate-bound access tokens: verifyCertificateBinding() checks a token's cnf.x5t#S256 thumbprint against the configured certificate. It returns false if the token isn't bound at all, throws if it's bound to someone else's certificate. The authoritative check still belongs to the resource server; this is a client-side sanity check.

mtls_endpoint_aliases handling: Providers may serve mTLS endpoints on a separate host/port that requests a client certificate. Those aliases are picked up from discovery and used automatically for token, userinfo, introspection, revocation, and registration. Certificate-bound tokens need the aliased token endpoint too, even when the client authenticates with a secret, that case is handled.

Dynamic registration now sends token_endpoint_auth_method and tls_client_certificate_bound_access_tokens, and no longer errors out when the provider returns no client_secret (mTLS clients don't get one).

Other changes

cert_path used to only take effect when verify_ssl was false. The documented way to trust a private CA (cert_path + verify_ssl: false) actually disabled verification entirely and silently trusted any certificate. A CA bundle is now always honored, and verification is only off when verify_ssl: false and no bundle is set.

Tests

31 new Pest tests in tests/Feature/MutualTlsTest.php, covering thumbprint computation (including certs with a bundled key), Guzzle option construction, endpoint alias resolution, secret suppression, registration, binding verification, and the verify semantics above. Test certificates are generated at runtime via openssl.

This is tested and working with Keycloak.

Summary by CodeRabbit

  • New Features

    • Added RFC 8705 mutual-TLS authentication for token, introspection, revocation, and UserInfo requests.
    • Added support for certificate-bound access tokens and token binding verification.
    • Added client certificate configuration, private keys, passphrases, and certificate thumbprints.
    • Added automatic discovery and dynamic registration of mutual-TLS endpoints and capabilities.
  • Documentation

    • Expanded guidance on private-CA certificates, mutual TLS, certificate-bound tokens, and SSL verification risks.

ckoval7 and others added 6 commits August 1, 2026 20:08
Adds support for OAuth 2.0 Mutual-TLS Client Authentication and
Certificate-Bound Access Tokens, letting a client authenticate with a
certificate presented during the TLS handshake instead of a shared secret.

- Added `TLS_CLIENT_AUTH` and `SELF_SIGNED_TLS_CLIENT_AUTH` to `ClientAuthMethod`, with an `isMutualTls()` helper.
- Added the `MutualTlsCertificate` value object, which carries the certificate, private key and optional passphrase, exposes the Guzzle request options that present it during the handshake, and computes the base64url SHA-256 thumbprint used in the `cnf` / `x5t#S256` claim.
- Added the `MutualTls` trait, which resolves the mutual-TLS endpoint aliases, suppresses the client secret when the certificate authenticates the client, and verifies that an access token is bound to the configured certificate.
- Suppressed the client secret across every token endpoint request: authorization code, refresh, token exchange, introspection and revocation. The latter two previously hardcoded Basic authentication.
- Parsed `mtls_endpoint_aliases` and `tls_client_certificate_bound_access_tokens` during auto discovery, and used the aliased endpoints automatically.
- Registered the mutual-TLS authentication method during dynamic registration, and stopped requiring a client secret that mutual-TLS clients do not have.
- Selected an announced mutual-TLS method only when a certificate is configured, so existing secret-based clients keep their current behaviour.
- Added a self-contained Docker test stack (Keycloak, certificate generation, `docker/run-tests.sh`) covering the happy paths plus the negative cases: a request without a certificate, a certificate with a mismatched subject DN, and a certificate-bound token replayed without its certificate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGmjsaiMbu1LxgCzWB9Djx
…certificate

`cert_path` was only applied when `verify_ssl` was `false`, because
`($verify_ssl ?: $cert_path) ?? false` short-circuits to `true` whenever
`verify_ssl` is true. A CA bundle was therefore silently ignored in the
default configuration, and the only way to make it take effect was to also
disable verification — which is exactly what the README documented.

The bundle is now always used when one is given, so a private CA can be
trusted without turning verification off:

| verify_ssl | cert_path | before     | after      |
|------------|-----------|------------|------------|
| true       | null      | true       | true       |
| true       | bundle    | true       | bundle     |
| false      | null      | false      | false      |
| false      | bundle    | bundle     | bundle     |

- Replaced the expression with `$this->cert_path ?? $this->verify_ssl`, so verification is only disabled when it is turned off *and* no bundle is given. No configuration verifies less than it did before.
- Updated Example 3, the Development Environments section and the constructor PHPDoc, which all documented `verify_ssl: false` as the way to enable `cert_path`.
- Added regression tests covering the four combinations, including one asserting that no configuration verifies less than the original expression did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGmjsaiMbu1LxgCzWB9Djx
The docker/ stack is local development tooling rather than part of the
distributed package, so remove it from version control and ignore it. The
files stay on disk for local RFC 8705 testing.

Drop the README section that pointed at ./docker/run-tests.sh, since that
script is no longer present in a fresh clone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Port the mutual-TLS checks from the local Docker harness into tests/Feature so
the RFC 8705 support is covered by the repo's own suite. The tests generate
their own certificates and configure clients without a provider_url, so they
need neither fixture files nor a running provider.

Covered: certificate request options and thumbprints, auth method selection,
dropping the client secret under mutual TLS, endpoint aliases, certificate
bound token verification, and that a CA bundle keeps verifying the provider
even alongside verify_ssl: false.

The handshake behaviour of a real provider — a token request being rejected
without a certificate, and a bound token being refused at userinfo — still
needs a live mutual-TLS provider and is not covered here.

Add a setProperty() reflection helper to TestCase alongside getProperty().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The docker/ ignore rule only matters to this working copy, so keep it out
of the committed .gitignore and move it to .git/info/exclude. The stack is
still ignored locally; the shared ignore file returns to upstream defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…applies

Two RFC 8705 endpoint-alias bugs.

`tls_client_certificate_bound_access_tokens` was parsed from discovery and
sent during registration, but never consulted when resolving endpoints:
`mtlsEndpoint()` keyed solely off the presence of a certificate. A client
authenticating with a secret while asking for certificate-bound tokens
therefore kept using the regular token endpoint, where no certificate is
requested during the handshake, so the token could not be bound.

Resolution now goes through `usesMutualTls()`, covering both reasons the
aliases exist: mutual-TLS client authentication, and certificate-bound
access tokens. This also stops a certificate that is used for neither
purpose from diverting endpoints, which the old check did.

`register()` posted to `registration_endpoint` directly while the other
four endpoints were resolved through the aliases. It is where a
mutual-TLS client announces its `token_endpoint_auth_method`, so it is
the call that most needs the certificate presented.

Verified against the local Keycloak stack (29/29) and the Pest suite,
including an end-to-end check that `register()` posts to the aliased URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ckoval7, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 30 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: caa41587-5012-425a-98a3-8ef0115c1ac5

📥 Commits

Reviewing files that changed from the base of the PR and between ac51d0e and b5b7306.

📒 Files selected for processing (6)
  • README.md
  • src/MutualTlsCertificate.php
  • src/Traits/MutualTls.php
  • src/Traits/Token.php
  • tests/Feature/MutualTlsTest.php
  • tests/Pest.php
📝 Walkthrough

Walkthrough

The client adds RFC 8705 mutual-TLS authentication and certificate-bound access-token support. It adds certificate handling, endpoint discovery and registration support, mutual-TLS token routing, certificate verification, tests, and documentation.

Changes

Mutual-TLS support

Layer / File(s) Summary
Certificate and authentication contracts
src/ClientAuthMethod.php, src/MutualTlsCertificate.php, src/Traits/MutualTls.php, tests/Feature/MutualTlsTest.php
Adds mutual-TLS authentication methods, certificate validation, Guzzle TLS options, certificate thumbprints, and authentication tests.
Endpoint discovery and registration
src/Traits/AutoDiscovery.php, src/Traits/DynamicRegistration.php, src/Traits/MutualTls.php, tests/Feature/MutualTlsTest.php
Processes mutual-TLS discovery metadata, resolves endpoint aliases, and sends mutual-TLS registration parameters.
Client and token request integration
src/Client.php, src/Traits/Token.php, src/Traits/MutualTls.php, tests/Feature/MutualTlsTest.php, tests/Pest.php, tests/TestCase.php
Applies certificate authentication to client, token, introspection, revocation, and UserInfo requests. It verifies certificate-bound token claims and preserves Basic authentication fallbacks.
Documentation and security guidance
README.md
Documents RFC 8705 configuration, certificate verification, endpoint aliases, certificate-bound tokens, and development settings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MutualTls
  participant Token
  participant Provider
  Client->>MutualTls: resolve authentication method and endpoint
  MutualTls->>Token: apply certificate request options
  Token->>Provider: send token, introspection, or revocation request
  Provider-->>Token: return OAuth response
  Token-->>Client: return parsed result
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding RFC 8705 mutual-TLS support.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@what-the-diff

what-the-diff Bot commented Aug 4, 2026

Copy link
Copy Markdown

PR Summary

  • Included Mutual-TLS Client Authentication reference
    A new reference for Mutual-TLS Client Authentication has been added in the application's ReadMe file to improve its documentation.

  • Updated application security guidance
    The security guidance has been updated to incorporate the usage of a CA bundle for self-signed certificates and also provides an explanation for the cert_path usage.

  • Introduction of new examples
    Two new examples have been added to the ReadMe file, one demonstrating Mutual-TLS client authentication and the other explaining certificate-bound access tokens.

  • Enhanced the Client class
    The Client class has been modified to support mutual-TLS configurations. It now includes a new MutualTlsCertificate trait, accepts parameters for mutual-TLS, and has an updated token request logic for easy mutual-TLS authentication.

  • Introduction of a new class MutualTlsCertificate
    A new class, MutualTlsCertificate, has been added to contain the details for handling client certificates used in mutual-TLS authentication.

  • Added support for mutual-TLS and certificate-bound tokens
    The PR also introduces support for mutual-TLS endpoint aliases and certificate-bound access tokens in dynamic registration functionality.

  • Incorporation of a new MutualTls trait
    A new MutualTls trait has been added in the application's code to implement Mutual-TLS Client Authentication and Certificate-Bound Access Tokens along with methods for various mutual TLS operations.

  • Updated token request methods
    The token request methods have been updated to support mutual TLS operations.

  • Added functionality to test mutual TLS
    The test suite has been updated to create self-signed certificates for mutual TLS testing and initializing a client configured for mutual-TLS authentication.

  • Modified TestCase for testing purposes
    The TestCase has been enhanced with a new setProperty() method to deliver more in-depth testing capabilities by allowing to set protected/private properties of a class.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/Traits/DynamicRegistration.php (1)

35-46: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Note the extra RFC 8705 registration metadata.

RFC 8705 requires more than the authentication method at registration. tls_client_auth needs exactly one subject identifier, for example tls_client_auth_subject_dn or tls_client_auth_san_dns (section 2.1.2). self_signed_tls_client_auth needs jwks or jwks_uri (section 2.2.2). A caller can supply these through $params, so registration still works. Document that requirement in the README section for dynamic registration, so users do not get an opaque provider error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Traits/DynamicRegistration.php` around lines 35 - 46, Update the README
section documenting dynamic registration to explain the RFC 8705 metadata
requirements: tls_client_auth must include exactly one subject identifier such
as tls_client_auth_subject_dn or tls_client_auth_san_dns, while
self_signed_tls_client_auth must include jwks or jwks_uri. Note that callers
provide these fields through the registration parameters.
tests/Feature/MutualTlsTest.php (1)

53-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The tempnam file is left behind.

tempnam() creates a file at the returned path. Appending .pem produces a different path, so the created file is never used and never deleted. Each test run leaks one empty temp file. Reuse the temp path directly, or build the bundle inside the existing test directory used by certificate().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Feature/MutualTlsTest.php` around lines 53 - 59, Update the bundle
setup in the test “certificate without a separate private key omits the ssl_key
option” to avoid creating an unused tempnam file: reuse the path returned by
tempnam directly when writing the PEM bundle, or create it within the existing
certificate test directory. Ensure the temporary bundle is cleaned up after the
test.
tests/Pest.php (1)

67-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tighten the generated key file and check the OpenSSL results.

mkdir uses 0700, but file_put_contents writes the private key with the default umask, usually 0644. Restrict the key file to match the directory intent. Also, openssl_pkey_new, openssl_csr_new, and openssl_csr_sign return false on failure. An unchecked failure surfaces later as a confusing certificate error instead of a clear test failure.

♻️ Proposed change
     if (!isset($certificates[$name])) {
         $key = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
+        if ($key === false) {
+            throw new RuntimeException('Unable to generate a test key pair: ' . openssl_error_string());
+        }
         $csr = openssl_csr_new(['commonName' => $name], $key, ['digest_alg' => 'sha256']);
         openssl_x509_export(openssl_csr_sign($csr, null, $key, 1, ['digest_alg' => 'sha256']), $certificate);
         openssl_pkey_export($key, $private_key);
@@
         file_put_contents("$directory/$name.crt", $certificate);
         file_put_contents("$directory/$name.key", $private_key);
+        chmod("$directory/$name.key", 0600);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Pest.php` around lines 67 - 89, Update certificate() to validate the
results of openssl_pkey_new, openssl_csr_new, and openssl_csr_sign, failing
immediately with a clear test error if any returns false. After writing the
private key in certificate(), restrict its file permissions to 0600, while
preserving the existing certificate caching and MutualTlsCertificate
construction.
src/Traits/MutualTls.php (1)

58-70: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Non-enum entries in token_endpoint_auth_methods_supported cause a TypeError.

The closure declares ClientAuthMethod $method. token_endpoint_auth_methods_supported is a public mutable array, and a caller can pass method names as strings. AutoDiscovery normalizes discovered values, but constructor input is not normalized. Accept both shapes to keep the selection tolerant.

♻️ Proposed change
         return collect($this->token_endpoint_auth_methods_supported)
-            ->first(static fn (ClientAuthMethod $method): bool => $method->isMutualTls());
+            ->map(static fn (ClientAuthMethod|string $method): ?ClientAuthMethod => $method instanceof ClientAuthMethod
+                ? $method
+                : ClientAuthMethod::tryFrom($method))
+            ->first(static fn (?ClientAuthMethod $method): bool => $method?->isMutualTls() === true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Traits/MutualTls.php` around lines 58 - 70, Update getClientAuthMethod so
its token_endpoint_auth_methods_supported selection tolerates both
ClientAuthMethod instances and string method names in the public mutable array.
Remove the closure’s strict ClientAuthMethod parameter type, validate or
normalize each entry before calling isMutualTls, and preserve returning the
first mutual-TLS method or null.
src/MutualTlsCertificate.php (1)

87-99: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Guard the preg_replace result before decoding.

preg_replace returns null when the PCRE engine fails, for example on backtrack limits with a large bundle. base64_decode(null) then raises a deprecation in PHP 8.1+ and returns an empty string. Cast the result so the failure path stays a clean OIDCClientException.

♻️ Proposed change
-            $der = base64_decode(preg_replace('/\s+/', '', $matches[1]), true);
+            $der = base64_decode((string) preg_replace('/\s+/', '', $matches[1]), true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/MutualTlsCertificate.php` around lines 87 - 99, Guard the
whitespace-stripping preg_replace result in the certificate parsing flow before
passing it to base64_decode, using a string fallback when preg_replace returns
null. Keep the existing first-certificate extraction behavior and ensure
failures continue to reach the clean OIDCClientException path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Traits/Token.php`:
- Around line 93-99: Update the introspection and revokeToken authentication
flows to use applyMutualTlsClientAuth() instead of relying on
usesMutualTlsClientAuth() before falling back to Basic authentication. This must
raise the established OIDCClientException when mutual-TLS is configured without
a certificate, while preserving certificate-based client authentication and
existing Basic-auth behavior for non-mutual-TLS clients.

In `@tests/Feature/MutualTlsTest.php`:
- Around line 154-160: Update the `$data['client_id']` assertion in the `no
client secret is sent under mutual tls` test to use a null-coalescing access,
preserving the expected `mtls-client` value while avoiding PHPStan’s
offsetAccess.notFound error after the by-reference `invokeMethod` call.

---

Nitpick comments:
In `@src/MutualTlsCertificate.php`:
- Around line 87-99: Guard the whitespace-stripping preg_replace result in the
certificate parsing flow before passing it to base64_decode, using a string
fallback when preg_replace returns null. Keep the existing first-certificate
extraction behavior and ensure failures continue to reach the clean
OIDCClientException path.

In `@src/Traits/DynamicRegistration.php`:
- Around line 35-46: Update the README section documenting dynamic registration
to explain the RFC 8705 metadata requirements: tls_client_auth must include
exactly one subject identifier such as tls_client_auth_subject_dn or
tls_client_auth_san_dns, while self_signed_tls_client_auth must include jwks or
jwks_uri. Note that callers provide these fields through the registration
parameters.

In `@src/Traits/MutualTls.php`:
- Around line 58-70: Update getClientAuthMethod so its
token_endpoint_auth_methods_supported selection tolerates both ClientAuthMethod
instances and string method names in the public mutable array. Remove the
closure’s strict ClientAuthMethod parameter type, validate or normalize each
entry before calling isMutualTls, and preserve returning the first mutual-TLS
method or null.

In `@tests/Feature/MutualTlsTest.php`:
- Around line 53-59: Update the bundle setup in the test “certificate without a
separate private key omits the ssl_key option” to avoid creating an unused
tempnam file: reuse the path returned by tempnam directly when writing the PEM
bundle, or create it within the existing certificate test directory. Ensure the
temporary bundle is cleaned up after the test.

In `@tests/Pest.php`:
- Around line 67-89: Update certificate() to validate the results of
openssl_pkey_new, openssl_csr_new, and openssl_csr_sign, failing immediately
with a clear test error if any returns false. After writing the private key in
certificate(), restrict its file permissions to 0600, while preserving the
existing certificate caching and MutualTlsCertificate construction.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 32a1659a-576a-40b0-9fa0-c18e7e7f0008

📥 Commits

Reviewing files that changed from the base of the PR and between 40c6ec3 and ac51d0e.

📒 Files selected for processing (11)
  • README.md
  • src/Client.php
  • src/ClientAuthMethod.php
  • src/MutualTlsCertificate.php
  • src/Traits/AutoDiscovery.php
  • src/Traits/DynamicRegistration.php
  • src/Traits/MutualTls.php
  • src/Traits/Token.php
  • tests/Feature/MutualTlsTest.php
  • tests/Pest.php
  • tests/TestCase.php

Comment thread src/Traits/Token.php
Comment thread tests/Feature/MutualTlsTest.php
ckoval7 and others added 2 commits August 4, 2026 16:11
Introspection and revocation used usesMutualTlsClientAuth(), which is false when
a mutual-TLS method is configured without a certificate. Both then fell back to
Basic authentication with a secret a mutual-TLS client does not have, and the
provider answered with an opaque 401. They now go through
applyMutualTlsClientAuth() like refreshToken() and requestTokens() already did,
so the misconfiguration raises a clear OIDCClientException instead. The
client_id override both methods accept is preserved.

getClientAuthMethod() typed its closure parameter as ClientAuthMethod, but
token_endpoint_auth_methods_supported is a public array typed only by docblock
and constructor input is never normalised — only discovered values are. Method
names passed as strings raised a TypeError, so entries are now normalised
through tryFrom().

preg_replace() returns null when the PCRE engine fails, for example on a
backtrack limit with a large bundle. base64_decode(null) is deprecated on the
PHP versions this package supports and yields an empty string, so the result is
cast to keep the clean OIDCClientException path.

Tests: tempnam() creates a file, so appending '.pem' leaked an empty temp file
on every run; the returned path is reused and cleaned up instead. The openssl
calls behind the test certificates are checked for failure, the generated key is
restricted to 0600 to match the 0700 directory, and one assertion uses a
coalescing access that PHPStan can follow through invokeArgs().

README: document the RFC 8705 metadata callers must pass to register(), a
subject identifier for tls_client_auth (section 2.1.2) and jwks/jwks_uri for
self_signed_tls_client_auth (section 2.2.2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
revokeToken() called acceptJson() without asForm(), so the body went out as
JSON. RFC 7009 section 2.1 requires application/x-www-form-urlencoded, and a
provider that cannot read client_id from the body rejects the request with
invalid_client — revocation never worked for any client.

Verified against Keycloak: same endpoint and same credentials, only the encoding
differs.

    acceptJson only: 401 {"error":"invalid_client", ...}
    asForm + acceptJson: 200

This predates the RFC 8705 work, but the mutual-TLS tests are what reached the
revocation endpoint for the first time and surfaced it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant