Add Authentication
Add credential resolution, interactive login, and authenticated HTTP requests to a maltty CLI.
Prerequisites
- An existing maltty CLI project
malttyinstalled (pnpm add maltty)
Steps
1. Register the auth middleware
Import auth from maltty/auth and add it to the middleware array in cli().
The strategies array defines which credential sources to try. Order matters -- strategies run in sequence and short-circuit on the first success.
2. Add a login command
Create a command that calls ctx.auth.login() to run the interactive strategies and persist the credential.
3. Add a logout command
Create a command that calls ctx.auth.logout() to remove the stored credential from disk.
4. Guard commands that require auth
Commands that need a credential should reject unauthenticated requests early. There are two approaches: an inline check for one-off guards, and a reusable middleware for consistent enforcement across commands.
Inline check
Check ctx.auth.authenticated() at the top of the handler.
Reusable middleware
Write a middleware that checks for a credential and short-circuits before the handler runs. This keeps handlers focused on business logic.
Apply it per-command via the middleware array:
Apply it globally by adding it to the root middleware array after auth():
When applied globally, every command (including login) must pass the check. Exclude the login command by applying requireAuth per-command instead of globally.
Ordering:
requireAuthmust come afterauth()in the middleware array because it depends onctx.authbeing decorated.
5. Add the HTTP middleware
For authenticated API requests, register the http() middleware after auth(). It reads ctx.auth.credential() automatically and injects the correct HTTP headers.
The namespace option determines the context property name. With namespace: 'api', the client is available as ctx.api.
6. Make authenticated requests
Use the typed HTTP client to make requests. Auth headers are injected automatically.
7. Support environment variables
Add env or dotenv strategies for non-interactive environments (CI, scripts).
Passive strategies (env, dotenv, file) run automatically on middleware init. Interactive strategies (oauth, device-code, token, custom) only run when ctx.auth.login() is called.
8. Use PKCE with Clerk as the Identity Provider
Configure the OAuth strategy to use Clerk as a public OAuth application with PKCE:
9. Use the device code flow for headless environments
For environments without a browser (SSH sessions, remote servers), use the device code flow:
The CLI displays a URL and a user code. The user opens the URL in any browser (including on a different device), enters the code, and completes authorization.
10. Combine multiple strategies
Chain strategies to support multiple authentication methods:
Verification
Troubleshooting
OAuth redirect not received
Issue: The browser opens but the CLI hangs waiting for the redirect.
Fix: Ensure the OAuth provider is configured to redirect to http://127.0.0.1:<port>/callback with code and state query parameters. Verify the clientId is correct and the application is configured as a public client with PKCE support. Check that no firewall is blocking the local port.
Token exchange fails
Issue: The redirect is received but no credential is returned.
Fix: Verify the tokenUrl is correct and accepts application/x-www-form-urlencoded POST requests. Ensure the OAuth provider accepts the code_verifier parameter for PKCE validation.
Device code flow times out
Issue: The CLI polls but never receives a token.
Fix: Verify the deviceAuthUrl and tokenUrl are correct. Ensure the OAuth provider supports the Device Authorization Grant (RFC 8628). Clerk does not support this flow -- use the oauth strategy instead.
Token not persisted after login
Issue: ctx.auth.credential() returns null on subsequent runs.
Fix: Check that the global store directory (~/.my-app/) is writable. Inspect ~/.my-app/auth.json for valid JSON.
Wrong environment variable name
Issue: The env strategy doesn't pick up the token.
Fix: The default variable name is derived from the CLI name: my-app becomes MY_APP_TOKEN. Use tokenVar to override if your variable has a different name.
Resources
- @clack/prompts
- RFC 7636 -- PKCE
- RFC 8252 -- OAuth for Native Apps
- RFC 8628 -- Device Authorization Grant