Skip to main content

Authentication

The framework supports two ways for a user to sign in: a local username + password (with the user store on disk or in the database) and OpenID Connect (any standards-compliant IdP). Both produce the same artefact — a short-lived JWT access token + a longer-lived refresh token — which the frontend stores in memory and the backend verifies on every API call.

The decision between local and OIDC, plus the JWT lifetimes and the OIDC claim mapping, are set under Settings → Framework → Authentication. The two can coexist: OIDC enabled for SSO users, the local backend kept for break-glass administration.


At a glance

LOCAL USERS
Settings → Users
Two storage variants:
• TOML file on the host
• Database tables (ly2_users)
Argon2id password hashes.
OIDC SSO
Authentik · Keycloak · Azure AD · Okta · Google.
Configured under Authentication → OIDC.
Maps the IdP's groups claim to Liberty roles 1:1.
SESSIONS
HS256 JWT access token (default 15 min).
Refresh token rotated on use (default 7 days).
Lifetimes editable per install.

Picking a local backend

In Settings → Framework → Authentication, the Backend dropdown offers two values:

BackendStorageProCon
Local — TOML (default)config/auth.toml on the host.Zero infrastructure — works without an external DB.Doesn't survive container rebuilds when the file isn't mounted; doesn't share state across replicas. Not appropriate past ~100 users.
Local — Databasely2_users / ly2_roles / ly2_permissions tables on the default pool.Survives container rebuilds when the DB is external. Shared across replicas — sign-in on one node works on every node. Scales to thousands of users.Requires the default pool to be reachable at startup. One more thing to back up.

The first time the framework is run on a fresh install, ./start.sh init-db bootstraps whichever backend is configured and seeds an admin user (the password is printed once on stdout).

Switching the backend is a restart-required change — the form highlights the field with a Restart required badge.


Managing local users — Settings → Users

The Users tab is the canonical editor for the local backend (both variants). The page lists every user with their roles, active flag and last sign-in:

Settings → Users
↻ Refresh+ New user
Username
Display name
Roles
Active
Last sign-in
admin
Administrator
admin
09:42
alice
Alice Dupont
editor viewer
2 days
bob
Bob Martin
viewer
30 days
ActionHow
Create a user+ New user — username, display name, roles (multi-select from the configured roles), password.
Reset passwordClick a user, Reset password — prompts for the new password (twice).
Add / remove rolesClick a user, edit the Roles multi-select.
DeactivateClick a user, toggle Active off. Soft-deactivates the user (preserves history).
Revoke sessionsClick a user, Revoke all sessions — invalidates every active access + refresh token.

The same operations are available via the CLI for shell scripting.

Password policy

Passwords are hashed with argon2id. The Argon2 tuning is editable under Settings → Framework → Authentication — defaults are Time cost = 2, Memory cost = 64 MiB, Parallelism = 1, tuned for a ~50 ms server cost on a modern CPU. Raise Memory cost on installs with stricter threat models.

A pluggable validator rejects weak passwords. The bundled validator enforces:

  • 10+ characters.
  • At least one digit, one upper-case, one lower-case.
  • Not equal to the username.

To override, set Password validator under Settings → Framework → Authentication to a callable from your apps repo (e.g. myapp.security:validate). See Apps & Plugins → Plugins for the function signature.


OIDC SSO

In Settings → Framework → Authentication, flip the OIDC enabled toggle on. The OIDC sub-form appears:

OIDC
Enabled
● On
Issuer URL
Client ID
liberty
Client secret
🔒 ENC:… Reveal
Redirect URI
Email claim
email
Groups claim
groups
Auto-provision
● On
FieldDescription
Issuer URLURL of the OIDC provider. The framework fetches ${issuer}/.well-known/openid-configuration at startup to discover the endpoints.
Client ID / Client secretOAuth2 client credentials registered with the IdP. The secret is always encrypted in storage — the 🔒 icon is locked on.
Redirect URIThe framework's callback URL. Must be registered on the IdP side.
Email claimJWT claim used as the local username. Default email.
Groups claimJWT claim mapped to Liberty roles 1:1. Default groups. A group named editor on the IdP grants the Liberty role editor.
Auto-provisionWhen on, a Liberty user is created on first sign-in. When off, the user has to exist already — convenient when group membership is broader than the Liberty allow-list.

A Test sign-in button at the bottom of the OIDC sub-form opens the IdP redirect in a new tab and reports the result. Use it before saving — a wrong issuer URL is much easier to spot here than from the login page.

IdP-specific issuer formats

IdPIssuer URL template
Authentikhttps://auth.example.com/application/o/liberty/ (trailing slash matters)
Keycloakhttps://kc.example.com/realms/<realm> (and enable the groups mapper on the Liberty client)
Azure AD / Entrahttps://login.microsoftonline.com/<tenant>/v2.0 (set Groups claims in token config)
Oktahttps://<your-domain>.okta.com/oauth2/default
Google Workspacehttps://accounts.google.com (no group claim — use HD claim for domain restriction, manage roles in Liberty's Users tab)

The sign-in page

With OIDC on, the Sign in with SSO button appears next to the username/password form on the login page. The local form stays available for break-glass admins.


Token lifecycle

The lifetimes are editable in Settings → Framework → Authentication:

TokenDefaultEditor fieldStorageUse
Access token15 minAccess token lifetimeFrontend localStorage (key liberty_access)Sent as Authorization: Bearer … on every API call.
Refresh token7 daysRefresh token lifetimehttpOnly cookie (liberty_refresh), SameSite=StrictExchanged for a new access token when the previous one expires.

The frontend's API client transparently refreshes the access token when it gets a 401 with WWW-Authenticate: Bearer error="invalid_token". The failing request replays once with the new token.

Revocation

TriggerEffect
User logs outThe refresh token is dropped from the server-side store; the access token expires within the access TTL.
Operator clicks Revoke all sessions on the userSame as logout for every active session of the user.
Operator marks the user InactiveSubsequent token refreshes fail.
Restart of the framework with a new JWT signing keyEvery active access token becomes invalid immediately. Refresh tokens fail too — every user has to sign in again.

The JWT signing key is referenced by env-var name in the Authentication form (default ${LIBERTY_JWT_SECRET}). Rotating the key is a restart-required action.


Permissions

The Users tab is gated by users:read (view) / users:write (edit). The Roles tab follows the same model — see Roles & permissions. The Authentication sub-form on the Framework tab is gated by settings:framework.


Tips & best practices

  • Set the JWT signing key in production. An ephemeral key works for development but invalidates every session on each restart.
  • Use the database backend past a handful of users. TOML breaks down under concurrent edits; the DB backend handles it cleanly.
  • Keep at least one local admin even on OIDC installs. Break-glass access when the IdP is down is invaluable.
  • Mirror the IdP groups to Liberty roles 1:1. Cleanest contract — name your Liberty roles to match the IdP group names.
  • Rotate the JWT signing key on a schedule. Annual cadence is reasonable; the disruption is one re-sign-in per user.
  • Use Test sign-in on the OIDC form before saving. Catches wrong issuer URLs / unregistered redirect URIs early.

Under the hood

The Authentication settings live in config/app.toml under the [auth] and [auth.oidc] sections. Local user entries live in config/auth.toml (TOML backend) or the ly2_users tables (Database backend). Operators do not edit these files by hand — the Settings UI is the canonical interface. Advanced operators can still use the Raw TOML tab or the CLI for scripted setup.


What's next