Authentication and users
BoardGameTracker ships with local accounts. Authentication is on by default and every request to the API needs a signed-in user, except signing in, the health check, game covers and the public RSVP page for game nights.
First login
Section titled “First login”A user named admin is created on the first start. Its password comes from the ADMIN_PASSWORD environment variable; when that is empty the password is admin. Sign in, open Settings → Account and change it straight away.
The admin account is recreated with the same rule after a factory reset (Settings → Advanced → Danger zone), so set ADMIN_PASSWORD if you rely on that.
| Role | Can |
|---|---|
| Admin | Everything: manage users, change settings, reset data. |
| User | Add and edit games, sessions, players, locations, loans and game nights; upload rulebooks; use the rules assistant. |
| Reader | Look around: every page is read-only, the add/edit/delete buttons are hidden and the rules assistant is not available. |
Users are managed under Settings → Account → User management (admins only). Each user can be linked to a player, which the dashboard uses for the “welcome back” card and personal statistics.
Forgotten passwords
Section titled “Forgotten passwords”- An admin can reset any password from the user list; a temporary password is shown once.
- When email is configured, the login page shows a Forgot password link that mails a reset link. The link uses the Public URL setting under Settings → General, so set that to the address your users reach the app on; without it no reset email is sent.
Plain HTTP on your own network
Section titled “Plain HTTP on your own network”Local password login also works over plain HTTP, so a home install reached as http://192.168.1.20:5444 keeps working without certificates. The password then travels unencrypted, so anyone who can watch the network traffic can read it. Put any install that is reachable from outside your own network behind a reverse proxy with TLS and use the https address. Single sign-on is stricter and needs HTTPS, see below.
Running without authentication
Section titled “Running without authentication”Set AUTH_ENABLED=false to turn authentication off. Every visitor is then treated as an administrator, and the application prints a warning at startup to remind you of that.
Tokens and sessions
Section titled “Tokens and sessions”Logins use short-lived access tokens (15 minutes) and refresh tokens (7 days) signed with JWT_SECRET. The access token is kept only in the browser’s memory. The refresh token is an HttpOnly cookie named bgt_refresh, scoped to /api/auth, SameSite=Lax and marked Secure on HTTPS requests; the server stores it hashed. If a replaced refresh token comes back while its successor was never used (a refresh response lost to a reload or a flaky connection, or two tabs refreshing at once), the session continues with a fresh token. If a later token of that session was already used, the old token counts as stolen and every session of that user is revoked. Once a session ends, by signing out or by a revoke like this one, its access tokens stop working at once; other devices stay signed in after a sign-out. Changing JWT_SECRET signs everyone out.
Player photos under /images/profile are only served to signed-in browsers: logging in sets an HttpOnly cookie scoped to that path (it lives as long as the refresh token and is cleared on logout), and API clients can pass their bearer token instead. Game covers stay public. The public RSVP page therefore shows initials instead of photos.
Five failed logins for one username from one address block that username from that address for 15 minutes after the last failure (IPv6 clients are grouped per /64 network). On top of that, one address can try 10 logins per minute. After 20 failures for one username across all addresses, that username gets one attempt every 30 seconds until the failures age out; the account is slowed down, never locked. Behind a reverse proxy this only works when TRUSTED_PROXIES is set.
OpenID Connect
Section titled “OpenID Connect”One generic OpenID Connect provider (Keycloak, Authentik, Authelia, Zitadel, Entra ID, …) can sit next to local accounts. The flow is the authorization-code flow with PKCE, exchanged by the backend: the browser is sent to the provider, the provider sends it back to the backend, and the backend hands the session to the app without ever putting a token or code in a URL the app sees.
HTTPS is required
Section titled “HTTPS is required”Single sign-on only runs over HTTPS. Starting a sign-in or a link and both callbacks are refused over plain HTTP: the login page shows Single sign-on requires HTTPS and hides the provider button, and the link button under Settings → Account is hidden as well. The only exception is a loopback address (localhost, *.localhost, 127.0.0.1, [::1]) for development, which browsers treat as secure too.
The scheme is the one the browser used. Behind a reverse proxy that terminates TLS the application only sees it when TRUSTED_PROXIES contains the proxy and the proxy sends X-Forwarded-Proto; without that every request looks like plain HTTP and single sign-on is refused.
Under Settings → Single sign-on the provider cannot be enabled while the Public URL starts with http:// and is not a loopback address (or, without a public URL, while you are connected over plain HTTP); the page explains why and the server refuses the change with the same message. The sign-in cookies of the flow are always marked Secure.
Register the client at your provider
Section titled “Register the client at your provider”- Create a public client (no client secret is required; a secret is accepted but not needed with PKCE).
- Allow the two redirect URIs shown under Settings → Single sign-on → Register at the provider:
<PUBLIC_URL>/api/auth/oidc/callbackfor sign-in and<PUBLIC_URL>/api/auth/oidc/link-callbackfor linking an existing local account. Each has a copy button. - Allow the post-logout redirect URI from the same list,
<PUBLIC_URL>/login, so signing out of BoardGameTracker also ends the session at the provider and comes back to the login page. - Grant the scopes
openid profile email.openidis always requested, even when it is missing from the scopes field, because the sign-in relies on the id_token.
PUBLIC_URL (or the Public URL setting) must be the address users type in the browser, e.g. https://games.example.com. When it is not set, the backend derives it from the incoming request, which only works when the reverse proxy forwards the host and scheme (see TRUSTED_PROXIES); the settings page warns when this fallback is in use. Changing the public URL after setup changes the redirect URI, so update the provider registration as well.
Configure the provider in BoardGameTracker
Section titled “Configure the provider in BoardGameTracker”An administrator fills in the provider under Settings → Single sign-on. Enter the authority (issuer URL) and press Test discovery before saving: it fetches the provider’s discovery document and tells you whether the reported issuer matches what you typed. Most providers only need the button label, authority and client ID; the claim fields are filled in with the standard defaults and the endpoint fields are for providers that do not follow them.
The same configuration is available through the API (POST /api/admin/oidc-providers, also in Swagger when SWAGGER_ENABLED=true):
{ "displayName": "Company login", "authority": "https://sso.example.com/realms/games", "clientId": "boardgametracker", "scopes": "openid profile email", "autoProvisionUsers": true, "rolesClaimType": "groups", "adminGroupValue": "bgt-admins", "defaultRole": "User"}-
authority (the provider’s address, not BoardGameTracker’s) must use
https; plainhttpis accepted only for a provider on a local address, and single sign-on itself still needs HTTPS as described above. It must match theissuerin the provider’s discovery document. Pointing an existing provider at a different authority unlinks every account linked to it, because a user id from one issuer says nothing about the same id at another; users sign in with their password and link the provider again. -
autoProvisionUsers creates an account with the default role on first sign-in. When the username or the email already belongs to a local account that is not linked, the sign-in is refused and the user is told to log in with the password first and link the provider under Settings → Account → External logins. Accounts are never linked automatically by username or email.
-
The claim fields start with these defaults, and an empty field uses its default as well:
Field API property Default Username claim usernameClaimTypepreferred_usernameEmail claim emailClaimTypeemailDisplay name claim displayNameClaimTypenameAvatar claim pictureClaimTypepictureGroups/roles claim rolesClaimTypegroupsAdmin group value adminGroupValuebgt-admins -
rolesClaimType / adminGroupValue / defaultRole map the provider’s groups to a role:
Adminwhen the named group is present in that claim, otherwise the default role (UserorReader). With the defaults, members of a group namedbgt-adminsin thegroupsclaim become Admin, so create that group at your provider (or set your own group name) and make sure the provider sends the groups claim. For accounts created through the provider this is applied again on every sign-in, so a group change at the provider takes effect at the next sign-in. The last active admin is never demoted; the server logs a warning instead. -
When the provider sends no display name, the username becomes the display name; when it sends no email, the account gets a placeholder address ending in
@sso.external. At every later sign-in an empty display name and the placeholder email are filled in from the provider, but only when no other account uses that email. Values a user or admin set themselves are never overwritten, and local accounts that were linked to the provider keep their own profile. With debug logging enabled, the server logs which claim types (names only, no values) arrived in the id_token and the userinfo response, which helps when a claim seems to be missing. -
A role that an admin sets under Settings → Account is a local override and is no longer changed by the provider. Access → Let the identity provider manage the role hands it back. Local accounts that were linked to the provider later keep their local role.
-
displayName is the label of the sign-in button and of the linked account under Settings → Account. It can be changed at any time; links are stored per issuer and subject, so linked accounts keep working and the redirect URIs stay the same.
-
Only one provider can be configured; creating a second one is refused. The login page shows one Continue with … button. Switch Enabled off to hide the button without losing the configuration.
-
Removing the provider also removes every linked external account; people keep their local password if they have one.
Avatars from the provider
Section titled “Avatars from the provider”On every sign-in and link, BoardGameTracker reads the standard OIDC picture claim from the id_token or the userinfo response and uses that picture as the user’s avatar in the menu and the user lists. Set Avatar claim (pictureClaimType) when your provider puts the picture address in another claim. Your identity provider must include the claim, which usually means requesting the profile scope; if the provider sends no picture, users simply keep their initials.
- The server downloads the picture once, right after the sign-in, and stores it like an uploaded profile picture. Browsers never load it from the provider. The download only accepts
httpsaddresses (a provider onlocalhostmay use its own loopback address during development), refuses addresses that resolve to private, link-local or cloud metadata IPs unless they belong to the provider’s own host, follows redirects only on the same host, gives up after 5 seconds, and only accepts images up to 2 MB. - A picture the user uploads under Settings → Account always wins; the provider never replaces it. After a user removes their picture, the same provider picture is not imported again; it only comes back when the provider reports a different picture address.
- The picture is downloaded again only when its address changes. A failed download is logged as a warning, never blocks the sign-in, and is retried at the next sign-in.
- Some providers only fill the claim when their avatar setting allows it. In Authentik, for example, the
pictureclaim depends on the configured avatar source.
Linking an existing account
Section titled “Linking an existing account”Anyone with a local account can link it to the provider under Settings → Account → External logins. Afterwards both the password and the provider sign in to the same account, and the same screen shows when the link was made and lets the user unlink it again. An account created through the provider has no password, so its last external login cannot be unlinked until a password is set (Forgot password, or an admin reset). A sign-in through the provider is refused when the email already belongs to an unlinked local account, so link first.
How the sign-in is checked
Section titled “How the sign-in is checked”The backend exchanges the code itself and validates the returned id_token before it trusts anything in it: the signature against the provider’s published keys (cached, and fetched again when an unknown key id shows up), the issuer from the discovery document, the audience (the client ID, and azp when there are several audiences), the expiry with two minutes of clock skew, and the nonce of this sign-in. Only RS256 and ES256 are accepted. The sub of the id_token identifies the user; the userinfo endpoint is still called for extra claims when the provider has one, and its sub must match. When the provider sends the iss parameter on the callback (RFC 9207), it must be the provider’s issuer.
Disabling users and the break-glass account
Section titled “Disabling users and the break-glass account”An admin opens Settings → Account → Access next to a user to see the identity provider accounts linked to them, unlink one, or disable the user. A disabled user cannot sign in with a password or through the provider, their refresh tokens are revoked, their provider session is forgotten and access tokens that were already issued stop working right away. You cannot disable yourself or the last active admin.
What to expect
Section titled “What to expect”- Sign-in errors come back to the app as a short message (provider refused, email already registered, provisioning disabled, …); the details are in the server log.
- Local accounts keep working, and admins can still create local users (for example Reader accounts for the kids) while a provider is enabled. Keep the local
adminpassword somewhere safe: it is the way in when the provider is down. - Removing a user at the provider does not end an existing BoardGameTracker session. Disable or delete the user under Settings → Account as well.
- Signing out of BoardGameTracker after a provider sign-in also signs out at the provider when its discovery document advertises an
end_session_endpoint; the id_token needed for that stays on the server. - For development with the Vite dev server set
PUBLIC_URL=http://localhost:5443so both the provider round trip and the app hand-off go through the dev proxy.