- PHP 8.2 or newer.
- A PSR-17 implementation (response and stream factories), for example
nyholm/psr7orlaminas/laminas-diactoros. The server builds PSR-7 responses with them. ext-pdowith a driver (SQLite, MySQL/MariaDB or PostgreSQL) if you usePdoStorage.ext-curlif you accept Client ID Metadata Documents (the default). Without it, either pass your ownHttpTransportInterfacetoAuthorizationServeror setcimd.enabledtofalse.- Optional:
mcp/sdk0.9 or newer, only formcpTokenValidator(). An older SDK is refused with aLogicExceptionwhen the validator is built. See MCP.
The library has no mbstring dependency.
composer require cakedc/oauth-server nyholm/psr7PdoStorage uses four tables: oauth_clients, oauth_authorization_codes, oauth_tokens and
oauth_consents. The simplest way to create them is from PHP:
use CakeDC\OAuth\Server\Storage\Pdo\PdoStorage;
$pdo = new PDO('sqlite:/var/lib/myapp/oauth.sqlite');
PdoStorage::createTables($pdo);createTables(PDO $pdo, string $tablePrefix = 'oauth_') picks the file in schema/ from the PDO driver
name and runs it. It uses CREATE TABLE IF NOT EXISTS, so it is safe to call again. If you use a
table prefix other than oauth_, pass the same prefix to new PdoStorage($pdo, $prefix).
If you prefer to run SQL yourself (a migration tool, for example), use the files directly:
| Database | File |
|---|---|
| SQLite | schema/sqlite.sql |
| MySQL, MariaDB | schema/mysql.sql |
| PostgreSQL | schema/pgsql.sql |
MySQL tables use utf8mb4 with the binary collation utf8mb4_bin, so subjects and ids compare
case-sensitively. Open your MySQL connection with charset=utf8mb4 in the DSN. See
storage for every column.
You need two interfaces from your application: UserProviderInterface and
UserContextInterface. This example assumes a class MyUsers that implements both.
<?php
declare(strict_types=1);
use CakeDC\OAuth\Server\AuthorizationServer;
use CakeDC\OAuth\Server\Config\ServerConfig;
use CakeDC\OAuth\Server\Storage\Pdo\PdoStorage;
use Nyholm\Psr7\Factory\Psr17Factory;
require __DIR__ . '/vendor/autoload.php';
$pdo = new PDO('sqlite:' . __DIR__ . '/oauth.sqlite');
PdoStorage::createTables($pdo);
$users = new MyUsers($pdo);
$factory = new Psr17Factory();
$server = new AuthorizationServer(
ServerConfig::fromArray([
'issuer' => 'https://auth.example.com',
// At least 32 characters (for example 32 random bytes, hex or base64 encoded). Keep it out of version control.
'secret' => getenv('OAUTH_SECRET'),
'resources' => [
'https://auth.example.com/api' => [
'name' => 'Example API',
'scopes' => ['read' => 'Read your profile', 'write' => 'Change your profile'],
'implies' => ['write' => ['read']],
'defaultScopes' => ['read'],
],
],
]),
new PdoStorage($pdo),
$users, // UserProviderInterface
$users, // UserContextInterface
$factory, // ResponseFactoryInterface
$factory, // StreamFactoryInterface
);Generate a secret once with php -r 'echo bin2hex(random_bytes(32)), PHP_EOL;' and keep it in the
environment. All settings are listed in configuration.
The constructor takes more optional arguments: an event dispatcher ($events), a PSR-16 cache
for rate limiting ($cache), a PSR-20 clock, a PSR-3 logger, a CIMD transport and DNS resolver,
Messages, custom page renderers and a $clientIp closure. They are covered where they matter
(events, security, pages, deployment).
The server gives you one PSR-15 request handler per endpoint. You decide how requests reach them. Compare the request path with the paths the configuration computes:
| Handler | Path | Methods |
|---|---|---|
metadataHandler() |
$config->metadataPath() |
GET, OPTIONS |
authorizeHandler() |
$config->endpointPath('authorize') |
GET, POST |
tokenHandler() |
$config->endpointPath('token') |
POST, OPTIONS |
registerHandler() |
$config->endpointPath('register') |
POST, OPTIONS |
revokeHandler() |
$config->endpointPath('revoke') |
POST, OPTIONS |
introspectHandler() |
$config->endpointPath('introspect') |
POST, OPTIONS |
connectionsHandler() |
$config->endpointPath('connections') |
GET, POST |
With the default routePrefix of /oauth and the issuer above the paths are
/.well-known/oauth-authorization-server, /oauth/authorize, /oauth/token, /oauth/register,
/oauth/revoke, /oauth/introspect and /oauth/connections. If the issuer has a path, for example
https://example.com/auth, the metadata path is
/.well-known/oauth-authorization-server/auth and the endpoints live below /auth/oauth/.
$config->endpointUrl('token') and $config->metadataUrl() give the absolute URLs.
A minimal front controller, adapted from examples/standalone/server.php:
$config = $server->config();
$path = $request->getUri()->getPath();
$response = match (true) {
$path === $config->metadataPath() => $server->metadataHandler()->handle($request),
$path === $config->endpointPath('authorize') => $server->authorizeHandler()->handle($request),
$path === $config->endpointPath('token') => $server->tokenHandler()->handle($request),
$path === $config->endpointPath('register') => $server->registerHandler()->handle($request),
$path === $config->endpointPath('revoke') => $server->revokeHandler()->handle($request),
$path === $config->endpointPath('introspect') => $server->introspectHandler()->handle($request),
$path === $config->endpointPath('connections') => $server->connectionsHandler()->handle($request),
$path === '/api/me' => $server->bearerMiddleware('https://auth.example.com/api')->process($request, $me),
default => new Nyholm\Psr7\Response(404),
};$request is a PSR-7 ServerRequestInterface. Build it from the PHP globals with your PSR-7
implementation (see examples/standalone/server.php) or let your framework do it
(frameworks).
Then check the discovery document:
curl -s https://auth.example.com/.well-known/oauth-authorization-serverYou can try everything locally with the example in examples/standalone/README.md.
- Users: connect your login and your permissions.
- Clients: register a client or connect Claude.
- Security and deployment before you go to production.