Skip to content

Commit d695051

Browse files
committed
feat: OAuth implementation
1 parent 74446ae commit d695051

18 files changed

+1278
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authorization\Contracts;
6+
7+
use Mcp\Server\Authorization\Entities\Client;
8+
9+
/**
10+
* Client repository interface for managing OAuth clients
11+
*/
12+
interface ClientRepositoryInterface
13+
{
14+
/**
15+
* Store a client registration
16+
*/
17+
public function storeClient(Client $client): void;
18+
19+
/**
20+
* Retrieve a client by its identifier
21+
*/
22+
public function getClient(string $clientId): ?Client;
23+
24+
/**
25+
* Remove a client from storage
26+
*/
27+
public function revokeClient(string $clientId): void;
28+
29+
/**
30+
* Check if a client exists
31+
*/
32+
public function clientExists(string $clientId): bool;
33+
34+
/**
35+
* Get all registered clients
36+
*
37+
* @return Client[]
38+
*/
39+
public function getAllClients(): array;
40+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authorization\Contracts;
6+
7+
use Mcp\Server\Authorization\Entities\AccessToken;
8+
use Mcp\Server\Authorization\Exception\AuthorizationException;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
11+
/**
12+
* Resource server interface for protecting MCP endpoints
13+
*/
14+
interface ResourceServerInterface
15+
{
16+
/**
17+
* Extract and validate access token from HTTP request
18+
*
19+
* @throws AuthorizationException When authorization fails
20+
*/
21+
public function validateRequest(ServerRequestInterface $request, string $expectedAudience): AccessToken;
22+
23+
/**
24+
* Generate Protected Resource Metadata per RFC 9728
25+
*/
26+
public function getResourceMetadata(): array;
27+
28+
/**
29+
* Check if a request requires authorization
30+
*/
31+
public function requiresAuthorization(ServerRequestInterface $request): bool;
32+
33+
/**
34+
* Create WWW-Authenticate response header per RFC 6750 and RFC 9728
35+
*/
36+
public function createWwwAuthenticateHeader(?string $error = null, ?string $errorDescription = null): string;
37+
38+
/**
39+
* Generate OAuth 2.0 Authorization Server Metadata per RFC 8414
40+
*/
41+
public function getAuthorizationServerMetadata(): array;
42+
43+
/**
44+
* Generate OpenID Connect Discovery metadata
45+
*/
46+
public function getOpenIdConnectDiscoveryMetadata(): array;
47+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authorization\Contracts;
6+
7+
use Mcp\Server\Authorization\Entities\AccessToken;
8+
9+
/**
10+
* Token repository interface for storing and retrieving tokens
11+
*/
12+
interface TokenRepositoryInterface
13+
{
14+
/**
15+
* Store an access token
16+
*/
17+
public function storeToken(AccessToken $token): void;
18+
19+
/**
20+
* Retrieve a token by its identifier
21+
*/
22+
public function getToken(string $tokenId): ?AccessToken;
23+
24+
/**
25+
* Remove a token from storage
26+
*/
27+
public function revokeToken(string $tokenId): void;
28+
29+
/**
30+
* Check if a token exists and is valid
31+
*/
32+
public function isTokenValid(string $tokenId): bool;
33+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authorization\Contracts;
6+
7+
use Mcp\Server\Authorization\Entities\AccessToken;
8+
use Mcp\Server\Authorization\Exception\InvalidTokenException;
9+
10+
/**
11+
* Token validator interface for JWT and other token formats
12+
*/
13+
interface TokenValidatorInterface
14+
{
15+
/**
16+
* Validate and parse an access token
17+
*
18+
* @throws InvalidTokenException When token is invalid, expired, or malformed
19+
*/
20+
public function validateToken(string $token, string $expectedAudience): AccessToken;
21+
22+
/**
23+
* Check if this validator supports the given token format
24+
*/
25+
public function supports(string $token): bool;
26+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authorization\Discovery;
6+
7+
use Mcp\Server\Authorization\Contracts\ResourceServerInterface;
8+
9+
/**
10+
* Handles OAuth 2.1 discovery endpoint resolution and metadata generation
11+
*/
12+
final readonly class DiscoveryEndpointHandler
13+
{
14+
public function __construct(
15+
private ResourceServerInterface $resourceServer,
16+
private DiscoveryUriBuilder $uriBuilder,
17+
) {}
18+
19+
/**
20+
* Check if the request path matches any discovery endpoint
21+
*/
22+
public function isDiscoveryEndpoint(string $path): bool
23+
{
24+
return $this->isOAuthAuthorizationServerEndpoint($path) ||
25+
$this->isOpenIdConnectDiscoveryEndpoint($path) ||
26+
$this->isProtectedResourceMetadataEndpoint($path);
27+
}
28+
29+
/**
30+
* Get metadata for the given discovery endpoint
31+
*/
32+
public function getMetadataForPath(string $path): ?array
33+
{
34+
if ($this->isOAuthAuthorizationServerEndpoint($path)) {
35+
return $this->resourceServer->getAuthorizationServerMetadata();
36+
}
37+
38+
if ($this->isOpenIdConnectDiscoveryEndpoint($path)) {
39+
return $this->resourceServer->getOpenIdConnectDiscoveryMetadata();
40+
}
41+
42+
if ($this->isProtectedResourceMetadataEndpoint($path)) {
43+
return $this->resourceServer->getResourceMetadata();
44+
}
45+
46+
return null;
47+
}
48+
49+
/**
50+
* Get discovery URIs for a given issuer URL (for client implementation)
51+
*/
52+
public function getDiscoveryUrisForIssuer(string $issuerUrl): array
53+
{
54+
return [
55+
'oauth_authorization_server' => $this->uriBuilder->buildOAuthDiscoveryUris($issuerUrl),
56+
'openid_connect_discovery' => $this->uriBuilder->buildOpenIdConnectDiscoveryUris($issuerUrl),
57+
];
58+
}
59+
60+
/**
61+
* Check if path is OAuth 2.0 Authorization Server Metadata endpoint
62+
*/
63+
private function isOAuthAuthorizationServerEndpoint(string $path): bool
64+
{
65+
return $path === '/.well-known/oauth-authorization-server' ||
66+
\str_starts_with($path, '/.well-known/oauth-authorization-server/');
67+
}
68+
69+
/**
70+
* Check if path is OpenID Connect Discovery endpoint
71+
*/
72+
private function isOpenIdConnectDiscoveryEndpoint(string $path): bool
73+
{
74+
return $path === '/.well-known/openid-configuration' ||
75+
\str_ends_with($path, '/.well-known/openid-configuration') ||
76+
\str_starts_with($path, '/.well-known/openid-configuration/');
77+
}
78+
79+
/**
80+
* Check if path is Protected Resource Metadata endpoint
81+
*/
82+
private function isProtectedResourceMetadataEndpoint(string $path): bool
83+
{
84+
return $path === '/.well-known/oauth-protected-resource' ||
85+
\str_starts_with($path, '/.well-known/oauth-protected-resource/');
86+
}
87+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authorization\Discovery;
6+
7+
/**
8+
* Builds well-known discovery URIs per RFC 8414 and OpenID Connect Discovery
9+
*/
10+
final readonly class DiscoveryUriBuilder
11+
{
12+
/**
13+
* Build OAuth 2.0 Authorization Server Metadata URIs per RFC 8414
14+
*
15+
* @return string[] Priority-ordered list of discovery URIs
16+
*/
17+
public function buildOAuthDiscoveryUris(string $issuerUrl): array
18+
{
19+
$parsedUrl = parse_url($issuerUrl);
20+
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
21+
22+
if (isset($parsedUrl['port']) && $parsedUrl['port'] !== 80 && $parsedUrl['port'] !== 443) {
23+
$baseUrl .= ':' . $parsedUrl['port'];
24+
}
25+
26+
$path = $parsedUrl['path'] ?? '';
27+
$path = trim($path, '/');
28+
29+
$uris = [];
30+
31+
if (!empty($path)) {
32+
// Path insertion method: https://auth.example.com/.well-known/oauth-authorization-server/tenant1
33+
$uris[] = $baseUrl . '/.well-known/oauth-authorization-server/' . $path;
34+
}
35+
36+
// Root method: https://auth.example.com/.well-known/oauth-authorization-server
37+
$uris[] = $baseUrl . '/.well-known/oauth-authorization-server';
38+
39+
return $uris;
40+
}
41+
42+
/**
43+
* Build OpenID Connect Discovery URIs per OpenID Connect Discovery 1.0
44+
*
45+
* @return string[] Priority-ordered list of discovery URIs
46+
*/
47+
public function buildOpenIdConnectDiscoveryUris(string $issuerUrl): array
48+
{
49+
$parsedUrl = parse_url($issuerUrl);
50+
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
51+
52+
if (isset($parsedUrl['port']) && $parsedUrl['port'] !== 80 && $parsedUrl['port'] !== 443) {
53+
$baseUrl .= ':' . $parsedUrl['port'];
54+
}
55+
56+
$path = $parsedUrl['path'] ?? '';
57+
$path = trim($path, '/');
58+
59+
$uris = [];
60+
61+
if (!empty($path)) {
62+
// Path insertion method: https://auth.example.com/.well-known/openid-configuration/tenant1
63+
$uris[] = $baseUrl . '/.well-known/openid-configuration/' . $path;
64+
65+
// Path appending method: https://auth.example.com/tenant1/.well-known/openid-configuration
66+
$uris[] = $baseUrl . '/' . $path . '/.well-known/openid-configuration';
67+
}
68+
69+
// Root method: https://auth.example.com/.well-known/openid-configuration
70+
$uris[] = $baseUrl . '/.well-known/openid-configuration';
71+
72+
return $uris;
73+
}
74+
75+
/**
76+
* Build Protected Resource Metadata URIs per RFC 9728
77+
*
78+
* @return string[] Priority-ordered list of discovery URIs
79+
*/
80+
public function buildResourceMetadataUris(string $resourceUrl, ?string $specificPath = null): array
81+
{
82+
$parsedUrl = parse_url($resourceUrl);
83+
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
84+
85+
if (isset($parsedUrl['port']) && $parsedUrl['port'] !== 80 && $parsedUrl['port'] !== 443) {
86+
$baseUrl .= ':' . $parsedUrl['port'];
87+
}
88+
89+
$uris = [];
90+
91+
if ($specificPath !== null) {
92+
// Path-specific metadata
93+
$cleanPath = trim($specificPath, '/');
94+
$uris[] = $baseUrl . '/.well-known/oauth-protected-resource/' . $cleanPath;
95+
}
96+
97+
// Root metadata
98+
$uris[] = $baseUrl . '/.well-known/oauth-protected-resource';
99+
100+
return $uris;
101+
}
102+
}

0 commit comments

Comments
 (0)