| type | Auth Model | |||
|---|---|---|---|---|
| title | Auth Model | |||
| description | Login flows for lecturers and participants, origin-based cookie selection in the backend, JWT scopes, and LTI launch rules. | |||
| timestamp | 2026-07-07 | |||
| tags |
|
The non-obvious core: the backend chooses which auth cookie to read based on the request's Origin header. jwtMiddleware (apps/backend-docker/src/app.ts) inspects req.headers.origin against the APP_MANAGE_SUBDOMAIN/APP_CONTROL_SUBDOMAIN/APP_STUDENT_SUBDOMAIN env vars (defaults manage/control/pwa):
| Request origin | Cookie(s) tried, in order |
|---|---|
| manage / control | next-auth.session-token |
| pwa | participant_token → temporary_participant_token → next-auth.session-token |
assessment (ASSESSMENT_MODE=true) |
next-auth.participant-session-token |
A Bearer authorization header is always the final fallback (assessment live-quiz mode depends on it — marked DO NOT TOUCH in the source). Whatever token is found is verified with verifyJWT(token, APP_SECRET); failure just yields an unauthenticated context, not an error. Consequence for local setups: apps and backend must share APP_SECRET, and cookie domains must match the origin the backend expects — this is why the Traefik *.klicker.com path mirrors production more faithfully than raw localhost.
NextAuth (Auth.js) with @auth/prisma-adapter, JWT session strategy with a custom encode (so the backend can verify the same token), configured in apps/auth/src/pages/api/auth/[...nextauth].ts. Two provider groups:
- Edu-ID OIDC (
EduIDLecturerProvider) — only registered whenEDUID_CLIENT_SECRETis set; scope includeshttps://login.eduid.ch/authz/User.Read. Without Edu-ID credentials (typical local dev), this provider is absent — use delegated login. - Delegated login (
CredentialsProvider) — authenticates againstUser.shortname+ aUserLoginrecord. EachUserLogincarries aUserLoginScopethat ends up astoken.scopein the JWT callback: the ladderACCOUNT_OWNER > FULL_ACCESS > SESSION_EXEC > READ_ONLYis enforced field-by-field in the API layer (three-layer auth). Edu-ID logins get scopeEDUID.
The NextAuth cookie domain is derived by stripping the first subdomain label from the auth URL, so the session cookie is shared across *.klicker.com-style app domains. The Catalyst flag is computed from Edu-ID affiliations (packages/util/src/auth.ts:reduceCatalyst — any uzh.ch/usz.ch affiliation).
- Username/password — PWA
LoginForm→ login mutation →participant_tokencookie; the PWA Apollo client additionally sends the token asBearerfrom sessionStorage. - Magic link —
services/accounts.ts:sendMagicLinksigns a 15-minute JWT and emails${APP_ORIGIN_PWA}/magicLogin?token=…; themagicLoginpage exchanges it viaLoginParticipantMagicLinkDocument(loginParticipantMagicLink). - Edu-ID for participants — separate NextAuth config in the same auth app (
EduIDParticipantProvider), sameEDUID_CLIENT_SECRETgating. - Temporary (anonymous) —
temporary_participant_tokencookie, roleTEMPORARY_PARTICIPANT. - LTI —
apps/lti(ltijs). Launch targets resolve in strict precedencecustom claim (klicker_redirect_to)→query redirectTo, with no env fallback; validation fails closed on the first present-but-invalid source and checks URL hostnames exact/subdomain againstCOOKIE_DOMAINandDF_DOMAIN— never substring matching (apps/lti/src/launchTarget.ts).
Note the account-duplication trap: participant emails are only unique per auth mode (@@unique([email, isSSOAccount]) — details in Data & Migrations).
Authentication (this page) only puts a verified user on the GraphQL context. All authorization — role gates, scope ladder, object-level permissions, sharing grants — is enforced per-field in the API layer; see GraphQL API Layer.