Repository navigation
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.
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.
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.
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 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.
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 afetchfrom an expired tab gets something parseable. -
NODE_ENV=developmenton alocalhost,127.0.0.1or0.0.0.0hostname → 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.
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
GETandHEADare allowed. Anything else is 403 on an API path and a redirect to/eventson 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/prefixesdevices,device,events,installs,identity,system,applications,hardware,network,security,management,inventory,peripherals.
Every
/api/adminroute 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/dashboardrefresh. - pages:
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.
/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.
AGPL-3.0 · reportmate.app