This module allows Drupal sites to act as OAuth 2.0 servers. It enables your Drupal site to authenticate users from other applications using the OAuth 2.0 protocol.
A lot of the code in this module has been created using an AI assistant using Strikethroo & Kenkeep for improved Drupal code generation.
Overview
Adds OAuth2 scope checks to MCP Server tool calls. Set a tool's authentication mode to Required, pick the scopes an access token has to carry, and calls arriving without them are refused before the tool runs.
The module is opt-in and self-contained. Core MCP Server has no OAuth dependency; enable this and the /_mcp route starts accepting Bearer tokens alongside cookies, uninstall it and you are back to cookie auth plus Drupal permissions.
Built on Simple OAuth and Simple OAuth 2.1. Scope policy is stored on MCP Server Tool Bridge tool configurations, so that module is what makes enforcement useful.
Key capabilities
Per-tool scope policy
Each tool carries its own authentication mode and required scope list. A read-only lookup tool and a node-deleting tool can sit on the same server under different rules.
Policy travels with config
Settings are stored as third-party settings on the tool configuration entity, so they export to YAML and deploy with everything else. No separate OAuth config to keep in sync.
Correct 401 and 403
A missing or invalid token gets 401 with a WWW-Authenticate challenge. A valid token missing a scope gets 403. Clients can tell "log in" apart from "you cannot do this".
Scope enforcement, from config to refused call
The policy:
one details element added to the tool form. Authentication mode plus the scopes a token must carry.
The enforcement:
the call never reaches the tool. Refusal happens on the SDK request event, before execution.
Configuring a tool
- Go to Configuration ยป Web services ยป MCP Server ยป Tools and edit a tool.
- Open the OAuth2 Authorization details element.
- Set Authentication mode to Required. The scope selector appears.
- Pick the Required scopes. All of them must be present on the token; the check is an AND, not an OR.
- Save. The export for that tool now carries a
third_party_settings.mcp_server_oauthblock.
What happens on a call
The module subscribes to the PHP SDK's RequestEvent and inspects every CallToolRequest before the SDK dispatches it:
- Find the tool configuration entity whose ID matches the requested tool name. No match means no policy, and the call proceeds.
- Read
authentication_mode. Anything other thanrequiredand the call proceeds. - Read the Bearer token from the request, validate it through Simple OAuth's resource server, load the matching
oauth2_token, confirm it is not revoked, and collect its scope names. - Compare against the required list. If every required scope is present, the call proceeds.
- Otherwise throw
McpAuthorizationDeniedException, which the controller turns into an HTTP error.
Token extraction is deliberately defensive. A malformed header, a failed signature check, a revoked token, or a storage error all resolve to "no scopes" rather than an exception, which then falls through to the refusal path below.
How a refusal is reported
Situation Response No Bearer token, or the token failed validation 401authentication_required, with WWW-Authenticate: Bearer realm="mcp_server"
Valid token, but missing at least one required scope
403 insufficient_scope
The distinction is drawn from whether any scopes were recoverable at all. An empty scope set means the caller is effectively unauthenticated, so the response invites credentials. A non-empty set that falls short means the caller is known but not permitted, so retrying with the same token is pointless and the response says so.
A separate subscriber catches JSON-RPC error code -32001 on the way out and attaches the same 401 status and challenge header, so authentication failures raised elsewhere in the stack surface consistently.
What is and is not gated
Only tools backed by a tool configuration entity can be gated. Tool plugins registered directly in code by a module are never matched, so they run without a scope check no matter what is configured elsewhere. This is deliberate, but it means "enable the OAuth module" is not by itself a statement about the whole server.Reaching the endpoint at all still requires MCP Server's own access mcp server permission, and the tool's own access check still runs. Scope enforcement is a layer on top of both, not a replacement for either.
Discovery metadata
MCP clients discover how to authenticate before they call anything. Two pieces are handled for you:
- The
/.well-known/oauth-protected-resourcedocument (RFC 9728) gets every scope configured across enabled tools merged into itsscopes_supportedlist, deduplicated against whatever Simple OAuth already advertises. - That response is tagged with
mcp_server:discovery, so changing a tool's scopes invalidates the published metadata instead of leaving a stale document cached.
Dynamic client registration is a declared dependency, since MCP clients are expected to register themselves rather than be provisioned by hand.
Turning it off
Uninstalling leaves the third_party_settings.mcp_server_oauth blocks in your tool YAML. Nothing reads them while the module is out, so they sit dormant, and re-enabling restores every policy without reconfiguration.
The /_mcp route drops back to cookie-only authentication on the next cache rebuild.
Other authentication schemes
The gating mechanism is not OAuth-specific. For API keys, mTLS, or a policy of your own, subscribe to the SDK's RequestEvent in a custom module and throw McpAuthorizationDeniedException to refuse a call. This module is a worked example of that contract rather than a privileged one.
Architecture
- Authorization subscriber: the enforcement point on
RequestEvent, matching tool names to configuration entities and applying policy. - Scope validator: validates the Bearer token through Simple OAuth's resource server and resolves it to a list of scope names, returning an empty list on any failure.
- Scope discovery service: aggregates, deduplicates, and sorts scopes across enabled tool configurations. Feeds both the form's options and the published metadata.
- Route subscriber: appends
oauth2to the MCP route's_authoption, keeping core's route free of a Simple OAuth dependency. - Metadata and error subscribers: enrich the protected-resource document, tag it for invalidation, and normalise authentication errors to 401 with a challenge header.
- Form alter and entity builder: add the OAuth details element to the tool form and persist the values as third-party settings.
Related projects
- MCP Server โ the MCP runtime this module gates.
- MCP Server Tool Bridge โ the tool configuration entities that carry the scope policy.
- Simple OAuth โ token issuance and validation.
- Simple OAuth 2.1 โ server metadata and dynamic client registration.
- MCP Server UI โ admin forms for MCP Server.
Resources
- MCP Authorization specification
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 6750: Bearer Token Usage
- Official MCP PHP SDK
Credits
Maintained by the MCP Server contributors.