CLI Auth
Build a CLI tool that authenticates users by opening a browser window, completing the OAuth2 flow, and storing tokens locally. This is the standard pattern for CLI tools like gh, fly, and railway — and it uses a public client (no client secret needed).
CLI tools use a public client because there’s no way to keep a secret confidential in code that runs on the user’s machine — anyone can inspect the binary or source. Instead of a client secret, this flow relies on PKCE (Proof Key for Code Exchange) to prevent authorization code interception attacks. The code_verifier is generated fresh for each login attempt and never leaves the local machine, so even if an attacker intercepts the authorization code, they can’t exchange it for tokens.
The browser-based login pattern is also a better user experience than asking users to paste API keys or passwords into a terminal. The user authenticates in their familiar browser with all their existing sessions, password managers, and multi-factor authentication. Popular CLI tools like gh auth login, fly auth login, and railway login all use this same approach.
What you’ll build
Section titled “What you’ll build”A CLI with three commands:
login— Opens the browser for authentication, stores tokens locallywhoami— Fetches and displays the authenticated user’s profilelogout— Clears stored tokens
Prerequisites
Section titled “Prerequisites”- Paylent running locally (
mix phx.server) - Example data loaded
- Bun (or Node.js 18+)
Configure in the dashboard
Section titled “Configure in the dashboard”This example uses a public OAuth client. Public clients don’t have a client secret, which is appropriate for CLI tools where the secret can’t be kept confidential.
-
Open the dashboard
Go to
http://dashboard.localhost:4000and log in. Select the examples-dev environment. -
Register a public client
Navigate to Clients in the sidebar, then click Register Application. Fill in:
- Name: My CLI Tool
- Client Type: Public
- Grant Types: Authorization Code, Refresh Token
- Redirect URIs:
http://localhost:9999/callback

No client secret is generated for public clients. The redirect URI
http://localhost:9999/callbackis where the CLI starts a temporary HTTP server to receive the callback.
Add the
client_idto the example’s.envfile.
Run the example
Section titled “Run the example”-
Install dependencies
Terminal window cd examples/cli-authbun install -
Log in
Terminal window bun loginThe CLI opens your default browser to Paylent’s login page. Log in with:
- Email:
[email protected] - Password:
password

After login, the browser shows a success message and you can close the tab. The CLI prints a confirmation:
Opening browser for authentication...Waiting for callback on http://localhost:9999/callback...Successfully logged in! - Email:
-
Check who you are
Terminal window bun whoamiOutput:
Logged in as: [email protected]{"sub": "019c...","email": "[email protected]","given_name": "Test","family_name": "User"} -
Log out
Terminal window bun logoutThis clears the locally stored tokens.
How it works
Section titled “How it works”Login flow
Section titled “Login flow”The CLI implements Authorization Code + PKCE with a temporary local HTTP server:
// 1. Start a temporary HTTP server to receive the callbackconst server = Bun.serve({ port: 9999, async fetch(req) { const url = new URL(req.url); const code = url.searchParams.get("code"); // Exchange code for tokens... return new Response("Login successful! You can close this tab."); },});
// 2. Generate PKCE verifier and challengeconst codeVerifier = generateCodeVerifier();const codeChallenge = await generateCodeChallenge(codeVerifier);
// 3. Open the browserconst authUrl = `${ISSUER}/oauth/authorize?...`;open(authUrl);Token exchange (no client secret)
Section titled “Token exchange (no client secret)”Since this is a public client, the token exchange omits the client_secret:
const response = await fetch(`${ISSUER}/oauth/token`, { method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", client_id: CLIENT_ID, // No client_secret — public client! code, code_verifier: codeVerifier, redirect_uri: redirectUri, }),});PKCE (code_verifier / code_challenge) protects the flow instead of a client secret.
Token storage
Section titled “Token storage”Tokens are stored locally in a file (managed by the token-store module), so subsequent commands can use them without re-authenticating.
Next steps
Section titled “Next steps”- Hono OIDC — Add login to a web app using OIDC middleware
- Authentication guide — Learn about public vs. confidential clients and when to use each