Auth
OAuth2 authentication profiles and token management
Earl supports OAuth2 authentication through named profiles defined in config.toml. Profiles handle login, token storage, automatic refresh, and injection into API requests.
Supported Flows
The most common flow for user-facing applications. Earl opens a browser, the user authorizes, and a local callback server receives the authorization code.
Required fields: client_id, token_url, authorization_url
[auth.profiles.github]
flow = "auth_code_pkce"
client_id = "your-github-client-id"
token_url = "https://github.com/login/oauth/access_token"
authorization_url = "https://github.com/login/oauth/authorize"
scopes = ["repo", "read:org"]The default callback URL is http://127.0.0.1:8976/callback. Override it with redirect_url if your OAuth app is registered with a different callback address.
If device_authorization_url is also configured and the browser-based flow fails (e.g., headless environment), earl automatically falls back to the device code flow.
Best for CLI tools and headless environments. Earl displays a URL and a user code. The user visits the URL on any device to authorize.
Required fields: client_id, token_url, device_authorization_url
[auth.profiles.azure]
flow = "device_code"
client_id = "your-azure-client-id"
token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
device_authorization_url = "https://login.microsoftonline.com/common/oauth2/v2.0/devicecode"
scopes = ["User.Read"]Machine-to-machine authentication without user interaction. Requires a client secret.
Required fields: client_id, token_url, client_secret_key
[auth.profiles.internal_api]
flow = "client_credentials"
client_id = "service-account-id"
client_secret_key = "internal_api.client_secret"
token_url = "https://auth.internal.example.com/oauth/token"
scopes = ["api:read", "api:write"]The client_secret_key references a secret stored in the OS keychain. Store it with:
earl secrets set internal_api.client_secretClient credentials tokens are automatically obtained on first use -- you do not need to run earl auth login for this flow. When a token expires and no refresh token is available, earl automatically re-authenticates.
Profile Configuration Reference
Every profile requires flow, client_id, and token_url (or an issuer that provides it via OIDC discovery). Additional fields depend on the flow:
| Field | Type | Description |
|---|---|---|
flow | string | Required. One of auth_code_pkce, device_code, or client_credentials. |
client_id | string | Required. OAuth2 client identifier. |
token_url | string | Token endpoint URL. Required unless issuer provides it via OIDC discovery. |
authorization_url | string | Authorization endpoint. Required for auth_code_pkce. Can be discovered via issuer. |
device_authorization_url | string | Device authorization endpoint. Required for device_code. Can be discovered via issuer. |
client_secret_key | string | Secret key in earl's keychain for the client secret value. |
issuer | string | OIDC issuer URL. Earl fetches /.well-known/openid-configuration to discover token_url, authorization_url, and device_authorization_url. Explicitly set URLs take precedence over discovered ones. |
redirect_url | string | OAuth callback URL for auth_code_pkce. Defaults to http://127.0.0.1:8976/callback. |
scopes | list | OAuth scopes to request. Defaults to []. |
use_auth_request_body | bool | Send client credentials in the request body instead of the Authorization header. Defaults to false. |
OIDC Discovery
If your identity provider supports OpenID Connect, you can set issuer instead of manually specifying endpoint URLs:
[auth.profiles.okta]
flow = "auth_code_pkce"
client_id = "your-okta-client-id"
issuer = "https://dev-123456.okta.com"
scopes = ["openid", "profile"]Earl fetches https://dev-123456.okta.com/.well-known/openid-configuration and extracts authorization_endpoint, token_endpoint, and device_authorization_endpoint. Any explicitly set URL in the profile overrides the discovered value.
The issuer URL is validated with the same SSRF checks as other OAuth endpoints -- it must use HTTPS (or HTTP to a loopback address).
Commands
Login
earl auth login githubStarts the OAuth flow defined in the profile. For auth_code_pkce, this opens a browser and starts a local callback server. For device_code, this displays the verification URL and user code. For client_credentials, this exchanges credentials for a token directly.
Check status
earl auth status githubShows whether a token is stored, when it expires, and which scopes were granted.
Refresh token
earl auth refresh githubForces a token refresh using the stored refresh token. If no refresh token is available and the flow is client_credentials, earl re-authenticates automatically. For other flows without a refresh token, earl prints an error directing you to run earl auth login.
Logout
earl auth logout githubDeletes the stored token for the profile from the OS keychain.
Token Storage
OAuth tokens are stored in the OS keychain under the key oauth2.<profile>.token. The stored payload is a JSON object containing:
access_token-- the bearer tokenrefresh_token-- if provided by the identity providertoken_type-- typicallyBearerexpires_at-- expiration timestamp (if the provider returnsexpires_in)scopes-- granted scopes
Tokens are considered expired 30 seconds before their actual expiration time to account for clock skew and network latency.
Token debug output (via RUST_LOG) redacts access_token and refresh_token fields to prevent accidental logging of credentials.
Using Auth in Templates
Reference an OAuth2 profile in a template's auth block:
auth {
kind = "oauth2_profile"
profile = "github"
}When the template is executed, earl automatically:
- Loads the stored token for the profile
- Checks if the token is expired
- Refreshes the token if needed (using the refresh token or re-authenticating for client credentials)
- Injects the access token as a
Bearerauthorization header
If the profile has no stored token and the flow is not client_credentials, earl prints an error directing the user to run earl auth login <profile>.
Endpoint Security
All OAuth endpoint URLs are validated before use:
- Must use
httpsscheme (orhttpto loopback addresses127.0.0.1,::1,localhostfor local development) - Must resolve to public IP addresses (SSRF protection blocks private, reserved, and metadata IPs)
- The same validation applies to OIDC discovery URLs
See Security Model for the full list of blocked IP ranges.