Authentication
The auth system for maltty CLIs. Provides credential resolution from multiple sources, an interactive login flow, persistent token storage, and automatic HTTP header injection.
Auth is a sub-export of the maltty package (maltty/auth), not a separate package. It ships as middleware that decorates ctx.auth with a credential() reader, a login() method, and a logout() method.
Key Concepts
Passive vs Interactive Resolution
Auth strategies are split into two categories:
- Passive strategies run automatically when the middleware initializes. They check non-interactive sources (file store, environment variables) without prompting the user. The first match wins.
- Interactive strategies run only when the handler explicitly calls
ctx.auth.login(). They prompt the user (OAuth browser flow, password input) or call a custom function.
This split allows commands to work without auth when no credential is found, and only prompt when a command actually needs authentication.
Credential Types
All credentials use a discriminated union on the type field:
Token Storage
Credentials are persisted as JSON files using maltty's file store system.
The file contains the raw credential object:
Credentials loaded from disk are validated against a Zod schema. Invalid data is silently ignored (returns null).
Resolver Builders
The auth function doubles as a namespace with builder methods for constructing strategy configs. Each builder returns the same StrategyConfig type with the source discriminator pre-filled. Raw config objects ({ source: 'env' }) still work.
Resolvers
env -- Environment Variable
Reads a bearer token from process.env.
The default variable name is derived from the CLI name: my-app becomes MY_APP_TOKEN.
dotenv -- Dotenv File
Reads a bearer token from a .env file without mutating process.env.
file -- JSON File
Reads any credential type from a JSON file on disk via maltty's store system.
oauth -- OAuth Authorization Code + PKCE (RFC 7636)
Implements the standard OAuth 2.0 Authorization Code flow with Proof Key for Code Exchange (PKCE) per RFC 7636 and RFC 8252 for native apps.
The flow:
- CLI generates a
code_verifierand derives thecode_challenge(S256) - CLI starts a local HTTP server on
127.0.0.1and opens the browser to the authorization URL - User authenticates in the browser; the authorization server redirects back to the local server with an authorization code via GET
- CLI exchanges the code at the token endpoint with the
code_verifier - Token endpoint validates the verifier and returns an access token
Compatible with any OAuth 2.0 provider that supports PKCE with public clients, including Clerk (configured as a public OAuth application).
device-code -- Device Authorization Grant (RFC 8628)
Implements the OAuth 2.0 Device Authorization Grant for headless or browserless environments.
The flow:
- CLI requests a device code from the authorization server
- CLI displays a verification URL and user code for the user to enter in a browser
- CLI polls the token endpoint until the user completes authorization
- Token endpoint returns an access token on success
The device code flow handles RFC 8628 error codes: authorization_pending (continue polling), slow_down (increase interval), expired_token (return null), and access_denied (return null).
Supported by GitHub, Azure AD, and Google. Not supported by Clerk.
token -- Interactive Token Input
Prompts the user for a token via a masked password input. Aliased as auth.apiKey().
custom -- User-Provided Function
Calls a user-supplied function that returns a credential or null. The function is passed directly as the argument (not wrapped in an options object).
AuthContext
The auth middleware decorates ctx.auth with an AuthContext:
login() accepts an optional LoginOptions object to override strategies or add a validate callback for a single login attempt:
ctx.auth.login()
Walks the configured strategies in order, runs each interactive strategy, and persists the first successful credential to the global file store.
ctx.auth.logout()
Removes the stored credential file from the global file store. Returns ok(filePath) on success, including when the file did not exist (idempotent).
AuthError
Requiring Authentication
Auth is opt-in by default. The auth() middleware decorates ctx.auth with credential readers but never blocks command execution. Commands that run without a credential (public commands, the login command itself) work without any extra configuration.
Use the built-in auth.require() helper to create a middleware that checks ctx.auth.authenticated() and calls ctx.fail() to short-circuit before the handler runs:
You can customize the error message:
Alternatively, write a custom middleware for full control:
Command-level enforcement
Apply the middleware to individual commands via the middleware array. This is the recommended approach when only some commands require authentication (login, help, and version remain open).
Global enforcement
Apply the middleware to the root middleware array to enforce authentication on every command. Place it after auth() since it depends on ctx.auth.
When applied globally, all commands -- including login -- must pass the check. Use command-level enforcement when some commands need to run unauthenticated.
See the Add Authentication guide for step-by-step instructions.
HTTP Integration
Auth and HTTP are separate middleware. The http() middleware (from maltty/http) creates a typed HTTP client on ctx[namespace]. To inject auth credentials into HTTP requests automatically, use auth.headers() as the headers option on http().
auth.headers() returns a function (ctx) => headers that reads ctx.auth.credential() and converts it into the appropriate HTTP header format. It returns an empty record when no auth middleware is present or no credential exists.
Place auth() before http() in the middleware array so that ctx.auth is available when HTTP requests resolve headers.
Multiple HTTP clients work the same way:
Both ctx.api and ctx.admin get auth credential headers injected automatically.
http() without auth
The http() middleware does not require auth(). Use it standalone for public APIs or when providing headers explicitly.
Resources
- RFC 7636 -- Proof Key for Code Exchange
- RFC 8252 -- OAuth 2.0 for Native Apps
- RFC 8628 -- Device Authorization Grant