[OAuth] Basic integration: embedded authorization & resource server - #1958
Draft
wwidergoldpimcore wants to merge 19 commits into
Draft
[OAuth] Basic integration: embedded authorization & resource server#1958wwidergoldpimcore wants to merge 19 commits into
wwidergoldpimcore wants to merge 19 commits into
Conversation
Introduce an opt-in, self-contained OAuth module under src/OAuth that adds no wiring into the application's global security configuration. - Add league/oauth2-server dependency. - Contracts under OAuth/Contract: ResourceRegistryInterface, TokenValidatorInterface, TokenIssuerInterface, IdentityResolverInterface, IdentityMapperInterface. - DTOs: ResolvedAccess, ProtectedResource, and the RFC 9728 ProtectedResourceMetadata document. - ConfigProtectedResourceRegistry: config-driven, multi-resource, keyed by canonical URI with normalisation-insensitive lookups. - EmbeddedIdentityResolver: resolves a numeric token subject to a valid, enabled Pimcore user; rejects disabled/invalid users. - CanonicalUri: canonicalises resource URIs (lowercase scheme+host, drop default ports, no fragment/trailing slash). - New config tree pimcore_studio_backend.oauth (enabled/issuer/keys/clients/ resources), disabled by default; the extension exposes oauth.* parameters and seeds the resource registry. - Unit tests for CanonicalUri, the registry, and the identity resolver. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validate OAuth JWT access tokens on the pimcore_mcp firewall and resolve them to a Pimcore user, additive to the existing authenticator chain. - EmbeddedTokenValidator: verifies a JWT signature against the configured public key, plus expiry and (when set) issuer via lcobucci/jwt; checks revocation and resolves the subject to a Pimcore user; returns granted scopes, audience and client id. The endpoint-vs-audience check is currently a placeholder that accepts any audience. - TokenRevocationCheckerInterface with a null implementation that reports nothing as revoked until token persistence exists. - OAuthAccessTokenAuthenticator: claims only JWT-shaped bearer tokens, declines the pmcp_ prefix, stays inert unless OAuth is enabled, and returns null on failure so later authenticators still run. Inserted before PatAuthenticator in the pimcore_mcp chain. - Extension wires the validator public key/issuer and the authenticator enabled flag from configuration. - Unit tests for token validation (valid, expired, wrong issuer, bad signature, revoked, unresolvable user, malformed, missing key) and the authenticator's support matrix and passport building. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address hardening findings from a security review of the resource-server validator. - Reject tokens without an `exp` claim. LooseValidAt only checks time claims that are present, so a token missing `exp` would otherwise never expire; fail closed instead. (StrictValidAt is unsuitable as it also mandates `nbf`, which is optional for our tokens.) - Split the scope claim on any whitespace run so repeated spaces no longer produce empty scope entries. - Add regression tests asserting rejection of a missing-`exp` token, an RS->HS256 algorithm-confusion token (HMAC-signed with the RSA public key), and a hand-crafted alg=none token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement RFC 9728 discovery for the MCP resource server. - ProtectedResourceMetadataController serves the Protected Resource Metadata document as JSON, keyed by the path suffix of the well-known URL; unknown resources return 404. - Register the endpoint at the root path /.well-known/oauth-protected-resource via an explicit route (outside the API prefix) instead of the prefixed attribute loader. - McpAuthenticationEntryPoint returns 401 for unauthenticated MCP requests and, when OAuth is enabled, adds a "WWW-Authenticate: Bearer" challenge advertising the resource metadata URL and mcp:read scope; plain 401 when disabled. - Register the entry point on the pimcore_mcp firewall settings and set its enabled flag from configuration. - Unit tests for the controller (metadata served / 404) and the entry point (challenge when enabled / plain 401 when disabled). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Symfony authenticator manager runs the firewall chain until an authenticator returns a response; a successful authenticator whose onAuthenticationSuccess() returns null does not stop it. PatAuthenticator claimed every non-pmcp_ bearer, so after OAuthAccessTokenAuthenticator authenticated a JWT successfully, PatAuthenticator still ran, failed to match it against the PAT map, and its 401 overrode the success. Decline JWT-shaped bearer tokens in PatAuthenticator::supports() so they are handled only by OAuthAccessTokenAuthenticator, mirroring the existing pmcp_ exclusion that protects McpAccessTokenAuthenticator. Add a support-matrix test covering opaque, pmcp_ and JWT bearers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uance Stand up the isolated league authorization server that mints RFC 9068 JWT access tokens, and expose the token and discovery endpoints. - Entities: ClientEntity (with a service-user for client_credentials), ScopeEntity, and AccessTokenEntity — the latter replaces league's default token trait with a custom toString() emitting a space-delimited `scope` string, `client_id` and `iss` (resource `aud` binding is added later). - Repositories: config-driven ClientRepository (with secret validation), ScopeRepository (mcp:read / mcp:write), and a stateless AccessTokenRepository (persistence/revocation land with refresh tokens). - AuthorizationServerFactory builds a standalone server from the configured signing/encryption keys and enables the client_credentials grant. - Endpoints (routed under /pimcore-oauth and the well-known path, outside the Studio API prefix): POST /pimcore-oauth/token and the RFC 8414 authorization server metadata document. - Config: activate oauth.clients (with service_user), keys.private_key / encryption_key and access_token_ttl; preserve client-id keys verbatim. - Unit tests for the JWT claim shape, client secret validation and scope resolution. A client_credentials token issued by /pimcore-oauth/token is accepted by the resource-server authenticator on the pimcore_mcp firewall, resolving to the client's service user. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a token record store so issued OAuth artifacts can be revoked, and back the resource-server revocation check with it (previously a no-op). - OAuthTokenRecord entity + migration (bundle_studio_oauth_token): identifier, type, expiry, revoked flag, user/client, created-at; indexed by user and expiry. - TokenRecordStore(Interface): persist (rejecting duplicate identifiers), revoke, isRevoked (blocklist — an unknown identifier counts as not revoked), and expired-record cleanup. - AccessTokenRepository now records each issued token and answers revoke/isRevoked against the store. - StoredTokenRevocationChecker replaces the null checker, so a revoked access token is rejected on its next request. - Unit test for the revocation checker delegation. A client-credentials token is now persisted on issue and rejected by the MCP firewall once its record is revoked. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the grant machinery the auth-code flow needs; the user-facing authorize + consent endpoints follow separately. - Entities: AuthCodeEntity, RefreshTokenEntity and UserEntity (the resource owner, whose identifier is the numeric Pimcore user id). - Store-backed AuthCodeRepository and RefreshTokenRepository, so codes are one-time-use and refresh tokens are revoked on rotation (reuse detection). - AuthorizationServerFactory now enables AuthCodeGrant (PKCE S256) and RefreshTokenGrant alongside client_credentials. - Config: auth_code_ttl and refresh_token_ttl. - Unit test for the auth-code repository. client_credentials issuance and resource-server validation are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the authorization-code + PKCE flow with a Studio UI consent handoff.
- AuthorizeController (GET /pimcore-oauth/authorize): validates the request,
stashes it under an opaque id in a short-lived store, and redirects the
browser to the Studio UI consent route.
- Consent API under the Studio API firewall (session-authenticated):
GET /oauth/authorizations/{id} returns the client, scopes and acting user;
POST /oauth/authorizations/{id} {approved} completes the request and returns
the client redirect location carrying the code, state and RFC 9207 iss.
- PendingAuthorizationStore (cache-backed) and AuthorizationRequestValidator
(re-validates the stored parameters into a league AuthorizationRequest).
- Authorization server metadata now advertises the authorization endpoint,
the authorization_code/refresh_token grants, response_types and S256 PKCE.
- Config: consent_path for the redirect target.
- Unit test for the pending-authorization store.
Verified end to end: login, authorize, consent, approve, code+PKCE exchange
(access + refresh tokens), and resource-server acceptance as the consenting
Pimcore user.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align the consent endpoints with the rest of the Studio API: they now extend AbstractApiController, are documented with OpenAPI operation/parameter/response attributes, and return serialized response schemas. - Add an OAuth tag. - Response schemas: AuthorizationConsent (+ client/user) and AuthorizationRedirect. - AuthorizationDetailsController / AuthorizationApprovalController: Get/Post operations, path + request-body parameters, success responses referencing the schemas, and NotFoundException for an unknown/expired authorization id. Response bodies are unchanged; the endpoints now appear in the OpenAPI document. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review findings on the authorization server. - Reject public (non-confidential) clients for the client_credentials grant: a public client carries no secret, so accepting it there would let one with a service_user obtain a service-account token with only its public client id. - Validate at config time that a client with a service_user is confidential and defines a secret, so the footgun configuration is rejected on boot. - Advertise the "none" token-endpoint auth method for public PKCE clients in the authorization server metadata (and fix its now-stale docblock). - Give the pending-authorization store a dedicated filesystem-backed cache pool instead of cache.app, so the authorize request and the later consent approval share the same store regardless of the project's cache.app adapter. - Test that a public client cannot authenticate for client_credentials. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Studio UI uses a browser router, so the authorize endpoint must redirect to /pimcore-studio/oauth/consent (a hash-fragment path would not resolve). Update the consent_path default accordingly; deployments with a different UI base can still override it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MCP entry point hardcoded the protected-resource metadata path (/.well-known/oauth-protected-resource/pimcore-mcp) in the WWW-Authenticate challenge. That baked in a single fixed resource path and no longer matched when the protected endpoint differed. Build the URL from the request path instead, so the challenge points at the metadata for the endpoint that was actually requested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tadata URL Commit 1e3ddb8 changed McpAuthenticationEntryPoint to derive the 401 resource_metadata URL from the request path but left this test asserting the previous hardcoded value. Update the expectation to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Let a client that is not pre-registered identify itself and complete the authorization-code flow, so a native MCP client (e.g. Claude Code) can connect without an open registration endpoint. All additions are opt-in and off by default. Client ID Metadata Documents (CIMD): a URL-form client_id is resolved by fetching the client's metadata document. The fetch is SSRF-guarded (https only outside a dev flag, optional host allow-list, private/loopback network blocked, no redirects, size/time caps, and the document's own client_id must match the URL) and cached. ClientRepository delegates URL-form ids to the resolver; such clients are public (PKCE, no client_credentials). The authorization-server metadata advertises client_id_metadata_document_supported. Loopback redirects (RFC 8252): league applies the port-agnostic loopback match only to the IP literals 127.0.0.1/[::1], and reports a redirect mismatch as invalid_client. A dedicated validator plus a thin AuthCodeGrant subclass extend the port exception, with localhost included behind allow_localhost_loopback_ redirect (default on). RFC 8252 marks localhost as NOT RECOMMENDED, so this accommodates a not-recommended client choice; tracked upstream in anthropics/claude-code#42765. Config: client_id_metadata_documents (enabled, allowed_hosts, allow_insecure, cache_ttl) and allow_localhost_loopback_redirect, plus a dedicated cache pool for fetched documents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lable type - Wrap three lines over the 120-column limit (the CIMD resolver log call, the allow_insecure config info, and the token-store @throws docblock). - Import LogicException and UniqueTokenIdentifierConstraintViolationException rather than referencing them by inline fully-qualified name. - Drop the unused nullable return type on the identity-resolver test closures (PHPStan return.unusedType). - Make the loopback redirect validator's properties readonly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Split the oversized addOAuthNode config builder by extracting the CIMD node into its own method (was over the 150-line limit). - Replace the generic RuntimeException in the server factory with a dedicated MissingKeyMaterialException, and extract the repeated 'PT%dS' DateInterval construction into a helper. - Reduce EmbeddedTokenValidator::validate to eight returns by extracting token parsing/verification into a helper; split the two assignments-in-expressions; add the /u flag to the scope-splitting regex. - Define constants for the duplicated '$issuer'/'$enabled' DI argument names in the extension. - Extract the memoisation assignment in the CIMD resolver and align the access-token scope-string arguments. Kept intentionally: the unused $audience/$resourceUri parameters on isAudienceAllowed are the deferred RFC 8707 audience-enforcement seam, and the catch(Throwable) blocks are deliberate fail-closed boundaries on the token endpoints and validator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add RFC 7591 Dynamic Client Registration as a second client-identity mechanism next to CIMD, so a client can self-register when it does not present a CIMD URL. Both are opt-in and off by default. - POST /pimcore-oauth/register (ClientRegistrationController + ClientRegistrar) validates the request, creates a public or confidential client, persists it (OAuthClientRecord + migration, via DynamicClientStore) and returns the client_id and a one-time secret stored only as a SHA-256 hash; returns 404 when disabled. - ClientRepository now resolves clients from three sources in order: config, CIMD URL, and the dynamic store. Dynamic clients are limited to the interactive grants and never client_credentials. - The authorization-server metadata advertises registration_endpoint (when DCR is enabled) in addition to client_id_metadata_document_supported (CIMD). - Config: oauth.dynamic_client_registration.enabled (extracted into its own builder method), the /register route, and the wiring. Verified: metadata advertises both; register -> authorize -> token recognises a dynamic client; CIMD and loopback redirects still work. 70 OAuth + 17 Security unit tests pass; PHPStan clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- OAuthClientRecord: reduce the constructor to seven parameters (S107) by deriving token_endpoint_auth_method from the confidential flag and stamping created_at internally. - EmbeddedTokenValidator: drop the no-op isAudienceAllowed() method (S1172 x2). The audience is still captured for ResolvedAccess, and $resourceUri remains the interface seam for deferred RFC 8707 enforcement. - ClientRegistrar: add the /u flag to the scope-splitting regex (S5867). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Changes in this pull request
Refs pimcore/product-management#1308 (OAuth basic integration).
Adds an embedded, opt-in OAuth 2.1 authorization + resource server to the Studio Backend bundle, isolated from the application's global security configuration (it does not touch the global user providers or firewall map, so a project's existing OAuth setup is unaffected).
league/oauth2-serverinstance behind/pimcore-oauth/*, opt-in and disabled by default.pimcore_mcpfirewall chain (additive; declinespmcp_and PAT tokens), resolving a token to a Pimcore user so existing ACLs apply. RFC 9068 JWTs with signature/expiry/issuer checks and revocation.401WWW-Authenticatechallenge on the MCP endpoint.authorization_code+ PKCE (S256) with a Studio UI consent handoff,client_credentialsfor service accounts, and refresh tokens; token and authorize endpoints./pimcore-studio/api(session-authenticated, documented in the OpenAPI spec).pimcore_studio_backend.oauth(opt-in), with unit tests and end-to-end verification of both grant flows.Additional info
Deferred (tracked; out of scope for this slice):
studio-ui-bundle.Verified end-to-end against a local Pimcore instance: the
authorization_code+ PKCE round-trip (login → consent → code → token → resource server), theclient_credentialsgrant, discovery documents, the401challenge, and access-token revocation.🤖 Generated with Claude Code