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.

[auth]
mode = "token" # "token" | "oidc" | "both"
ModeUse case
tokenSmall teams, CI/CD, agents. No IdP needed.
oidcTeams with Google/Okta/Keycloak SSO.
bothOIDC for humans, API tokens for agents and CI.

Terminal window
# Via CLI
dbward token create --subject alice --role admin
dbward token create --subject bob --role developer --groups "backend-team" --expires 90d
dbward token create --subject prod-agent --role agent-default --subject-type agent
Terminal window
# Via REST API (requires token.manage permission)
curl -X POST http://localhost:3000/api/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject_id": "bob",
"roles": ["developer"],
"groups": ["backend-team"],
"name": "Bob CI token",
"expires_at": "2026-09-01T00:00:00Z"
}'
FieldTypeDescription
subject_idstringRequired. User or service identifier.
rolesstring[]Roles to assign. Default: [] (uses default_role).
subject_typestringuser or agent. Default: user.
namestringHuman-readable label.
groupsstring[]Group memberships (for workflow approver matching).
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_own 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}"

[auth]
mode = "oidc" # or "both" to also allow API tokens
[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 = "readonly" # Role when no mapping matches (default: readonly)
# 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 = "developer"

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)

Pre-provision users and control their status in server.toml:

[[users]]
id = "alice"
status = "active"

On server start or config reload, the server reconciles user status:

  • Suspend: Revokes all tokens + cancels all pending requests (same effect as the API suspend endpoint).
  • Reactivate: Sets status to active. Revoked tokens remain revoked — issue new ones.
  • Remove from config: Revokes tokens, cancels requests, and deletes the user record.

This is the recommended way to offboard users in token-only deployments.

OIDC users are created with source = "token" and cannot be managed via [[users]] config (the source guard skips them). To disable an OIDC user:

  1. API suspend: POST /api/users/{id}/suspend — 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 API suspend is effective regardless of JWT lifetime.
MethodEffectPersistence
POST /api/users/{id}/suspendImmediate suspend + revokeReverts to config value on restart
status = "suspended" in configSuspend + revoke on restart/reloadPermanent until config changes

For config-managed users, prefer config changes for permanent status changes.


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, developer
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.

Admin can assign groups to API tokens (for CI/CD or service accounts that need to act as approvers):

Terminal window
curl -X POST http://localhost:3000/api/tokens \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subject_id": "ci-bot",
"roles": ["developer"],
"groups": ["backend-team"]
}'

Agents use API tokens with subject_type = "agent":

Terminal window
dbward token create --subject prod-agent --role agent-default --subject-type agent

In OIDC mode (mode = "oidc"), agents are the only entities allowed to use API tokens. Human users must authenticate via OIDC.

In mode = "both", both API tokens and OIDC JWTs are accepted for all users.


  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. Use mode = "both" — 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 mode = "token"Change to mode = "oidc" or "both"
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