This document describes how to implement OAuth 2.0 authentication for MCP servers using the PHP SDK, following the MCP Authorization Specification.
The MCP OAuth 2.0 implementation follows these standards:
- OAuth 2.1 (IETF DRAFT) - Core authentication framework
- RFC 9728 - Protected Resource Metadata
- RFC 8414 - Authorization Server Metadata Discovery
- RFC 7591 - Dynamic Client Registration
- RFC 7592 - Client Registration Management
- RFC 7662 - Token Introspection
- RFC 6750 - Bearer Token Usage
- RFC 8707 - Resource Indicators
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ MCP Client │────▶│ MCP Server │────▶│ Auth Server │
│ │ │ (Resource) │ │ (Keycloak, │
│ │ │ │ │ Azure AD...) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
│ 1. Discovery │ │
│──────────────────────▶│ │
│ Protected Resource │ │
│ Metadata (401) │ │
│◀──────────────────────│ │
│ │ │
│ 2. Get Token │ │
│──────────────────────────────────────────────▶│
│ │ │
│ 3. Token Response │ │
│◀──────────────────────────────────────────────│
│ │ │
│ 4. MCP Request │ │
│ (Bearer Token) │ │
│──────────────────────▶│ │
│ │ 5. Validate JWT │
│ │ (via JWKS) │
│ │ │
│ 6. MCP Response │ │
│◀──────────────────────│ │
use Mcp\Server\Auth\JwtTokenAuthenticator;
$tokenAuthenticator = new JwtTokenAuthenticator(
jwksUri: 'https://your-auth-server/.well-known/jwks.json',
issuer: 'https://your-auth-server',
audience: 'your-api-identifier', // MCP server canonical URI
algorithms: ['RS256'],
);use Mcp\Server\Auth\ProtectedResourceMetadata;
$resourceMetadata = new ProtectedResourceMetadata(
resource: 'https://mcp.example.com',
authorizationServers: ['https://your-auth-server'],
scopesSupported: ['mcp:read', 'mcp:write'],
);use Mcp\Server\Auth\OAuth2Configuration;
$authConfig = new OAuth2Configuration(
tokenAuthenticator: $tokenAuthenticator,
resourceMetadata: $resourceMetadata,
);use Mcp\Server\Transport\OAuth2HttpTransport;
$transport = new OAuth2HttpTransport(
request: $psrServerRequest,
authConfig: $authConfig,
logger: $logger,
);
$server->run($transport);Validates JWT access tokens using public keys from a JWKS endpoint.
Constructor Parameters:
| Parameter | Type | Description |
|---|---|---|
jwksUri |
string | URL to fetch JSON Web Key Set |
issuer |
string | Expected token issuer (iss claim) |
audience |
string|null | Expected audience (aud claim) |
algorithms |
string[] | Allowed signing algorithms |
leeway |
int | Clock skew tolerance in seconds |
jwksCacheTtl |
int | JWKS cache duration in seconds |
Supported Algorithms:
- RS256, RS384, RS512 (RSA)
- ES256, ES384, ES512 (ECDSA)
Represents the OAuth 2.0 Protected Resource Metadata document (RFC 9728).
Properties:
| Property | Type | Description |
|---|---|---|
resource |
string | Canonical URI of the MCP server |
authorizationServers |
string[] | List of authorization server issuers |
scopesSupported |
string[]|null | Supported OAuth scopes |
bearerMethodsSupported |
string[]|null | Token delivery methods |
resourceName |
string|null | Human-readable name |
Combines all OAuth2 settings for the transport.
Properties:
| Property | Type | Description |
|---|---|---|
tokenAuthenticator |
TokenAuthenticatorInterface | Token validator |
resourceMetadata |
ProtectedResourceMetadata | RFC 9728 metadata |
publicPaths |
string[] | Paths that skip authentication |
HTTP transport with built-in OAuth2 authentication.
Features:
- Automatic Protected Resource Metadata endpoint (
/.well-known/oauth-protected-resource) - Bearer token extraction from Authorization header
- WWW-Authenticate challenges for 401/403 responses
- Scope-based access control
$tenantId = 'your-tenant-id';
$clientId = 'your-client-id';
$tokenAuthenticator = new JwtTokenAuthenticator(
jwksUri: "https://login.microsoftonline.com/{$tenantId}/discovery/v2.0/keys",
issuer: "https://login.microsoftonline.com/{$tenantId}/v2.0",
audience: $clientId,
);$domain = 'your-domain.auth0.com';
$tokenAuthenticator = new JwtTokenAuthenticator(
jwksUri: "https://{$domain}/.well-known/jwks.json",
issuer: "https://{$domain}/",
audience: 'your-api-identifier',
);$realm = 'your-realm';
$keycloakUrl = 'https://keycloak.example.com';
$tokenAuthenticator = new JwtTokenAuthenticator(
jwksUri: "{$keycloakUrl}/realms/{$realm}/protocol/openid-connect/certs",
issuer: "{$keycloakUrl}/realms/{$realm}",
audience: 'your-client-id',
);$domain = 'your-domain.okta.com';
$authServerId = 'default'; // or custom auth server ID
$tokenAuthenticator = new JwtTokenAuthenticator(
jwksUri: "https://{$domain}/oauth2/{$authServerId}/v1/keys",
issuer: "https://{$domain}/oauth2/{$authServerId}",
audience: 'api://your-api',
);Returned when:
- No Authorization header present
- Invalid token format
- Token validation fails (expired, invalid signature, wrong issuer/audience)
Response includes WWW-Authenticate header with:
resource_metadata- URL to Protected Resource Metadatascope- Required scopes (if configured)
Returned when the token is valid but lacks required scopes.
Response includes WWW-Authenticate header with:
error="insufficient_scope"scope- Required scopes for the operation
Create a custom authenticator by implementing TokenAuthenticatorInterface:
use Mcp\Server\Auth\TokenAuthenticatorInterface;
use Mcp\Server\Auth\AuthenticationResult;
class CustomTokenAuthenticator implements TokenAuthenticatorInterface
{
public function authenticate(string $token, ?string $resource = null): AuthenticationResult
{
// Your validation logic here
// Could use token introspection, database lookup, etc.
if ($valid) {
return AuthenticationResult::authenticated([
'sub' => 'user-id',
'scope' => 'read write',
// ... other claims
]);
}
return AuthenticationResult::unauthenticated(
'invalid_token',
'Token validation failed'
);
}
}The SDK includes a Docker setup with Keycloak for local testing:
cd docker
docker-compose upThis starts:
- MCP Server at http://localhost:8080
- Keycloak at http://localhost:8180 (admin/admin)
- MCP Inspector at http://localhost:6274
See docker/README.md for detailed instructions.
For server-side applications:
$clientMetadata = ClientRegistration::forConfidentialClient(
redirectUris: ['https://myapp.example.com/callback'],
clientName: 'My Server App',
scope: 'mcp:read mcp:write mcp:admin',
tokenEndpointAuthMethod: 'client_secret_basic', // or 'client_secret_post', 'private_key_jwt'
);For opaque tokens, use RFC 7662 Token Introspection:
use Mcp\Server\Auth\IntrospectionTokenAuthenticator;
$tokenAuthenticator = new IntrospectionTokenAuthenticator(
introspectionEndpoint: 'https://auth.example.com/oauth/introspect',
clientId: 'mcp-server',
clientSecret: 'server-secret',
expectedAudience: 'https://mcp.example.com',
logger: $logger,
);| Class | RFC | Description |
|---|---|---|
JwtTokenAuthenticator |
- | JWKS-based JWT validation |
IntrospectionTokenAuthenticator |
7662 | Token introspection |
ProtectedResourceMetadata |
9728 | Protected resource metadata |
OAuth2Configuration |
- | OAuth2 configuration container |
OAuth2HttpTransport |
- | HTTP transport with OAuth2 |
WwwAuthenticateChallenge |
6750 | WWW-Authenticate header builder |
AuthorizationServerMetadata |
8414 | Auth server metadata model |
DynamicClientRegistration |
7591 | Dynamic client registration |
ClientRegistration |
7591 | Client registration metadata |
ClientRegistrationResponse |
7591 | Registration response model |
- Always use HTTPS in production
- Validate audience to prevent token misuse
- Use short-lived tokens with refresh tokens
- Implement scope-based access control for sensitive operations
- Cache JWKS appropriately but allow for key rotation
- Validate PKCE support before proceeding with authorization
- Store registration tokens securely if using dynamic client registration