Persisting tokens and registrations¶
Both stores default to memory, through the shipped InMemoryTokenStore and InMemoryClientRegistrationStore,
so a restart authorizes again. For tokens, the shipped EncryptedFileTokenStore outlives the process.
Implement the interfaces for anything else:
interface TokenStoreInterface
{
public function read(string $resource): ?AccessToken;
public function write(string $resource, AccessToken $token): void;
public function forget(string $resource): void;
}
interface ClientRegistrationStoreInterface
{
public function read(string $issuer): ?ClientRegistration;
public function write(string $issuer, ClientRegistration $registration): void;
public function forget(string $issuer): void;
}
EncryptedFileTokenStore¶
The file-backed store keeps its whole token map in one file, encrypted with XChaCha20-Poly1305 (it needs
ext-sodium, which most PHP builds bundle). The file is created owner-only (0600) and replaced atomically
on every write.
use Nexus\Mcp\Client\Auth\EncryptedFileTokenStore;
$store = new EncryptedFileTokenStore('/var/lib/myapp/mcp-tokens.enc', $key);
The key is 32 raw bytes. Generate it once with random_bytes(32), keep it outside the file's directory (an
environment variable or a secret manager), and hand the same bytes to every process sharing the file. A file
that does not decrypt with the configured key is refused rather than silently emptied, so a rotated or
mistyped key surfaces as an error instead of a quiet re-authorization. Delete the file to start over.
Keys and issuers¶
Tokens are keyed by the MCP server, and registrations by the issuer. Each AccessToken carries the issuer
that minted it. That is what makes an authorization server change safe. A token stamped with an issuer the
resource no longer names is dropped rather than presented or refreshed at the new one.
Reading a token back from a store therefore costs one discovery round trip before the first request goes out. The SDK must not send a token to a server other than the one that issued it, and until discovery has run it cannot tell. Later requests in the same process present the stored token directly.
A registration the authorization server stops recognising is dropped from the store rather than presented again.
One whose client_secret_expires_at has passed is registered again before it is presented, so a store persisting
ClientRegistration must keep that field alongside the secret.
Store both confidentially. They are credentials.
Sharing a store across workers¶
Grants and renewals run under a lock so that one client never redeems a refresh token twice. That lock is an
Amp\Sync\Semaphore, and the default spans one process only. Two workers sharing a persisted token store can
therefore both renew the same token, which an authorization server with reuse detection treats as theft and
answers by revoking the whole grant.
Pass a cross-process semaphore as the lock argument of AuthorizedHttpClient whenever the store is shared, for
example Amp\Sync\PosixSemaphore. Holding it, a worker re-reads the store before renewing, so the second worker
finds the token the first one just obtained and presents it instead.