Skip to content

Authentication

Rod Christiansen edited this page Oct 8, 2026 · 2 revisions

Authentication

The dashboard uses NextAuth v4 with a JWT session strategy. Sessions last 24 hours, cookies are marked secure when NODE_ENV=production, and the sign-in and error pages are /auth/signin and /auth/error. Everything below is from lib/auth.ts, lib/auth-roles.ts, src/lib/kiosk/ and middleware.ts.

Providers

AUTH_PROVIDERS is a comma-separated list, defaulting to azure-ad. Only the providers named there are constructed.

Provider key Package provider Status
azure-ad next-auth/providers/azure-ad The supported path. Requires AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET, AZURE_AD_TENANT_ID; the module throws at load if any is missing
google next-auth/providers/google Wired and functional. Requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET
credentials next-auth/providers/credentials Present but deliberately non-functional — its authorize logs an error and returns null, so it authenticates nobody

Do not enable the credentials provider expecting a local login; it rejects every attempt by design.

The Entra ID profile mapping keeps sub as the user id, email (falling back to preferred_username), the tenant id from tid, and the roles claim.

Redirect URI

Register <your base URL>/api/auth/callback/azure-ad with the identity provider. The provider config builds that URI from NEXTAUTH_URL, falling back to NEXT_PUBLIC_SITE_URL, and the redirect callback rewrites any host that does not match. Behind a proxy or CDN, set NEXTAUTH_URL to the public URL or callbacks will point at the container's own address.

Domain restriction

ALLOWED_DOMAINS is a comma-separated list of email domains. The signIn callback refuses any address whose domain is not listed. An empty value allows every domain, so on any deployment reachable from the internet, set it.

REQUIRE_EMAIL_VERIFICATION=true additionally demands email_verified on the profile — but only for the Google provider.

Roles

Roles arrive in the roles claim, are copied into the JWT, and surface as session.user.roles. There is one role the code checks: admin, defined as ADMIN_ROLE in lib/auth-roles.ts.

requireAdmin(request) is the server-side guard used by every admin route handler. It reads the JWT directly, returns a 401 {"error":"Unauthenticated"} when there is no token and a 403 {"error":"Forbidden","detail":"Requires role admin"} when the token lacks the role, otherwise it returns a minimal session object.

Grant the role through your identity provider's app-role assignment. The claim is stamped into the JWT at sign-in, so a newly granted role only appears after the user signs out and back in.

What the middleware enforces

middleware.ts matches every path except static assets, api/auth, api/healthz, api/health, api/transmission and api/version, plus a further public-prefix list covering /auth, /kiosk, PWA assets and the service worker.

  • No session, page request → redirect to /auth/signin?callbackUrl=<url>.
  • No session, /api/* request → 401 {"error":"Unauthorized"} as JSON, so a fetch from an expired tab gets something parseable.
  • NODE_ENV=development on a localhost, 127.0.0.1 or 0.0.0.0 hostname → auth skipped entirely.
  • NEXT_PUBLIC_DEMO_MODE=true → auth skipped entirely, for a public demo.

The upstream X-Internal-Secret authenticates the proxy to the API, not the user, so the middleware is the only thing preventing anonymous reads of the data routes. Do not add a data route to the public-prefix list.

Kiosk (viewer) sessions

A wall display should not hold a person's session. KIOSK_TOKENS holds one opaque token per screen, comma-separated, either label:token or a bare token; entries shorter than 16 characters are discarded.

Opening /kiosk?token=<token> mints a NextAuth JWT with role: "viewer" and redirects to the kiosk home page from Settings (or to ?next=<path>, or to /events if settings cannot be read). Two cookies are set: the normal session cookie, and a long-lived cookie pinning the token, because NextAuth re-issues session JWTs with the app's 24-hour lifetime. When the session lapses, the middleware re-mints it from the pinned token, so a display never lands on the sign-in page while its token is still listed.

A viewer session is read-only by construction:

  • Only GET and HEAD are allowed. Anything else is 403 on an API path and a redirect to /events on a page path.

  • Only an allow-list of paths is reachable, from src/lib/kiosk/tokens.ts:

    • pages: /, /dashboard, /events, /devices, /device/…, /system, /installs, /applications, /hardware, /network, /security, /management, /inventory, /identity, /peripherals, /reports;
    • API: /api/settings (read only, by the method rule), /api/dashboard, /api/events, /api/device, /api/device-names, /api/version, and the /api/v1/ prefixes devices, device, events, installs, identity, system, applications, hardware, network, security, management, inventory, peripherals.

    Every /api/admin route is outside it, and so is /api/v1/negotiate, so a kiosk dashboard does not open the live stream; its feed updates from the 30-second /api/dashboard refresh.

Rotating KIOSK_TOKENS invalidates every display session immediately. Token comparison is constant-time and implemented without Node crypto so it runs in the edge middleware runtime.

Checking a deployment

/api/test-auth reports which auth variables are set (as SET / MISSING, never the values) and whether the provider list loaded. It is a useful first call when sign-in fails at startup rather than at the identity provider.

Clone this wiki locally