Skip to content

Authentication

Daimyo supports OAuth2/OIDC authentication for two purposes:

  1. Client sessions — CLI users log in once; tokens are stored securely and renewed automatically, so subsequent commands work without re-prompting.
  2. Scope access control — scopes can declare required JWT roles; the REST server validates Bearer tokens and rejects unauthorised requests.

Prerequisites

Configure the OIDC issuer and client before using auth commands:

# .daimyo/config/settings.toml
oidc_issuer_url = "https://auth.example.com/realms/my-realm"
oidc_client_id  = "daimyo-cli"

Or via environment variables:

export DAIMYO_OIDC_ISSUER_URL="https://auth.example.com/realms/my-realm"
export DAIMYO_OIDC_CLIENT_ID="daimyo-cli"

Daimyo discovers all OIDC endpoints automatically from <oidc_issuer_url>/.well-known/openid-configuration. No endpoint URLs need to be hardcoded.

Login

Browser flow (PKCE)

daimyo login

Opens the system browser for the authorization code + PKCE flow. After the user authenticates, the provider redirects to http://localhost:<oidc_redirect_port> (default port 8555). Tokens are stored immediately on success.

Custom redirect URI

By default the redirect URI is constructed as http://localhost:{oidc_redirect_port}. If the provider requires a different URI — a specific path, 127.0.0.1 instead of localhost, or an alternate port — override it explicitly:

oidc_redirect_uri = "http://127.0.0.1:8555/auth/callback"

The local callback server still listens on localhost:oidc_redirect_port; the custom URI is only sent to the authorization server and included in the token exchange request. Ensure the URI you configure here matches exactly what is registered with the identity provider.

Device authorization flow (headless)

daimyo login --device

Uses RFC 8628. Daimyo prints a device code and a verification URL; the user opens the URL on any browser and enters the code. The CLI polls until the authorization completes or times out. Use this in CI environments, SSH sessions, or containers where a browser cannot be opened.

Logout

daimyo logout

Removes all stored tokens from the configured backend. If no tokens are found the command exits cleanly with an informational message.

Token Storage

Tokens are persisted across invocations so the user does not need to log in for every command.

Keyring (default)

token_storage = "keyring"                    # default
token_storage_keyring_service = "daimyo"     # service name in the keyring

Delegates to the platform secret store:

  • Linux: Freedesktop Secret Service (e.g., GNOME Keyring, KWallet)
  • macOS: macOS Keychain
  • Windows: Windows Credential Manager

File

token_storage      = "file"
token_storage_file = "~/.daimyo/tokens.json"   # default path

The file is created with mode 0600 (owner read/write only). Use this backend on systems where a keyring daemon is unavailable (e.g., minimal Docker images).

Warning

The file backend stores tokens in plain JSON. Ensure the file is on an encrypted volume or otherwise protected at the OS level.

Silent Token Renewal

When the stored access token has expired, daimyo automatically exchanges the refresh token for a new access token before proceeding. This happens transparently — no user interaction is required unless the refresh token itself has expired, in which case daimyo prompts the user to log in again.

OIDC Provider SSL

By default daimyo verifies SSL certificates when connecting to the OIDC provider's endpoints (discovery, token, and JWKS). Two settings control this behaviour:

Custom CA bundle

When the identity provider uses a certificate signed by a private or internal CA:

oidc_ssl_ca_bundle = "/etc/ssl/certs/company-ca.pem"

The file must be a PEM-encoded CA certificate bundle. When set, it takes precedence over the system CA store for all OIDC provider requests.

Disable verification

For development environments with self-signed certificates where a CA bundle is unavailable:

oidc_ssl_verify = false

Warning

Disabling SSL verification removes protection against man-in-the-middle attacks. Never use oidc_ssl_verify = false in production or against a provider that handles real credentials.

These settings apply to every outbound HTTP call daimyo makes to the OIDC provider: the discovery document, the token endpoint, and the JWKS endpoint used for server-side JWT validation.

Authenticated Federation

When a valid session exists, daimyo injects the access token as a Bearer header into every request sent to the master server:

GET /api/v1/scopes HTTP/1.1
Authorization: Bearer <access_token>

If the master server returns 401 (token expired), daimyo refreshes the token and retries once. If the server returns 401 or 403 after the retry, the error is logged as a warning and the remote scope is skipped — local scopes are never affected by remote authentication failures.

See Server Federation for master server configuration.

Protecting Scopes

Declare an auth block in a scope's metadata.yml to require a valid JWT with specific roles:

name: my-secure-scope
description: Restricted scope

auth:
  accepted_roles:
    - "admin"
    - "daimyo-.*"       # regex patterns are supported
  roles_claim: roles    # optional; defaults to global oidc_roles_claim setting

How it works:

  • accepted_roles is a list of regex patterns.
  • The server extracts the value of the JWT claim named by roles_claim (typically a list of strings).
  • A request is authorised if any role value in the token matches any pattern in accepted_roles.
  • Scopes without an auth block are publicly accessible — no token is required.

Role patterns follow Python re syntax. Examples:

Pattern Matches
admin Exact string admin
daimyo-.* Any role starting with daimyo-
(admin\|superuser) Either admin or superuser

Nested claim paths

roles_claim supports dot-notation to reach values nested inside the JWT payload. This is necessary for providers such as Keycloak, which places realm roles under realm_access.roles:

{
  "realm_access": {
    "roles": ["admin", "user"]
  }
}

Configure the path with dots:

auth:
  accepted_roles:
    - "admin"
  roles_claim: "realm_access.roles"

Each segment is resolved in sequence; if any intermediate key is absent or is not an object, the claim resolves to an empty list and authorization fails. A plain (non-dotted) name still works as before.

Server-side JWT Validation

For scopes with auth.accepted_roles, the REST API validates incoming Bearer tokens:

  1. The server fetches the OIDC JWKS document from <oidc_issuer_url>/.well-known/openid-configuration → jwks_uri.
  2. JWKS keys are cached in memory for jwks_cache_ttl seconds (default 300).
  3. The token signature, expiry, and audience (if oidc_audience is set) are verified.
  4. Role values from the configured claim are matched against accepted_roles.
  5. A missing or invalid token returns 401 Unauthorized; a valid token with no matching role returns 403 Forbidden.

Note

The MCP server does not perform token validation. Scope access control via auth only applies to the REST API.

Troubleshooting 401 Errors

If clients receive 401 Unauthorized even with recently issued, apparently valid tokens, work through the causes below in order.

Issuer URL mismatch

The most common cause. The oidc_issuer_url setting must match exactly the value the identity provider places in the iss claim of every token it issues.

In containerised or reverse-proxied deployments the URL daimyo uses to reach the IdP internally can differ from the public URL the IdP writes into tokens:

oidc_issuer_url (what daimyo uses) Token iss claim (what IdP writes) Result
http://keycloak:8080/realms/myrealm https://auth.example.com/realms/myrealm 401
https://auth.example.com/realms/myrealm https://auth.example.com/realms/myrealm OK

To diagnose the mismatch, start daimyo with debug logging and send a request with your token:

DAIMYO_LOG_LEVEL=DEBUG daimyo serve

When a mismatch occurs the log shows both values:

DEBUG  Issuer mismatch — configured: http://keycloak:8080/realms/myrealm, token iss: https://auth.example.com/realms/myrealm

Set oidc_issuer_url to the value shown as token iss. If the IdP is only reachable at an internal address, configure it there and ensure the IdP issues tokens with that same URL as iss (Keycloak: set the Frontend URL or use realm hostname overrides).

Audience mismatch

If oidc_audience is set, the token's aud claim must match. Decode your token (e.g., with jwt.io) and compare the aud value against your oidc_audience setting. Leave oidc_audience empty to skip this check when the server is an internal tool.

Roles claim not found

If the token is accepted (200 OK) when no accepted_roles are configured but fails with 403 Forbidden when roles are required, the roles_claim path is likely wrong.

Decode the token and locate where roles are stored. For Keycloak that is usually realm_access.roles; for other providers it may be groups, roles, or a custom claim. Set roles_claim in metadata.yml to the correct path, using dot-notation for nested values.

OIDC provider unreachable

If the OIDC provider's JWKS endpoint is temporarily unavailable, daimyo returns 401 (not 500). Check provider connectivity and retry; JWKS are cached for jwks_cache_ttl seconds so a brief outage only affects the first request after the cache expires.