Skip to content

[Client] Add client credentials, private_key_jwt and cross-app access - #669

Merged
chr-hertel merged 1 commit into
modelcontextprotocol:mainfrom
chr-hertel:client-auth-machine-grants
Nov 10, 2026
Merged

chr-hertel merged 1 commit into
modelcontextprotocol:mainfrom
chr-hertel:client-auth-machine-grants

Conversation

@chr-hertel

@chr-hertel chr-hertel commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Closes #324, closes #325 - last slice of the client authorization in chr-hertel#53.

$auth = OAuth::forServiceAccount('Acme Report Job', 'my-client-id', 'my-client-secret')
    ->setExpectedIssuer('https://auth.example.com')
    ->setScopes('mcp:read')
    ->build();

Compared to the prototype, a client with only a key no longer falls back to Basic with an empty secret, a service account needs a secret or a key and never authenticates as a public client, the identity provider's token endpoint has to be https or loopback, and its answer needs the ID-JAG issued_token_type, so another type of token isn't presented as a grant. Without a user to consent, the client also keeps to the scopes of setScopes() - neither a server's insufficient_scope nor a stored token widens them. Keying the storage by client is #719.

The assertion follows RFC 10070: the issuer as the only aud, typed as client-authentication+jwt, and it lives 60 seconds like #324 sketched. PS256 & EdDSA are left out for now - the first is missing from firebase/php-jwt 6.10 & 7.0, the second can't sign with a PEM key.

CI's --suite all leaves out extension scenarios, so the three of them run as their own step now. The E2E suite also gets two service accounts against Keycloak, one with a secret and one with a key.

@chr-hertel chr-hertel added Client Issues & PRs related to the Client component auth Issues and PRs related to Authentication / OAuth labels Nov 5, 2026
@chr-hertel chr-hertel added this to the 1.0.0 milestone Nov 5, 2026
@chr-hertel
chr-hertel requested a balanced review from Copilot November 8, 2026 11:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

An unresolved critical issuer-binding flaw allows a hostile MCP server to direct service-account authentication to its own authorization server and receive the configured secret.

9 open issues
What changed in this PR

Extends the PHP SDK’s client authorization support with service-account credentials, private-key JWT authentication, and enterprise cross-app access, building on the earlier OAuth work.

Changes:

  • Adds machine-to-machine and enterprise authorization options alongside shared discovery, token exchange, and credential storage.
  • Integrates authentication and authorization failures into the HTTP transport.
  • Adds documentation, examples, unit and integration coverage, and a Keycloak end-to-end environment.
File Description
tests/​Unit/​Client/​Transport/​HttpTransportTest.php Tests authenticated requests and authorization failures.
tests/​Unit/​Client/​Auth/​TokenEndpointTest.php Tests token authentication and JWT signing.
tests/​Unit/​Client/​Auth/​TokenEndpointAuthMethodTest.php Tests authentication-method negotiation.
tests/​Unit/​Client/​Auth/​ProtectedResourceMetadataTest.php Tests resource URL boundaries.
tests/​Unit/​Client/​Auth/​PkceTest.php Tests PKCE generation and reconstruction.
tests/​Unit/​Client/​Auth/​OAuthTest.php Tests OAuth configuration and validation.
tests/​Unit/​Client/​Auth/​FileCredentialStorageTest.php Tests credential persistence and permissions.
tests/​Unit/​Client/​Auth/​ClientRegistrationTest.php Tests registration requests and responses.
tests/​Unit/​Client/​Auth/​AuthorizationServerMetadataTest.php Tests metadata requirements and secure endpoints.
tests/​Unit/​Client/​Auth/​AuthorizationHandlerTest.php Tests loopback and console handlers.
tests/​Unit/​Client/​Auth/​AuthorizationChallengeTest.php Tests Bearer challenge parsing.
tests/​Unit/​Client/​Auth/​AuthenticatingHttpClientTest.php Tests retries, body replay, and response cleanup.
tests/​Unit/​Client/​Auth/​AccessTokenTest.php Tests token expiry, scopes, and serialization.
tests/​Integration/​Fixture/​oauth/​resource-server.php Provides a protected MCP test server.
tests/​Integration/​Fixture/​oauth/​authorization-server.php Provides registration and PKCE test endpoints.
tests/​Integration/​ClientOAuthTest.php Exercises authorization over real HTTP.
tests/​E2E/​server.php Validates Keycloak tokens at an MCP server.
tests/​E2E/​run.sh Starts and runs the end-to-end environment.
tests/​E2E/​README.md Documents the end-to-end setup.
tests/​E2E/​nginx.conf Routes requests to the protected server.
tests/​E2E/​keycloak/​realm.json Defines test clients, users, and token audience.
tests/​E2E/​docker-compose.yml Configures Keycloak and MCP containers.
tests/​E2E/​client.php Checks protected access and credential reuse.
tests/​E2E/​BrowserAuthorizationHandler.php Automates the fixture’s login form.
tests/​Conformance/​conformance-baseline-2026-07-28.yml Removes authorization scenarios from the failure baseline.
tests/​Conformance/​conformance-baseline-2025-11-25.yml Updates authorization failure expectations.
tests/​Conformance/​client.php Configures authentication for conformance scenarios.
src/​Exception/​AuthorizationException.php Introduces an authorization-specific exception.
src/​Client/​Transport/​HttpTransport.php Adds authentication and refusal handling.
src/​Client/​Auth/​TokenEndpointAuthMethod.php Defines and negotiates client authentication methods.
src/​Client/​Auth/​TokenEndpoint.php Sends token requests and signs client assertions.
src/​Client/​Auth/​ProtectedResourceMetadata.php Parses metadata and checks resource coverage.
src/​Client/​Auth/​Pkce.php Generates PKCE verifiers and challenges.
src/​Client/​Auth/​OAuthConfiguration.php Holds and validates OAuth configuration.
src/​Client/​Auth/​MetadataDiscovery.php Discovers resource and authorization-server metadata.
src/​Client/​Auth/​LoopbackAuthorizationHandler.php Opens a browser and receives callbacks.
src/​Client/​Auth/​InMemoryCredentialStorage.php Stores credentials in memory.
src/​Client/​Auth/​HeadlessAuthorizationHandler.php Reads noninteractive authorization redirects.
src/​Client/​Auth/​Grant.php Defines supported OAuth grant identifiers.
src/​Client/​Auth/​FileCredentialStorage.php Persists credentials through private temporary files.
src/​Client/​Auth/​CrossAppAccess.php Configures enterprise identity-token exchange.
src/​Client/​Auth/​CredentialStorageInterface.php Defines credential storage operations.
src/​Client/​Auth/​ConsoleAuthorizationHandler.php Accepts pasted authorization redirect URLs.
src/​Client/​Auth/​ClientRegistration.php Represents client identity and authentication details.
src/​Client/​Auth/​ClientRegistrar.php Performs dynamic client registration.
src/​Client/​Auth/​ClientMetadata.php Builds client registration metadata.
src/​Client/​Auth/​BearerToken.php Attaches a fixed bearer token.
src/​Client/​Auth/​AuthorizationServerMetadata.php Parses server capabilities and validates endpoints.
src/​Client/​Auth/​AuthorizationHandlerInterface.php Defines the authorization interaction contract.
src/​Client/​Auth/​AuthorizationChallenge.php Parses Bearer challenge parameters.
src/​Client/​Auth/​AuthenticatorInterface.php Defines request authentication and challenge handling.
src/​Client/​Auth/​AuthenticatingHttpClient.php Wraps HTTP requests with authentication retries.
src/​Client/​Auth/​AccessToken.php Represents tokens, expiry, and granted scopes.
src/​Client.php Documents authorization exceptions on client operations.
mkdocs.yml Adds authorization to documentation navigation.
Makefile Adds an end-to-end test target.
examples/​client/​README.md Introduces the OAuth client example.
examples/​client/​oauth_client.php Demonstrates authorization and credential persistence.
docs/​get-started/​installation.md Lists the optional JWT signing dependency.
docs/​examples.md Links the OAuth client example.
docs/​client/​transports.md Documents transport authentication and failures.
docs/​client/​index.md Links the authorization guide.
docs/​client/​errors.md Explains authorization exception handling.
docs/​client/​authorization.md Documents OAuth flows and configuration.
composer.json Expands the JWT package suggestion.
CHANGELOG.md Records authorization features and exception changes.
.gitignore Excludes credentials and generated test artifacts.
.github/​workflows/​pipeline.yaml Adds the authorization end-to-end CI job.

Review effort: Balanced

Comment thread src/Client/Auth/OAuthConfiguration.php Resolved
Comment thread examples/client/oauth_client.php Resolved
Comment thread src/Client/Auth/AccessToken.php Resolved
Comment thread src/Client/Auth/CrossAppAccess.php Resolved
Comment thread src/Client/Auth/CrossAppAccess.php Resolved
Comment thread src/Client/Auth/TokenEndpoint.php Resolved
Comment thread src/Client/Auth/TokenEndpoint.php Resolved
Comment thread src/Client/Auth/TokenEndpoint.php Resolved
Comment thread tests/E2E/docker-compose.yml Resolved
@chr-hertel
chr-hertel force-pushed the client-auth-machine-grants branch from 6c78d1a to 08385be Compare November 8, 2026 12:09
@chr-hertel chr-hertel modified the milestones: 1.0.0, 0.10.0 Nov 8, 2026
@chr-hertel
chr-hertel force-pushed the client-auth-machine-grants branch 3 times, most recently from b27b293 to 54a4fc0 Compare November 10, 2026 00:06
@chr-hertel
chr-hertel requested a balanced review from Copilot November 10, 2026 00:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread src/Client/Auth/OAuthAuthenticator.php Resolved
Comment thread src/Client/Auth/OAuthAuthenticator.php Resolved
@chr-hertel
chr-hertel marked this pull request as ready for review November 10, 2026 00:19
@chr-hertel
chr-hertel requested a balanced review from Copilot November 10, 2026 00:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The new credential-signing and cross-IdP authorization flows warrant independent security review of authentication and issuer-audience policies, plus real-provider validation beyond mocked request-shape tests.

1 open issue
2 resolved since last review

Review effort: Balanced

@chr-hertel

chr-hertel commented Nov 10, 2026 •

Copy link
Copy Markdown
Member Author

Went through the open points once more after the rebase:

  • aud stays the issuer - rfc7523bis is out as RFC 10070, and §4 makes the issuer the sole audience a MUST, so that's my answer to the Copilot thread
  • the assertion lives 60 seconds now, like [Client] Implement client_credentials grant (client_secret_basic + private_key_jwt) #324 sketched (498e7fd)
  • typed as client-authentication+jwt (f4de05f) - RFC 10070 also says servers shouldn't reject untyped ones
  • CI ran --suite all, which leaves out auth/client-credentials-jwt, auth/client-credentials-basic & auth/enterprise-managed-authorization since they're extension scenarios - they run as their own step now (0a7d028)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Human assessment is needed because cached-token scopes can bypass the configured limit during non-interactive authentication, potentially restoring broader permissions.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A critical scope-enforcement gap allows an over-scoped stored token to be refreshed without a scope limit, potentially retaining and sending unauthorized admin privileges.

1 open issue

Review effort: Balanced

Comment thread src/Client/Auth/OAuthAuthenticator.php Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Fresh grant and refresh responses can bypass configured scope limits, allowing overprivileged tokens to be saved and used on retry.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread src/Client/Auth/OAuthAuthenticator.php Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

tests/Conformance/client.php:118 uses PHP's native RuntimeException instead of Mcp\Exception\RuntimeException, violating the repository's exception convention because it does not implement ExceptionInterface.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread tests/Conformance/client.php Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The ID-JAG delegation and private-key authentication flows cross multiple credential-bearing trust boundaries, warranting human security review and real IdP interoperability validation beyond the available static evidence.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

forServiceAccount() runs without a user, setPrivateKeyJwt() replaces the shared
secret with a signed assertion and setCrossAppAccess() exchanges an enterprise
identity token for a grant. Without a user, the client keeps to the scopes set
with setScopes().

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The security-sensitive two-provider ID-JAG flow has only permissive mock validation, leaving real-provider client authentication and grant binding unverified and requiring security/domain review.

No open issues 🎉

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Issues and PRs related to Authentication / OAuth Client Issues & PRs related to the Client component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Client] Implement Cross-App Access (token exchange, RFC 8693) [Client] Implement client_credentials grant (client_secret_basic + private_key_jwt)

2 participants