Skip to content

Authentication

dbward supports two authentication methods: API tokens (simple, self-hosted) and OIDC (SSO with your identity provider). You can use either or both.

dbward supports two authentication methods that work simultaneously:

  • API Tokens (dbw_...): Always accepted. Used by CLI, agents, and CI/CD.
  • OIDC JWTs (eyJ...): Accepted when [auth.oidc] is configured and a Team license is active.

When [auth.oidc] is present, both methods are accepted. When absent, only API tokens work.


Terminal window
# Via CLI
dbward token create --subject alice --scope-roles requester
dbward token create --subject bob --scope-roles requester,dba --expires 90d
dbward token create --subject prod-agent --subject-type agent --no-scope-ceiling
Terminal window
# Via REST API (requires token.create or token.create_agent permission)
curl -X POST http://localhost:3000/api/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject_id": "bob",
"subject_type": "user",
"scope_ceiling": {"roles": ["requester", "dba"]},
"name": "Bob CI token",
"expires_at": "2026-09-01T00:00:00Z"
}'
FieldTypeDescription
subject_idstringRequired. User or service identifier.
subject_typestringRequired. user or agent.
scope_ceilingobjectMax roles this token can activate. Format: {"roles": ["role1", "role2"]}. Effective roles = intersection of scope_ceiling.roles and the user’s assigned roles (direct + group-derived). Set null for agent tokens (--no-scope-ceiling). Optional (auto-derived from resolved roles when omitted) for user tokens.
namestringHuman-readable label.
expires_atdatetimeAbsolute expiry (RFC 3339). Unset = no expiration.
Create → Active → [Expired | Revoked]
  • Expiration: Tokens with expires_at are rejected after the deadline.
  • Revocation: Immediate via DELETE /api/tokens/{id}.
  • Self-revoke: Users with token.revoke permission can revoke their own tokens.
  • No rotate API: Revoke the old token and create a new one.
Terminal window
# Admin can revoke any token
curl -X DELETE http://localhost:3000/api/tokens/$TOKEN_ID \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Users can revoke their own tokens
curl -X DELETE http://localhost:3000/api/tokens/$TOKEN_ID \
-H "Authorization: Bearer $MY_TOKEN"
# In dbward.toml (client config)
[server]
url = "https://dbward.internal:3000"
token = "dbw_a1b2c3..."

Or via environment variable:

[server]
url = "https://dbward.internal:3000"
token = "${DBWARD_TOKEN}"

Tokens do not embed roles directly. The server resolves roles dynamically at request time:

  1. Direct roles: Roles assigned to the user via dbward user add --role or dbward user update --role
  2. Group-derived roles: Roles inherited from group membership ([[auth.groups]] with roles field in config)
  3. Default role: Falls back to [auth].default_role if no roles found

The token’s scope_ceiling intersects with the resolved roles to produce effective permissions.

For details on roles and permissions, see Authorization Reference.

Check a token’s current effective permissions:

Terminal window
dbward token inspect <ID>
Terminal window
# Via REST API (owner or token.list permission)
curl http://localhost:3000/api/tokens/$TOKEN_ID/inspect \
-H "Authorization: Bearer $MY_TOKEN"

[auth.oidc]
issuer = "https://accounts.google.com"
client_id = "123456789.apps.googleusercontent.com"
# # client_secret_env is not supported # Optional: env var name
# jwks_uri = "http://keycloak:8080/realms/dbward/protocol/openid-connect/certs" # Override for Docker
default_role = "approver" # Role when no mapping matches (default: approver)
# In dbward.toml (client config)
[server]
url = "https://dbward.internal:3000"
[server.oidc]
issuer = "https://accounts.google.com"
client_id = "123456789.apps.googleusercontent.com"
# discovery_url = "..." # Override discovery endpoint
# browser_url = "..." # Override authorize URL (for Docker)
# backchannel_url = "..." # Override token endpoint (for Docker)
Terminal window
# Browser-based login (opens browser for OAuth flow)
dbward login
# Device flow (for headless environments / SSH)
dbward login --device
# Check current identity
dbward whoami
# Logout (revokes token locally)
dbward logout

Map IdP claims to dbward roles:

# Map by group membership
[[auth.oidc.role_mappings]]
claim = "groups"
value = "db-admins"
role = "admin"
[[auth.oidc.role_mappings]]
claim = "groups"
value = "backend-team"
role = "requester"

How it works:

  • All matching mappings are collected (a user can have multiple roles)
  • If no mapping matches, [auth.oidc].default_role is used, then [auth].default_role
  • Roles grant specific permissions (see Authorization Reference)
IdPNotes
Google WorkspaceSet up OAuth consent screen + credentials
OktaCreate OIDC app, add groups claim to ID token
KeycloakCreate client, enable groups mapper
Azure AD (Entra)Register app, configure groups claim
Auth0Add groups via Rules/Actions (custom claim namespace)

Users are managed via the dbward user CLI commands:

Terminal window
# Add a user with a role
dbward user add alice --role requester
# Add a user to a group (inherits group roles)
dbward user add bob --role dba --group backend-team
# Suspend a user (revokes tokens, cancels pending requests)
dbward user suspend alice
# Reactivate
dbward user activate alice
# Remove entirely
dbward user rm alice

Roles are assigned directly to users via dbward user add --role or inherited from group membership (groups.roles in config).

OIDC users are created automatically on first login with source = "oidc". To disable an OIDC user:

  1. CLI suspend: dbward user suspend <id> — immediately blocks all requests (the server checks is_suspended on every request, even with a valid JWT).
  2. IdP-side disable: Prevents new JWT issuance. Existing JWTs are still checked against is_suspended per-request, so suspend is effective regardless of JWT lifetime.
MethodEffectPersistence
POST /api/users/{id}/suspendImmediate suspend + revokePersistent in DB
dbward user suspend <id>Immediate suspend + revokePersistent in DB

Groups enable team-based approval workflows. They come from the groups claim in the OIDC JWT.

IdP (groups: ["dba-team", "backend"])
│ JWT with groups claim
dbward server
│ Reads groups directly from JWT (no sync, no storage)
Workflow evaluation
│ "Does this user belong to group:dba-team?"
Approval decision
[[workflows]]
database = "primary"
environment = "production"
[[workflows.steps]]
type = "approval"
[[workflows.steps.approvers]]
group = "dba-team" # Anyone in the IdP "dba-team" group can approve
min = 1

There is no sync. dbward reads groups from the JWT on every request.

QuestionAnswer
When do group changes take effect?When the user gets a new JWT (re-login)
Can the IdP push changes?No. JWT-based, stateless.
How fast is the update?Depends on JWT lifetime (IdP setting, typically 5min–1hr)
Is there a manual refresh?dbward logout && dbward login

Recommendation: Set your IdP’s token lifetime to 5–15 minutes for near-real-time group updates.

GroupsRoles
SourceIdP groups claimrole_mappings conversion
PurposeWorkflow approver matchingAPI access control
Examplegroup:dba-teamadmin, requester
Stored in dbward?No (JWT only)No (JWT only)

Both are extracted from the JWT on every request. Neither is stored in dbward’s database.


Agents use API tokens with subject_type = "agent" and --no-scope-ceiling. Agents always resolve to the agent-default role regardless of ceiling:

Terminal window
dbward token create --subject prod-agent --subject-type agent --no-scope-ceiling

The --no-scope-ceiling flag removes the scope ceiling restriction, allowing the agent token to activate all roles assigned to the agent user. This is the recommended setup for agents since their permissions are fully controlled via user roles.

Note: --no-scope-ceiling conflicts with --scope-roles — they cannot be used together.

When [auth.oidc] is configured, both API tokens and OIDC JWTs are accepted. Agents always use API tokens.


  1. Use TTL on all tokens — Set expires_at to 90 days max. Rotate before expiry.
  2. Use OIDC for humans — Avoid sharing long-lived tokens between team members.
  3. Separate agent tokens — One token per agent. Revoke individually if compromised.
  4. Short JWT lifetime — Configure your IdP to issue tokens with 5–15 minute expiry.
  5. Configure [auth.oidc] — OIDC for humans, API tokens for agents. Best of both worlds.

SymptomCauseFix
401 invalid tokenToken revoked or wrongCheck dbward token list
401 token expiredTTL exceededCreate a new token
401 OIDC not configuredJWT sent but [auth.oidc] not configuredAdd [auth.oidc] section to server.toml
JWT verification failedWrong issuer/audience/expiredCheck issuer and client_id match IdP
JWKS fetch timeoutServer can’t reach IdPCheck network, or set jwks_uri override