Skip to content

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.

A CLI with three commands:

  • login — Opens the browser for authentication, stores tokens locally
  • whoami — Fetches and displays the authenticated user’s profile
  • logout — Clears stored tokens

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.

  1. Open the dashboard

    Go to http://dashboard.localhost:4000 and log in. Select the examples-dev environment.

  2. 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

    Register public client

    No client secret is generated for public clients. The redirect URI http://localhost:9999/callback is where the CLI starts a temporary HTTP server to receive the callback.

    CLI Auth public client

    Add the client_id to the example’s .env file.

  1. Install dependencies

    Terminal window
    cd examples/cli-auth
    bun install
  2. Log in

    Terminal window
    bun login

    The CLI opens your default browser to Paylent’s login page. Log in with:

    Paylent login page

    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!
  3. Check who you are

    Terminal window
    bun whoami

    Output:

    Logged in as: [email protected]
    {
    "sub": "019c...",
    "email": "[email protected]",
    "given_name": "Test",
    "family_name": "User"
    }
  4. Log out

    Terminal window
    bun logout

    This clears the locally stored tokens.

The CLI implements Authorization Code + PKCE with a temporary local HTTP server:

// 1. Start a temporary HTTP server to receive the callback
const 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 challenge
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// 3. Open the browser
const authUrl = `${ISSUER}/oauth/authorize?...`;
open(authUrl);

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.

Tokens are stored locally in a file (managed by the token-store module), so subsequent commands can use them without re-authenticating.

  • Hono OIDC — Add login to a web app using OIDC middleware
  • Authentication guide — Learn about public vs. confidential clients and when to use each