Skip to content

Authentication

Set up authentication for your Deckyard installation.

Authentication is on by default. Deckyard refuses to start while authentication is enabled and AUTH_SECRET is missing, so an instance never runs wide open by accident. To run without authentication on purpose, set AUTH_ENABLED=false (see Disabling Authentication). Sandbox and demo mode are the only other cases where the server starts without a secret.

Authentication supports:

  • Database users with password login
  • Magic link (passwordless) login via email
  • Single sign-on (SSO) with one OpenID Connect (OIDC) provider
  • Development bypass mode for local testing

Account settings showing profile photo upload, display name field, and password change form

The secret key used for signing session tokens. This is required when authentication is enabled.

Terminal window
AUTH_SECRET=your-random-secret-string-at-least-32-characters

Generate a secure secret:

Terminal window
# Using OpenSSL
openssl rand -base64 48
# Using Node.js
node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"

Important:

  • The secret must be at least 32 characters; the server refuses to start with a shorter one. AUTH_ALLOW_WEAK_SECRET=true overrides this check, but that is not recommended: rotate to a strong secret instead.
  • Keep this secret secure and never commit it to version control
  • Changing this secret will invalidate all existing sessions

Users are stored in the database; apart from the first admin of a fresh install (see Your first admin), there is no way to define users in environment variables. Admins add, edit and remove users in the admin panel, and can send a new user an invitation email (this needs the email service). Every user has one of two roles: admin or user. See User Management.

Set the administrator email address. The user matching this email automatically receives admin privileges, regardless of their configured role.

Terminal window
AUTH_ADMIN_EMAIL=admin@yourdomain.com

If not set, no user has automatic admin privileges; roles must be assigned explicitly.

Every login path needs an existing user, and only an admin can add one. A fresh install therefore creates its first admin from AUTH_ADMIN_EMAIL: when the server starts with authentication on and an empty user table, it adds that address as an admin without a password. The server log names the address and says how to sign in:

  • Without SSO: open /login and request a magic link, or use “Forgot password” to set a password. Both send an email, so the email service must be configured (see Email Configuration). When SSO is off and BREVO_API_KEY is not set, the log warns that no mail can be sent; configure email or SSO and restart.
  • With SSO: sign in through the SSO button with that address (see Your first login).

This runs only once. As soon as any user exists, the server creates nothing, so later users are added in the admin panel. It also does nothing when authentication is off (AUTH_ENABLED=false) or the instance runs in sandbox or demo mode, and it skips an address that is not a valid email (the log says so). You do not need to start with AUTH_ENABLED=false to create the first user.

Passwordless login via email links. Users receive a secure link that logs them in automatically.

Requirements:

  • Email service configured (see Email Configuration)
  • The user must already have an account; no link is sent to unknown addresses
  • Not available when SSO_ENFORCE is on (see Single Sign-On)

How it works:

  1. User enters their email address
  2. System sends a secure, time-limited link
  3. Clicking the link authenticates the user and creates a session
  4. Each link works once and expires after 15 minutes

Rate limits:

  • 5 link requests per email per hour
  • 15 link requests per IP per hour

Users with a password can reset it by email.

Requirements:

  • Email service configured
  • Not available when SSO_ENFORCE is on

Flow:

  1. User requests a password reset
  2. System sends an email with a secure reset link, valid for 1 hour
  3. User clicks the link and enters a new password (at least 8 characters)
  4. All existing sessions are invalidated
  5. User signs in with the new password

Rate limits:

  • 3 reset requests per email per hour
  • 10 reset requests per IP per hour

Point the whole instance at one OpenID Connect provider, such as Google Workspace, Microsoft Entra ID, Okta, Auth0 or Keycloak. Users click “Sign in with SSO”, authenticate at the provider and land in Deckyard with the same account as a password or magic-link user with that email address.

Terminal window
SSO_ENABLED=true
SSO_PROVIDER=oidc
OIDC_ISSUER_URL=https://login.example.com
OIDC_CLIENT_ID=...
OIDC_CLIENT_SECRET=...
OIDC_REDIRECT_URI=https://deck.example.com/api/auth/oidc/callback

OIDC_REDIRECT_URI is <APP_URL>/api/auth/oidc/callback, and must be registered at the provider exactly so. The server refuses to start when SSO_ENABLED=true but one of these settings is missing or a URL is malformed. It warns at startup when OIDC_REDIRECT_URI has another path or another origin than APP_URL, and the warning names the URI to set and register. That is a warning rather than a refusal because a reverse proxy may rewrite the callback; if yours does not, the warning is the bug. oidc is the only supported provider.

Optional settings:

VariableDefaultMeaning
OIDC_ALLOWED_DOMAINS(none)Comma-separated email domains allowed to sign in
OIDC_AUTO_PROVISIONtrueCreate unknown users on their first login; false means users must be invited first
OIDC_DEFAULT_ROLEuserRole for newly created users: user or admin
OIDC_ADMIN_GROUPS(none)Comma-separated provider group or role values that grant admin; matching ignores case
OIDC_GROUPS_CLAIMgroups,rolesComma-separated claims the group or role values are read from; each a claim name or a dot path such as realm_access.roles
OIDC_ORG_CLAIM(none)With several organizations: the claim whose value must match an existing organization’s external ID. A missing claim or an unknown value refuses the login
OIDC_EXTRA_SCOPES(none)Space-separated scopes requested on top of openid email profile, for claims the provider only sends behind a scope of their own. An invalid scope refuses startup
SSO_ENFORCEfalsetrue makes SSO the only way in: it hides the password and magic-link forms and refuses password login, magic links and password resets (see Enforcing SSO)
SSO_BUTTON_LABELthe translated “Sign in with SSO”The words on the SSO button, e.g. Sign in with Acme ID; the invite report that tells an inviter how a new member gets in uses the same words

With SSO_ENFORCE=true, the server refuses every local login and recovery request with 403 and { "ok": false, "error": "sso_required" }: password login, magic-link requests, “Forgot password”, and also redeeming a magic link or reset link. The refusal comes before any credential check, token or mail, so a link issued before you switched enforcement on can no longer open a session or reset a password.

Sessions that are already open stay valid; enforcement does not sign anyone out. Enforcement only takes effect while SSO is enabled and its configuration is valid, so a broken SSO setup cannot lock everyone out (the server refuses to start with an enabled but invalid configuration anyway). The development bypass keeps its own development-only rule.

An SSO login can grant admin (through OIDC_ADMIN_GROUPS or AUTH_ADMIN_EMAIL) but never removes it; demoting an admin is done in the admin panel.

Some providers send a claim only when its scope is requested. ZITADEL, for example, puts the user’s organization in urn:zitadel:iam:user:resourceowner:id only with the scope urn:zitadel:iam:user:resourceowner, so set the claim and the scope side by side:

Terminal window
OIDC_GROUPS_CLAIM=urn:zitadel:iam:org:project:roles
OIDC_ORG_CLAIM=urn:zitadel:iam:user:resourceowner:id
OIDC_EXTRA_SCOPES=urn:zitadel:iam:user:resourceowner

On an instance that signs in through SSO only, the first administrator comes from the provider too:

  1. Set AUTH_ADMIN_EMAIL to the address your provider account carries, and leave OIDC_AUTO_PROVISION at true.
  2. Register <APP_URL>/api/auth/oidc/callback at the provider and set the same value in OIDC_REDIRECT_URI. Start the server, or run npm run doctor: no warning about the redirect URI means the two agree.
  3. With OIDC_ORG_CLAIM set, create the organization before anyone signs in, carrying the provider’s organization ID: npm run org:create -- --slug acme --name "Acme" --external-id <provider-org-id> (see Multiple Organizations). Without an organization claim, skip this step.
  4. Open /login and choose the SSO button. On a fresh install the server already created AUTH_ADMIN_EMAIL as an admin at startup (see Your first admin), and the login signs you in as that user; otherwise your user is created on the spot. Either way you get the admin role, because the address matches AUTH_ADMIN_EMAIL.
  5. From here, grant admin through the provider (OIDC_ADMIN_GROUPS) or the admin panel, and set OIDC_AUTO_PROVISION=false if people should only get in by invitation.

A refused login returns to /login?error=sso_<reason>, and the reason names the gate: email_unverified, domain_not_allowed, org_claim_missing, org_not_found, not_provisioned, no_membership, state (the ten-minute login cookie expired or was blocked) or token_exchange_failed (client secret, redirect URI or clock; the server log has the provider’s answer).

Which claim carries the groups, and whether the organization claim arrived at all, differs per provider. Instance admins find the claims of the last ten SSO logins of the past 24 hours under Settings → Admin → SSO: claims of recent logins, with time, email and outcome. A refused login is listed too, as long as the token exchange succeeded. Sign in through SSO in a second browser, reload the settings page, and read off the names for OIDC_GROUPS_CLAIM, OIDC_ADMIN_GROUPS and OIDC_ORG_CLAIM.

The list holds claims, never tokens, and lives in memory only: nothing is written to the database, disk or log, and a restart clears it. Organization admins without the instance admin role do not see it.

The sign-in card can carry your own logo too: set APP_LOGO_URL (see Environment).

For local development, you can bypass authentication entirely.

Terminal window
NODE_ENV=development
AUTH_DEV_BYPASS=true

When enabled:

  • All requests are treated as coming from an admin user (dev@local.test)
  • No login required
  • Full admin privileges granted

The bypass only takes effect when NODE_ENV=development; in any other environment it is ignored.

⚠️ Security Warning: Never enable this in production. With NODE_ENV=production the server refuses to start while AUTH_DEV_BYPASS is on.

Set a cookie domain to share sessions across subdomains.

Terminal window
# Share sessions across all subdomains
COOKIE_DOMAIN=.example.com
# Single domain only
COOKIE_DOMAIN=app.example.com

Force secure cookies (HTTPS only). Secure cookies are used automatically when the request arrives over HTTPS, including behind a proxy that sets X-Forwarded-Proto: https.

Terminal window
SECURE_COOKIES=true
  • Sessions are valid for 14 days; this is fixed, not configurable
  • Sessions are stored as signed cookies (HttpOnly, SameSite=Lax)
  • Password changes invalidate all existing sessions for that user
  • Sessions are verified against the database on each request

To run Deckyard without authentication (not recommended for production):

Terminal window
AUTH_ENABLED=false

When disabled, all users have anonymous admin access. Only an explicit false, 0, no or off disables authentication; an empty or misspelled value leaves it on.

Switching authentication off is not a way to create the first admin: with auth off no user is created. Set AUTH_ADMIN_EMAIL instead (see Your first admin).