feat(auth): make Sim an OAuth 2.1 provider and move the CLI onto it - #7488
Draft
waleedlatif1 wants to merge 1 commit into
Draft
feat(auth): make Sim an OAuth 2.1 provider and move the CLI onto it#7488waleedlatif1 wants to merge 1 commit into
waleedlatif1 wants to merge 1 commit into
Conversation
Sim becomes an authorization server, so other software can sign a person in and act as them with their consent. `sim login` uses it by default, replacing a permanent personal API key with a short-lived, scoped, revocable token. Built on `@better-auth/oauth-provider@1.6.27`, pinned to match the Better Auth already in the tree — no upgrade required. Tokens are opaque rather than JWTs, so revoking an app in settings or running `sim logout` takes effect on the very next request. Access tokens last an hour; refresh tokens rotate on every use and expire after thirty days. A new principal kind, `oauth_access_token`, is admitted exactly where `personal_api_key` is and nowhere else; `check:principal-kind-parity` fails the build if the two ever drift apart. Write scope is derived from the HTTP method at v2 admission rather than from an operation's `minimumRole`, because several POST routes only read — a role-derived rule let a read-only token write. The consent card refuses to render for a request Sim did not issue: an unsigned query, a repeated parameter, or a client the server declines to name. Clients are operator-created rows. Dynamic registration is off, and every one of the plugin's client CRUD endpoints is closed at the source. Off with `OAUTH_PROVIDER_ENABLED=false`, which the CLI detects and falls back to the existing pairing-code handoff.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sim becomes an OAuth 2.1 authorization server, so other software can sign a person in and act as them with their consent.
sim loginuses it by default, replacing the permanent personal API key it used to mint with a short-lived, scoped, revocable token.Built on
@better-auth/oauth-provider@1.6.27, pinned to match thebetter-authalready in the tree — no upgrade required.Why
Today the only programmatic credential is an API key: permanent, unscoped, and useful to nothing but the CLI. This makes "a Sim login" something other software can obtain — the CLI and SDKs first, then MCP clients, partner integrations, and eventually "Sign in with Sim".
Design decisions worth reviewing
Opaque tokens, not JWTs. A JWT carries no
jtiand cannot be revoked; revoking an app in settings or runningsim logouthas to take effect on the very next request. Verification is one indexed read, the same cost as the API-key path. The cost is that client secrets are stored reversibly — with no JWKS the plugin signs ID tokens with the client secret, so it must read it back. SettingstoreClientSecret: 'hashed'is refused at construction and would take the app down on boot.Write scope comes from the HTTP method, not
minimumRole. Several POST routes only read (search, query, count), so deriving the required scope from an operation's role let a read-only token perform writes. Those four routes opt out withreadOnly: true, and a definition-time guard rejects that flag on a method that is already safe.A new principal kind admitted exactly where
personal_api_keyis.check:principal-kind-parityfails the build if the two drift apart. Its own header is explicit about what it cannot see — it reads inlineprincipalKindsliterals, notswitch (principal.kind)comparisons, which is the class of site that mis-admits silently.The consent card fails closed. It refuses to render a grant for a request Sim did not issue: an unsigned query, a repeated parameter (
URLSearchParams.getreads the first, the server the last), or a client the server declines to name. The first-party CLI never skips consent.Clients are operator-created rows. Dynamic registration is off, and
clientPrivileges: () => falsecloses every one of the plugin's client CRUD endpoints at the source — the route-level blocklist is defence in depth, not the only thing standing between a signed-in user and/oauth2/create-client.The CLI asks only for what it uses. No
openid profile email: a public client with no secret can never receive anid_token, and the CLI reads no profile or email. Asking would put three permissions on the consent card that the app cannot honour.CLI
Authorization code + PKCE S256 on a loopback listener (RFC 8252),
statechecked, refresh rotated. The pairing-code handoff stays for terminals whose browser cannot reach a local port, selected automatically over SSH.--read-onlyand--callback-portare refused on that path rather than silently ignored, since it can honour neither.Concurrent
simprocesses are serialised by a lock file carrying an owner token, so a reclaimed stale lock can never be removed by the waiter that observed it going live again. The lock is re-entrant by async context — a flag would let two genuinely concurrent refreshes both believe they held it, which is the exact failure it exists to prevent.Operations
OAUTH_PROVIDER_ENABLED=falseturns the whole thing off; discovery 404s and the CLI falls back on its own.Two migrations:
0321adds four tables and seeds the CLI client;0322adds aUNIQUE NULLS NOT DISTINCTconstraint on the consent grant key (the plugin's consent write is a non-atomic read-then-create) plus theexpires_atindexes the cleanup sweep uses. Requires Postgres 15+.Verification
turbo run type-check26/26 ·check:audits46/46 ·check:application-graphcleanNot done here
GET /api/cron/cleanup-oauth-tokensships and is documented, but neither shipped deployment schedules the retention endpoints. Without it, each CLI login leaves a lapsed access-token row behind every hour.