The plugin registers six commands, all run with bin/cake (inside DDEV: ddev exec bin/cake ...). The
commands use the same configuration and storage as the web endpoints.
| Command | Purpose |
|---|---|
oauth client add |
Register a client by hand |
oauth client list |
List the clients |
oauth client revoke |
Revoke a client and its tokens |
oauth token revoke |
Revoke the tokens of a person or a client |
oauth prune |
Delete expired data |
oauth check |
Check the setup |
bin/cake oauth client add NAME [--redirect-uri URI]... [--confidential] [--grant GRANT]...
[--scope SCOPE]... [--introspection] [--native]Registers a client (type Registered). NAME is the display name: up to 255 characters, with at least one
letter, number, punctuation or symbol, and without control characters, bidirectional controls or other
invisible characters (the core's name rules; joiners inside Persian or Indic words and emoji sequences are
fine). A name that breaks them prints the core's error and exits with a non-zero code. Private-use redirect URIs
(com.example.app:/oauth/callback) are accepted here whatever registration.allowPrivateSchemes says.
| Option | Meaning |
|---|---|
--redirect-uri |
A redirect URI. Repeat for more |
--confidential |
Give the client a secret (HTTP Basic authentication at the token endpoint) |
--grant |
authorization_code, refresh_token or client_credentials. Repeat for more. Default: the first two |
--scope |
A scope the client may ever get. Repeat for more. Default: any scope the resources declare |
--introspection |
Allow the client to call /introspect. Confidential clients only; a trusted resource server privilege |
--native |
The client is a native application (loopback or private-scheme redirect URIs) |
Examples:
# A web application with a secret
bin/cake oauth client add "Reporting dashboard" --confidential \
--redirect-uri https://dashboard.example.com/oauth/callback
# A service acting for itself (also needs userProvider.forClient, see users.md)
bin/cake oauth client add "Nightly import" --confidential --grant client_credentials --scope content:readOutput:
client_id: 0b6f0a3c...
client_secret: (the secret, shown once)
Store the secret now; it is not shown again.
The last line is a warning and goes to standard error, so bin/cake oauth client add ... > client.txt keeps only
the id and the secret in the file.
The secret appears only for confidential clients, once. A refused input (for example a public client with client credentials) prints the error and exits with a non-zero code. The service identity for the client credentials grant is described in users.
bin/cake oauth client listPrints a table of every client: client id, name, hosts, type, authentication method, created, last used and
revoked (- when empty). The name is what the client declared (a self-registered client chooses it, so the
column is headed "Name (declared)"); the hosts are the web hosts of its redirect URIs, plus "An application on
this computer" when one of them is a private-use scheme. Clients created from metadata documents (MetadataDocument) and by dynamic
registration (Dynamic) are listed next to the ones you added.
Client id Name (declared) Hosts Type Authentication Created Last used Revoked
0b6f0a3c... Reporting dashboard reports.example.com registered client_secret_basic 2026-10-16 12:00 2026-10-16 12:30 -
(The real output is drawn as a table with borders.)
bin/cake oauth client revoke CLIENT_IDMarks the client revoked, forever, and revokes every token it holds:
Revoked the client and 3 token(s).
An unknown id prints No client with that id. and exits with a non-zero code.
bin/cake oauth token revoke --subject ID [--consents]
bin/cake oauth token revoke --client CLIENT_ID| Option | Meaning |
|---|---|
--subject |
The person's id (the primary key of the users table). Revokes their access, refresh and personal tokens, and deletes the authorization codes they have not exchanged yet |
--consents |
With --subject: also delete the person's remembered consents, so no client they approved before can skip the consent page |
--client |
The client id. Revokes every token of that client and deletes its codes not exchanged yet (the client itself stays valid; use oauth client revoke to end it) |
Pending codes are deleted before the tokens are revoked, so a code issued a moment before cannot open a new connection afterwards; an exchange already in flight either fails or has its tokens revoked. Exchanged codes are kept, so a replayed code still revokes what it issued.
Revoked 4 token(s).
Deleted 2 consent(s).
The Deleted ... consent(s) line appears with --consents. Pass either --subject (optionally with
--consents) or --client; any other combination prints
Pass either --subject (optionally with --consents) or --client. and fails.
To cut a person off completely, use --subject ID --consents.
bin/cake oauth pruneDeletes what is no longer needed: expired authorization codes, expired and revoked tokens (kept for
retentionDays first) and clients nobody needs any more. Revoked clients stay as records, because revocation
is permanent. It prints:
Deleted 12 codes, 340 tokens and 1 clients.
Run it daily:
# crontab -e (as the user that runs the application)
17 3 * * * cd /var/www/app && bin/cake oauth prune >> logs/oauth_prune.log 2>&1
Pruning does not weaken the family lifetime cap: it is stored with every token (family_started_at).
Pruning is safe while the application serves requests: each unused client is claimed with one conditional
UPDATE inside its own transaction (after locking its row on MySQL, MariaDB and PostgreSQL) and deleted only
when the claim succeeds, so a client that a request used, or gave a new token, after oauth prune picked it is
kept with its token.
More in the core's operations.
bin/cake oauth checkRuns the checks below and prints one line each, [PASS], [WARN] or [FAIL]. The exit code is non-zero
when any check fails, so it is suitable for a deployment pipeline.
[WARN] Secret: derived from Security.salt; set OAuthServer.secret to rotate it without changing the salt
[PASS] Configuration: 1 resource(s), endpoints under https://example.com/oauth
[PASS] Issuer: https://example.com
[PASS] Tables: all four tables exist
[PASS] Users: Users with finder "active"
[PASS] Users integration: cakedc/users is not loaded
[PASS] Login URL: /users/login
[PASS] Routes: /oauth/token reaches the plugin
[PASS] Middleware: RoutingMiddleware runs before the CSRF and authentication middleware
[PASS] CSRF: the token, register, revoke and introspect endpoints are skipped
[PASS] Authenticators: Token comes before Session, Form
[PASS] RBAC: cakedc/users is not loaded
[PASS] Rate limiting: cache "default"
[PASS] Client metadata documents: enabled
[PASS] Logging: the "oauth" log engine is configured
| Check | Fails or warns when |
|---|---|
| Secret | OAuthServer.secret is shorter than 32 characters (fail), or is not set and Security.salt is missing or short (fail); a derived secret only warns |
| Configuration | Builds the server the way the endpoints do (no request, no I/O) and fails when that is refused: an unknown key or a bad type in any section (the plugin's users, consent, tokenPaths and so on included, and a setting 1.0.0 removed, such as services or users.paths, with its replacement), an undeclared resource, a tokenPrefix that is not 1 to 16 lowercase letters or digits, a missing rate limit cache. Whatever fails here makes the endpoints answer 503 |
| Issuer | The issuer is invalid (fail). Warns for plain http, for an issuer with a path (the RFC 8414 document must be served at the host root), and when the issuer path differs from a configured App.base. When App.base is false (Cake detects the directory per request) the CLI cannot compare them and says so instead of guessing |
| Tables | Inspects the connection of the plugin's tables (not necessarily default; the message names it). Fails when a table is missing, a column of the current migrations is missing (such as oauth_tokens.rotation_seal, added by 20261107120000_AddOAuthTokensRotationSeal.php) or a unique key is missing (client_id_hash, code_hash, token_hash, and subject, client and resource of a consent): migrate, see installation. On MySQL and MariaDB it also fails when a column is not utf8mb4_bin. Warns when the index on oauth_authorization_codes (subject) is missing (run bin/cake migrations migrate -p CakeDC/OAuthServer, or copy 20261017120000_AddOAuthCodesSubjectIndex.php among your application's migrations): revoking a person or a connection then scans every code. Each message names both ways to migrate |
| Users | The users table has no configured finder. A custom provider is reported as custom user provider |
| Users integration | Only with CakeDC/Users: OAuthServer.users is invalid, or the integration's resource is at the host root without paths in OAuthServer.tokenPaths (fail: the authenticator refuses every token); otherwise prints what the integration's Token authenticator validates and where. Warns when the one-time login authenticator (Auth.Authenticators.OneTimeToken) is not the plugin's guarded one, so it would read the token sent to the revoke and introspect endpoints (cakedc-users) |
| Login URL | Prints the resolved login URL; fails when it cannot be built. Warns when a local string URL matches no route or controller (another site's URL is not checked) |
| Routes | <prefix>/token does not reach the plugin's controller: no route, or a route of your application connected before the plugin's takes it (fail) |
| Middleware | Your middleware queue, plus what your plugins add, has CsrfProtectionMiddleware (or the session one) or AuthenticationMiddleware before RoutingMiddleware: there is no matched route yet, so no request on a resource's routes is skipped by csrfSkip() and the Token authenticator refuses every token. Fails when a Token authenticator of this plugin is loaded (its tokens cannot work, or every POST with one gets 403), warns otherwise. Also warns when there is no RoutingMiddleware |
| CSRF | The CsrfProtectionMiddleware or SessionCsrfProtectionMiddleware of your application does not skip the token endpoint (fail), or also skips a session route: it probes <prefix>/authorize, <prefix>/connections, / and your login (fail; a callback like fn () => true). Warns when there is no CSRF middleware in the queue (the consent and connections pages rely on it) or when it cannot inspect the middleware |
| Authenticators | Builds your authentication service with probe requests at your bearer paths. Fails when a session, cookie (remember-me) or form authenticator comes before the Token authenticator: OAuthServer::csrfSkip() skips CSRF for a request with a bearer token of this server without looking it up, and only the Token authenticator first keeps a session cookie sent with a made up token from being used. Also fails when the service cannot be built, or when a Token authenticator would refuse every token: an undeclared resource, a resource at the host root without token paths, an invalid OAuthServer.tokenPaths, or the paths or header option that 1.0.0 removed. Warns when there is no Token authenticator (ignore that when your resources live in another application). With cakedc/users it reads Auth.Authenticators |
| RBAC | Only with CakeDC/Users: RBAC denies the OAuth endpoints (fail). See cakedc-users |
| Rate limiting | rateLimit.cache is null (warn: the token endpoints are not throttled), the named cache configuration does not exist (fail), or it uses the Array or Null engine, which keeps nothing from one request to the next (warn: nothing is throttled) |
| Client metadata documents | Metadata documents are enabled but ext-curl is missing (fail), or rateLimit.cache is null or an Array or Null engine, so there is no fetch budget and no negative cache (warn) |
| Logging | No log engine receives the oauth scope (warn) |
No configuration breaks bin/cake: with an invalid one the routes are still connected (see
configuration), commands and migrations run, and oauth check
reports the problem on its Configuration line (or the line of the check that hit it). The web behaves the same
way: the application keeps working and only the OAuth endpoints answer 503. So a test of your application does
not fail on a bad OAuthServer setting unless it calls an OAuth endpoint; run oauth check in your pipeline.