Skip to content

Commit 7f17b38

Browse files
authored
Merge pull request #12 from llm-agents-php/feature/oauth
OAuth 2.1 Client Registration Implementation
2 parents 74446ae + 18ecfe0 commit 7f17b38

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2192
-3
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
],
1717
"require": {
1818
"php": ">=8.3",
19+
"ext-openssl": "*",
1920
"php-mcp/schema": "^1.0",
2021
"psr/clock": "^1.0",
2122
"psr/container": "^1.0 || ^2.0",

context.yaml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,16 @@ documents:
7272
- type: file
7373
sourcePaths:
7474
- src/Transports/HttpServer.php
75-
- vendor/react/http/src/HttpServer.php
76-
- vendor/react/http/src/Io/MiddlewareRunner.php
77-
- vendor/react/http/src/Middleware
75+
- src/Transports/HttpServerTransport.php
7876
- src/Transports/Middleware
7977

78+
- description: OAuth
79+
outputPath: src/oauth.md
80+
sources:
81+
- type: file
82+
sourcePaths:
83+
- src/Authentication
84+
8085
- description: Session
8186
outputPath: src/session.md
8287
sources:

src/Authentication/AuthInfo.php

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\Authentication;
6+
7+
/**
8+
* Information about a validated access token, provided to request handlers.
9+
*/
10+
interface AuthInfo
11+
{
12+
/**
13+
* The access token.
14+
*/
15+
public function getToken(): string;
16+
17+
/**
18+
* The client ID associated with this token.
19+
*/
20+
public function getClientId(): string;
21+
22+
/**
23+
* Scopes associated with this token.
24+
*
25+
* @return string[]
26+
*/
27+
public function getScopes(): array;
28+
29+
/**
30+
* When the token expires (in seconds since epoch).
31+
*/
32+
public function getExpiresAt(): ?int;
33+
34+
/**
35+
* The RFC 8707 resource server identifier for which this token is valid.
36+
* If set, this MUST match the MCP server's resource identifier (minus hash fragment).
37+
*/
38+
public function getResource(): ?string;
39+
40+
/**
41+
* Additional data associated with the token.
42+
* This field should be used for any additional data that needs to be attached to the auth info.
43+
*
44+
* @return array<string, mixed>
45+
*/
46+
public function getExtra(): array;
47+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authentication;
6+
7+
/**
8+
* Parameters for OAuth authorization flow.
9+
*/
10+
final readonly class AuthorizationParams
11+
{
12+
/**
13+
* @param string[] $scopes
14+
*/
15+
public function __construct(
16+
public string $codeChallenge,
17+
public string $redirectUri,
18+
public ?string $state = null,
19+
public array $scopes = [],
20+
public ?string $resource = null,
21+
) {}
22+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authentication\Contract;
6+
7+
use Mcp\Server\Authentication\Dto\OAuthClientInformation;
8+
9+
/**
10+
* Store for registered OAuth clients.
11+
*/
12+
interface OAuthRegisteredClientsStoreInterface
13+
{
14+
/**
15+
* Get client information by client ID.
16+
*
17+
* @throws \RuntimeException if client retrieval fails
18+
*/
19+
public function getClient(string $clientId): ?OAuthClientInformation;
20+
21+
/**
22+
* Register a new client (optional - for dynamic registration).
23+
*
24+
* @throws \RuntimeException if client registration fails
25+
*/
26+
public function registerClient(OAuthClientInformation $client): OAuthClientInformation;
27+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authentication\Contract;
6+
7+
use Mcp\Server\Authentication\AuthInfo;
8+
use Mcp\Server\Authentication\AuthorizationParams;
9+
use Mcp\Server\Authentication\Dto\OAuthClientInformation;
10+
use Mcp\Server\Authentication\Dto\OAuthTokenRevocationRequest;
11+
use Mcp\Server\Authentication\Dto\OAuthTokens;
12+
use Psr\Http\Message\ResponseInterface;
13+
14+
/**
15+
* Implements an end-to-end OAuth server.
16+
*/
17+
interface OAuthServerProviderInterface
18+
{
19+
/**
20+
* A store used to read information about registered OAuth clients.
21+
*/
22+
public function getClientsStore(): OAuthRegisteredClientsStoreInterface;
23+
24+
/**
25+
* Begins the authorization flow, which can either be implemented by this server itself
26+
* or via redirection to a separate authorization server.
27+
*
28+
* This server must eventually issue a redirect with an authorization response or an
29+
* error response to the given redirect URI. Per OAuth 2.1:
30+
* - In the successful case, the redirect MUST include the `code` and `state` (if present) query parameters.
31+
* - In the error case, the redirect MUST include the `error` query parameter, and MAY include
32+
* an optional `error_description` query parameter.
33+
*
34+
* @throws \RuntimeException if authorization fails
35+
*/
36+
public function authorize(
37+
OAuthClientInformation $client,
38+
AuthorizationParams $params,
39+
ResponseInterface $response,
40+
): ResponseInterface;
41+
42+
/**
43+
* Returns the `codeChallenge` that was used when the indicated authorization began.
44+
*
45+
* @throws \RuntimeException if code challenge retrieval fails
46+
*/
47+
public function challengeForAuthorizationCode(
48+
OAuthClientInformation $client,
49+
string $authorizationCode,
50+
): string;
51+
52+
/**
53+
* Exchanges an authorization code for an access token.
54+
*
55+
* @throws \RuntimeException if token exchange fails
56+
*/
57+
public function exchangeAuthorizationCode(
58+
OAuthClientInformation $client,
59+
string $authorizationCode,
60+
?string $codeVerifier = null,
61+
?string $redirectUri = null,
62+
?string $resource = null,
63+
): OAuthTokens;
64+
65+
/**
66+
* Exchanges a refresh token for an access token.
67+
*
68+
* @param string[] $scopes
69+
*
70+
* @throws \RuntimeException if token exchange fails
71+
*/
72+
public function exchangeRefreshToken(
73+
OAuthClientInformation $client,
74+
string $refreshToken,
75+
array $scopes = [],
76+
?string $resource = null,
77+
): OAuthTokens;
78+
79+
/**
80+
* Verifies an access token and returns information about it.
81+
*
82+
* @throws \RuntimeException if token verification fails
83+
*/
84+
public function verifyAccessToken(string $token): AuthInfo;
85+
86+
/**
87+
* Revokes an access or refresh token. If unimplemented, token revocation is not supported (not recommended).
88+
*
89+
* If the given token is invalid or already revoked, this method should do nothing.
90+
*
91+
* @throws \RuntimeException if token revocation fails
92+
*/
93+
public function revokeToken(
94+
OAuthClientInformation $client,
95+
OAuthTokenRevocationRequest $request,
96+
): void;
97+
98+
/**
99+
* Whether to skip local PKCE validation.
100+
*
101+
* If true, the server will not perform PKCE validation locally and will pass the
102+
* code_verifier to the upstream server.
103+
*
104+
* NOTE: This should only be true if the upstream server is performing the actual PKCE validation.
105+
*/
106+
public function skipLocalPkceValidation(): bool;
107+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authentication\Contract;
6+
7+
use Mcp\Server\Authentication\AuthInfo;
8+
9+
/**
10+
* Slim implementation useful for token verification only.
11+
*/
12+
interface OAuthTokenVerifierInterface
13+
{
14+
/**
15+
* Verifies an access token and returns information about it.
16+
*
17+
* @throws \RuntimeException if token verification fails
18+
*/
19+
public function verifyAccessToken(string $token): AuthInfo;
20+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authentication;
6+
7+
/**
8+
* Default implementation of AuthInfo.
9+
*/
10+
final readonly class DefaultAuthInfo implements AuthInfo
11+
{
12+
/**
13+
* @param string[] $scopes
14+
* @param array<string, mixed> $extra
15+
*/
16+
public function __construct(
17+
private string $token,
18+
private string $clientId,
19+
private array $scopes,
20+
private ?int $expiresAt = null,
21+
private ?string $resource = null,
22+
private array $extra = [],
23+
) {}
24+
25+
public function getToken(): string
26+
{
27+
return $this->token;
28+
}
29+
30+
public function getClientId(): string
31+
{
32+
return $this->clientId;
33+
}
34+
35+
public function getScopes(): array
36+
{
37+
return $this->scopes;
38+
}
39+
40+
public function getExpiresAt(): ?int
41+
{
42+
return $this->expiresAt;
43+
}
44+
45+
public function getResource(): ?string
46+
{
47+
return $this->resource;
48+
}
49+
50+
public function getExtra(): array
51+
{
52+
return $this->extra;
53+
}
54+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Mcp\Server\Authentication\Dto;
6+
7+
/**
8+
* OAuth client information as used in dynamic registration and client authentication.
9+
*/
10+
final readonly class OAuthClientInformation implements \JsonSerializable
11+
{
12+
public function __construct(
13+
public string $clientId,
14+
public ?string $clientSecret = null,
15+
public ?int $clientIdIssuedAt = null,
16+
public ?int $clientSecretExpiresAt = null,
17+
) {}
18+
19+
public function getClientId(): string
20+
{
21+
return $this->clientId;
22+
}
23+
24+
public function getClientSecret(): ?string
25+
{
26+
return $this->clientSecret;
27+
}
28+
29+
public function getClientIdIssuedAt(): ?int
30+
{
31+
return $this->clientIdIssuedAt;
32+
}
33+
34+
public function getClientSecretExpiresAt(): ?int
35+
{
36+
return $this->clientSecretExpiresAt;
37+
}
38+
39+
public function jsonSerialize(): array
40+
{
41+
return \array_filter([
42+
'client_id' => $this->clientId,
43+
'client_secret' => $this->clientSecret,
44+
'client_id_issued_at' => $this->clientIdIssuedAt,
45+
'client_secret_expires_at' => $this->clientSecretExpiresAt,
46+
], static fn($value) => $value !== null);
47+
}
48+
}
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\Authentication\Dto;
6+
7+
/**
8+
* OAuth error response as defined in RFC 6749 Section 4.1.2.1.
9+
*/
10+
final readonly class OAuthErrorResponse implements \JsonSerializable
11+
{
12+
public function __construct(
13+
public string $error,
14+
public ?string $errorDescription = null,
15+
public ?string $errorUri = null,
16+
) {}
17+
18+
public function jsonSerialize(): array
19+
{
20+
return \array_filter([
21+
'error' => $this->error,
22+
'error_description' => $this->errorDescription,
23+
'error_uri' => $this->errorUri,
24+
], static fn($value) => $value !== null);
25+
}
26+
}

0 commit comments

Comments
 (0)