The problem
On self-host, an API key can mint more API keys.
The apiKey plugin runs with enableSessionForAPIKeys (better-auth.ts, L170). Its before-hook turns any request that carries a valid x-api-key header into its owner's session, on every Better Auth endpoint. /api/auth/* goes straight to Better Auth (app.ts), and the account provider hands the raw request headers to Better Auth (getSession better-auth-account-provider.ts, createApiKey L89-L91).
Reading the code, a request with a key in x-api-key to POST /api/account/api-keys (or POST /api/auth/api-key/create) gets a session and creates a new key for the same user. Listing and revoking keys work the same way. I traced this through the source on current main and @better-auth/api-key 1.6.x; I haven't reproduced it against a running instance.
The result: once a key leaks, revoking that key doesn't contain the leak, because the holder may already have minted others. The only safe response is to revoke every key on the account.
Cloud already draws this line. /account/* accepts only the browser session cookie, "NOT api-key Bearer" (account-api.ts). #2077 applies the same rule to its new /api/access/* routes and refuses any request carrying Authorization or x-api-key.
Proposed shape
On self-host, refuse key management when the caller authenticated with an API key (the x-api-key header, or a Bearer value that resolves only as an API key):
/api/account/api-keys list, create and revoke;
- Better Auth's own
/api/auth/api-key/* endpoints.
It's worth checking the other account-changing Better Auth endpoints too (password and profile changes, organization invites), since the same key-derived session reaches them.
Why it matters for security
An API key should be able to do what its owner allowed, and not make itself harder to revoke. Keys are handed to agent runtimes precisely because those runtimes are the likeliest place for a credential to leak.
Alternatives
Operators can watch the key list and revoke anything they didn't create, but that's detection after the fact, and it only works if nothing else creates keys on the account.
Where it belongs
Self-host (Docker)
Before you submit
The problem
On self-host, an API key can mint more API keys.
The
apiKeyplugin runs withenableSessionForAPIKeys(better-auth.ts, L170). Its before-hook turns any request that carries a validx-api-keyheader into its owner's session, on every Better Auth endpoint./api/auth/*goes straight to Better Auth (app.ts), and the account provider hands the raw request headers to Better Auth (getSessionbetter-auth-account-provider.ts,createApiKeyL89-L91).Reading the code, a request with a key in
x-api-keytoPOST /api/account/api-keys(orPOST /api/auth/api-key/create) gets a session and creates a new key for the same user. Listing and revoking keys work the same way. I traced this through the source on current main and@better-auth/api-key1.6.x; I haven't reproduced it against a running instance.The result: once a key leaks, revoking that key doesn't contain the leak, because the holder may already have minted others. The only safe response is to revoke every key on the account.
Cloud already draws this line.
/account/*accepts only the browser session cookie, "NOT api-key Bearer" (account-api.ts). #2077 applies the same rule to its new/api/access/*routes and refuses any request carryingAuthorizationorx-api-key.Proposed shape
On self-host, refuse key management when the caller authenticated with an API key (the
x-api-keyheader, or a Bearer value that resolves only as an API key):/api/account/api-keyslist, create and revoke;/api/auth/api-key/*endpoints.It's worth checking the other account-changing Better Auth endpoints too (password and profile changes, organization invites), since the same key-derived session reaches them.
Why it matters for security
An API key should be able to do what its owner allowed, and not make itself harder to revoke. Keys are handed to agent runtimes precisely because those runtimes are the likeliest place for a credential to leak.
Alternatives
Operators can watch the key list and revoke anything they didn't create, but that's detection after the fact, and it only works if nothing else creates keys on the account.
Where it belongs
Self-host (Docker)
Before you submit