The security model (what the server protects against, RFC 9700 item by item, refresh token rotation and reuse detection, SSRF protections for metadata documents, secrets at rest) is the core's. Read it in security. The plugin wraps the core and never relaxes what it enforces. This page lists what the plugin adds and what only you can do.
- Cake's CSRF stays on the consent and connections forms. By
OAuthServer::csrfSkip()only the token, register, revoke and introspect endpoints are skipped, plus, on the routes of a resource (see the next point), requests with a bearer token of this server's prefix (a page on another site cannot make a browser attach one without a CORS preflight approved with credentials, and an expired or revoked token gets the authenticator's 401) and requests without any credential (noAuthorizationheader, no cookie but the CSRF cookie: CSRF has nothing to protect there). A request without a token that carries a session or other cookie gets the normal CSRF check, and so do the login page and the consent and connections pages. The forms are built withFormHelper; a template override must do the same (pages). The core also signs the forms withOAuthServer.secret. - The Token authenticator comes first. A bearer request skips CSRF without its token being looked up, so a
session cookie sent with a made up token must never reach the session authenticator. With the Token
authenticator before the session, cookie and form authenticators it gets the 401 challenge instead;
oauth checkfails on any other order (protecting-apis). - A token works only on the routes of its resource. The Token authenticator authenticates a token only
when both the request path and the static part of the template of the route that matched it are below
the path of the resource it was issued for (
/apiforhttps://example.com/api, or itspathsoption); anywhere else a request carrying this server's token is refused with 401, andOAuthServer::csrfSkip()skips CSRF on a resource's routes only by the same rule. A consented token therefore cannot act as the person on a session page whose route is not connected below the resource's path, including routes that start with a placeholder (/{tenant}/...). What you connect below that path is part of the resource, and inside it your actions still check scopes (protecting-apis). - A bearer token is never a browser login. The consent and connections pages accept a session login only. Stateless authenticators (Token, HttpBasic, JWT, ...) and impersonation are refused, so a token cannot approve a client or mint tokens.
- The Token authenticator is stateless and only challenges its own tokens. Nothing it authenticates is persisted in a session, and it never answers 401 for other schemes (protecting-apis).
- The authorization check is skipped only for the plugin's own endpoints.
OAuthControllercallsskipAuthorization()oncakephp/authorizationfor its requests and never for any other controller. - Pages keep the core's hardening headers (
no-store,X-Frame-Options: DENY, a restrictive Content-Security-Policy,Referrer-Policy: no-referrer). With an application layout, the policy is extended for same-origin styles, images and fonts only, never for scripts or framing (pages). - Subjects are matched exactly.
OrmUserProvidercompares the key as a string and refuses a different spelling of the same number; the tables useutf8mb4_binon MySQL and MariaDB. - Inactive people stop at once. The person is loaded again with your finder on every use.
- The pages never headline a client by a name it chose. The consent page names a metadata document client
by its
client_idhost and a dynamically registered client by the host its answer goes to ("An application on this computer" for a private-use scheme); the declared name appears only inside a<q>element on an unverified line, so it cannot pose as the server's text. The connections page labels a dynamically registered client by the hosts of its redirect URIs. The shipped templates do this; keep it in overrides (pages). - No redirect before the login, and none to a client nobody vouched for. The core sends an error back to the
client only after the person signed in, and only when an administrator registered the client or this person
approved it before. Otherwise the error page names the destination and offers a plain link
(
CakeErrorPageRendererimplements the core'sRedirectNoticeRendererInterface; pages). - A remembered consent does not answer for a client whose identity cannot be confirmed. A public client with a loopback or private-scheme redirect (any program on the computer can receive the answer) sees the consent page on every connection, with a line saying the person approved it before.
- Never logging tokens. The plugin's own code does not write token values, client secrets or codes to
any log or event. Its parameters that carry them (the configuration with
secretandregistration.initialAccessToken, the test trait's bearer token) are#[\SensitiveParameter], like the core's, so a stack trace in a log showsObject(SensitiveParameterValue)instead. Frames of other libraries are outside this (the core's secrets in logs and traces names them), and so is your own code: keepzend.exception_ignore_args = Onin production (the default of PHP's productionphp.ini), which drops every argument from traces. - A derived secret needs a real salt. Without
OAuthServer.secret, the secret is derived fromSecurity.salt, which must be at least 32 characters.
The plugin logs with the scope oauth. When no log engine receives that scope (the skeleton's debug
and error engines have no scopes, so they do not), the plugin registers a FileLog named oauth that
writes logs/oauth.log. bin/cake oauth check reports Logging and warns when security events would
not be recorded. To send them elsewhere, configure your own engine for the scope:
'Log' => [
'oauth' => [
'className' => 'Cake\Log\Engine\FileLog',
'path' => LOGS,
'file' => 'oauth',
'scopes' => ['oauth'],
],
],Events (the audit trail) are described in events.
- Cut a person off. Deactivating the account is enough (tokens stop validating). In every other case
run
bin/cake oauth token revoke --subject ID --consents, so tokens and remembered consents go. - Revoke after policy changes.
canAuthorize()runs at/authorizeonly. Revoke tokens when the rule changes (users). - Introspection is a privilege.
allowIntrospectionlets a client learn about any token of its resource; grant it only to resource servers you trust. - Check scopes on your routes. Without
users.resourcethe authenticator accepts default-resource tokens on the routes below the default resource's path; checkOAuthServer::token($request)->hasScope()where it matters. Keep session pages out of your resources' paths. Think twice before settingpathsortokenPathsto['/']: it makes tokens work on every route. - Do not run the endpoints inside a request-wide transaction (deployment).
- Listeners must not throw (events).
-
bin/cake oauth checkhas noFAIL, and you read everyWARN. -
OAuthServer.issuer(orApp.fullBaseUrl) is set explicitly to the public https URL, never left to CakePHP'sHostheader fallback (deployment); subdirectory installs have the well-known rewrite (deployment). -
OAuthServer.secretis set from the environment (at least 32 random characters), not derived, and not in version control. -
rateLimit.cachenames a cache shared by all PHP processes. It is notnull. -
trustProxyis enabled only when the application is firewalled so that only your proxies reach it (Cake does not checkREMOTE_ADDRagainst the list), with the proxies listed (deployment). -
OAuthServer::csrfSkip()is in yourCsrfProtectionMiddleware,OAuthServer.csrfCookieNamematches itscookieName, and the Token authenticator comes before the session, cookie and form authenticators. - Your CORS policy never allows credentials for other sites on the routes of your resources (that would let their pages send bearer requests through a visitor's browser).
-
bin/cake oauth pruneruns daily. - Registration is
closedortokenunless you needopen;cimd.allowedHostslists the hosts you expect (includeclaude.aiif you use it). - Features you do not use are off:
introspection,clientCredentials,personalTokens. -
tokenPrefixnames your product. - Anthropic's egress range
160.79.104.0/21is not blocked or challenged on the OAuth and MCP paths (it stays subject to the rate limits). - A listener records
OAuthServer.refreshTokenReusedandOAuthServer.tokenRevoked. - Revocation is wired to the events that matter to you: password change, account removal, role change.
- Backups of the
oauth_*tables are encrypted. (OrmStorageruns on the connection of the plugin's tables and the people table is read through your own table, usually thedefaultconnection, so the database user of the application needs both; only a separate connection for the OAuth tables could narrow it.) -
bin/cake oauth checkhas no warning about query logging leaking tokens: with cakedc/users, the one-time login authenticator is the plugin's (cakedc-users).