Security
What the library handles
- Tokens never appear in errors or logs.
Bearervalues, the four JSON and form token fields, and bare JWTs are stripped from every thrown message, log line, and nested diagnostic: redacted before truncation, so a cut string cannot defeat the scrubber. Enforced by tests in every port. - Callback state is compared in constant time, and validated before the
codeorerrorparameter is read at all. - The loopback listener binds
127.0.0.1only, never0.0.0.0. It accepts one path, stops after the first result, times out after five minutes, and never interpolates callback values into its HTML response. - Credentials are encrypted at rest in every built-in store, with a typed error on authentication failure rather than a silent empty read.
- PKCE with S256. No client secret exists, because this is a public client.
What is yours
Deriving the subject. The library takes whatever you pass. If that comes from a request body, you have built a way for one user to spend another’s subscription. See Concepts.
Your credential store, if you wrote one. Atomic CAS, encryption at rest, and loud failures. See Storage.
Session and CSRF protection on your own routes. The login and logout routes you expose are state-changing endpoints in your application, and they need whatever protection the rest of your app uses.
Transport to your own server. HTTPS, cookie flags, the usual.
JWT claims are unverified, by design
accountId, planType and email are read from the token payload without signature
verification, because this client has no trusted verification step in the protocol.
They are safe for exactly two things: routing metadata (chatgpt-account-id on outbound requests)
and display. They must never determine identity, permissions, row ownership, or any authorization
decision. extractUnverifiedClaims is named the way it is on purpose.
Threats worth thinking about
A stolen refresh token is a stolen subscription. It is long-lived and, until revoked, sufficient. Encrypt at rest, keep it off logs, and scope database access accordingly.
Local processes on a developer machine can reach the loopback listener during the five-minute
sign-in window. It refuses to settle on a callback whose state does not match, so a process that
cannot guess a 32-byte secret cannot complete or cancel a sign-in.
A compromised dependency in your build can read anything your process can. The TypeScript package ships zero runtime dependencies for this reason, and the repository pins a minimum release age and refuses provenance downgrades for its own build tooling.
Multi-tenant pooling is the failure this design exists to prevent. One subject, one credential row, one app-server process. Do not build a shared pool or resell subscription access.
Before you ship
- The subject comes from your session, never from the request.
- Your store’s CAS is one atomic operation, and returns the winner on conflict.
- Credentials are encrypted at rest with a key that is not stored beside them.
- A
storeerror is surfaced as an error, not as “logged out”. -
reauth_requiredprompts a fresh sign-in rather than retrying. - No JWT claim is used for an authorization decision.
- You have an API-key or local-model fallback for when this breaks.
- Your logs have been checked for token-shaped strings.
Reporting something
Open an issue for anything non-sensitive. For a vulnerability, use GitHub’s private security advisory reporting on the repository rather than a public issue.