Reference implementation of the OAuth 2.0 Authorization Code + PKCE localhost-callback flow for adding Clerk authentication to command-line tools.
Runtime-agnostic TypeScript (Node.js 18+, Bun), ~300 lines of real code, tested end-to-end against a real Clerk dev instance.
Status: example / prototype. Fork it, copy it, or file-link it. Not a published package.
Adding a "sign in with Clerk" flow to a CLI follows a well-known pattern (PKCE + localhost callback + keychain storage), but the details are non-obvious if you've never built one. This repo is the documented reference. See the companion write-up at clerk.com/blog/adding-clerk-auth-to-your-cli (link to be finalized).
You need two things: an OAuth Application registered with a Clerk instance, and the client_id + issuer URL from it.
Pick whichever path fits your workflow.
Clerk Dashboard (recommended for most devs) — in your dev instance, go to Configure → OAuth Applications → Create. Set:
- Name: your CLI's name
- Redirect URI:
http://127.0.0.1:*/callback(wildcard port; if wildcard isn't offered, use a specific port likehttp://127.0.0.1:8787/callbackand configure the SDK withcallbackPort: 8787) - Public client (PKCE): enabled
- Scopes:
profile email openid offline_access
curl against BAPI — if you prefer scripting. Replace $SK with your instance's secret key:
curl -X POST https://api.clerk.com/v1/oauth_applications \
-H "Authorization: Bearer $SK" \
-H "Content-Type: application/json" \
-d '{
"name": "my-cli",
"redirect_uris": ["http://127.0.0.1:0/callback"],
"public": true,
"pkce_required": true,
"scopes": "profile email openid offline_access"
}'Clerk CLI (if you have it installed) — clerk api /oauth_applications --instance <ins_...> -X POST -d '<payload>' --yes does the same thing with keychain-based auth, no secret-key-in-env.
All three paths return a JSON object with client_id. Grab it along with your instance's Frontend API URL (the issuer, e.g. https://clerk.your-subdomain.accounts.dev or a custom domain like https://clerk.yourapp.com).
export CLERK_OAUTH_CLIENT_ID="..." # from step 1
export CLERK_ISSUER="https://clerk.your-subdomain.accounts.dev"import { ClerkCliAuth } from "@clerk/cli-auth";
const auth = new ClerkCliAuth({
clientId: process.env.CLERK_OAUTH_CLIENT_ID!,
issuer: process.env.CLERK_ISSUER!,
scopes: ["profile", "email", "openid", "offline_access"],
storage: "keychain",
keychainService: "my-cli",
});
// Opens a browser, starts a one-shot localhost listener, exchanges the code,
// stores tokens in the OS keychain. Returns the token set and userinfo.
const { tokens, user } = await auth.login();
// Returns the cached access token; auto-refreshes when within 30s of expiry.
const token = await auth.getAccessToken();
// Reads the cached user. If no cache, fetches from /oauth/userinfo.
const me = await auth.whoami();
// Clears keychain + cached userinfo.
await auth.logout();1. CLI generates PKCE (code_verifier, code_challenge=S256(verifier)) + CSRF state.
2. CLI binds a one-shot HTTP server on 127.0.0.1:0 (random port).
3. CLI opens browser to:
{issuer}/oauth/authorize?client_id=...&code_challenge=...
&redirect_uri=http://127.0.0.1:{port}/callback&state=...
&code_challenge_method=S256
4. User signs in via Clerk's hosted UI and approves consent.
5. Clerk redirects the browser to http://127.0.0.1:{port}/callback?code=...&state=...
6. Server validates state, responds with "You can close this tab", closes.
7. CLI posts to {issuer}/oauth/token with grant_type=authorization_code + code_verifier.
8. CLI stores the token set in the OS keychain (falls back to chmod 600 JSON file).
bun install
bun run typecheck
bun test
bun run build # emits dist/index.cjs + dist/index.mjs + .d.ts10 tests across 4 files cover PKCE correctness, localhost server happy path + state mismatch + timeout, credential store round-trips, and end-to-end login against stubbed OAuth endpoints.
- No token revocation on logout. Logout only clears local storage; the refresh token remains valid on Clerk's side until it expires or is explicitly revoked via
/oauth/token/revoke. - Keychain path is tested structurally, not in CI. Keychain access triggers OS credential-manager prompts in headless environments, so automated tests use memory and file stores. The keychain path does get exercised end-to-end when you run the demo against a real Clerk instance.
- Device Authorization Grant (RFC 8628) is not implemented. The localhost-callback flow needs an open port, which doesn't work for CI, containers, or SSH sessions. If you need that, open an issue.
MIT