feat: Azure (Entra ID) login as user login (#16)
Replaces the client-side PIN login with real authentication against Azure Entra ID via PocketBase's built-in OAuth2 (OIDC) flow. Every Azure user is auto-provisioned as a team_member on first login. - pb_migrations: team_members becomes a PocketBase auth collection (OIDC provider configured from env); all collections require @request.auth.id - pb_hooks: provisioning of role (ENTRA_ADMIN_EMAILS allow-list) and enrollment_status, with admin-role re-sync on every login - frontend: "Sign in with Microsoft" login, pb.authStore-based session, TeamManager manages roster/roles (no PIN/manual create) - infra: Entra env wiring + pb_hooks mount for dev (Labs) and prod - src/lib/azureAuth.js + tests for the canonical role/allow-list logic - docs/auth-spec.md Knowledge base, generated tests and micro-learnings are left untouched. Existing PIN users are dropped (no migration required, per sign-off). Closes #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
105
docs/auth-spec.md
Normal file
105
docs/auth-spec.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Authentication — Azure (Entra ID) login
|
||||
|
||||
> Implemented for issue #16. Replaces the previous client-side PIN login.
|
||||
|
||||
## Doel
|
||||
|
||||
Elke gebruiker logt in met zijn Respellion **Microsoft (Azure Entra ID)**-account.
|
||||
Bij de eerste login wordt automatisch een `team_members`-record aangemaakt. Er is
|
||||
geen PIN, geen handmatige user-aanmaak en geen client-side wachtwoordcontrole meer.
|
||||
|
||||
## Architectuur in één oogopslag
|
||||
|
||||
```
|
||||
Browser (SPA) PocketBase (auth) Entra ID
|
||||
───────────── ───────────────── ────────
|
||||
"Sign in with Microsoft"
|
||||
└─ pb.collection('team_members')
|
||||
.authWithOAuth2({provider:'oidc'})
|
||||
──────────────────► opens OIDC popup ───► login.microsoftonline.com
|
||||
token exchange ◄─── (server-side, client secret)
|
||||
◄────────────────── auth record + token
|
||||
pb.authStore (token in localStorage)
|
||||
```
|
||||
|
||||
- **PocketBase is de OIDC-client.** De token-exchange en het client-secret blijven
|
||||
server-side in PocketBase — er is geen aparte backend en geen client-side key.
|
||||
- **`team_members` is een PocketBase `auth`-collection.** De record-`id` blijft de
|
||||
user-identifier die alle voortgangscollecties (`test_results`,
|
||||
`on_demand_attempts`, `micro_learning_completions`, `leaderboard`,
|
||||
`theme_session_completions`) als `user_id` referencen.
|
||||
- **Sessie** = `pb.authStore` (token in `localStorage`), niet langer een
|
||||
`team_members.id` in `sessionStorage`.
|
||||
|
||||
## Belangrijke beslissingen (ADR)
|
||||
|
||||
| ADR | Beslissing | Reden |
|
||||
|---|---|---|
|
||||
| 001 | OIDC-provider tegen Entra v2-endpoints i.p.v. eigen MSAL-flow of header-trust | Sluit aan op "PocketBase = enige backend"; secret server-side; auto-provisioning gratis; werkt los van de perimeter-implementatie |
|
||||
| 002 | `team_members` hergebruikt als auth-collection (zelfde naam) | Alle `user_id`-referenties blijven werken; minimale blast-radius |
|
||||
| 003 | Provisioning + admin-rol via `pb_hooks/team_members.pb.js` | Rol-logica server-side, niet in de client |
|
||||
| 004 | Admin-rol via e-mail-allowlist (`ENTRA_ADMIN_EMAILS`), re-synced elke login | Geen handmatig beheer; allowlist-wijziging werkt bij volgende login |
|
||||
| 005 | API-rules → `@request.auth.id != ""` op alle app-collecties | Geen anonieme toegang meer; admins/users zijn beide geauthenticeerd |
|
||||
|
||||
## Bestanden
|
||||
|
||||
| Bestand | Rol |
|
||||
|---|---|
|
||||
| `pb_migrations/1781000000_team_members_to_auth.js` | Dropt de oude PIN-collection, maakt `team_members` als auth-collection met de OIDC-provider (creds uit env). |
|
||||
| `pb_migrations/1781000001_tighten_api_rules.js` | Zet alle niet-systeem-collecties op `@request.auth.id != ""`. |
|
||||
| `pb_hooks/team_members.pb.js` | Auto-provisioning (`role`, `enrollment_status`, naam-fallback) + admin-rol re-sync. |
|
||||
| `src/lib/azureAuth.js` | Canonieke, unit-geteste helpers (`OIDC_PROVIDER`, `resolveRole`, `parseAdminEmails`, `deriveName`). |
|
||||
| `src/store/AppContext.jsx` | `loginWithAzure()` / `logout()` op basis van `pb.authStore` + `authRefresh()`. |
|
||||
| `src/pages/Login.jsx` | "Sign in with Microsoft"-scherm (geen dropdown/PIN). |
|
||||
| `src/components/admin/TeamManager.jsx` | Roster-beheer: rol wijzigen + verwijderen (geen aanmaken/PIN). |
|
||||
|
||||
> De rol-/allowlist-logica bestaat tweemaal: canoniek in `src/lib/azureAuth.js`
|
||||
> (met tests) en herhaald in `pb_hooks/team_members.pb.js` (PocketBase JSVM kan de
|
||||
> module niet importeren). **Houd ze in sync.**
|
||||
|
||||
## Configuratie (environment)
|
||||
|
||||
Gerenderd in `.env` door de Ansible deploy-playbooks en doorgegeven aan de
|
||||
`pocketbase-learning`-container:
|
||||
|
||||
| Variabele | Betekenis |
|
||||
|---|---|
|
||||
| `ENTRA_TENANT_ID` | Entra tenant-id (of `common`). |
|
||||
| `ENTRA_CLIENT_ID` | App-registration client-id. |
|
||||
| `ENTRA_CLIENT_SECRET` | App-registration client-secret. |
|
||||
| `ENTRA_ADMIN_EMAILS` | Komma-gescheiden e-mail-allowlist die `role=admin` krijgt, bv. `rve@respellion.nl,admin@respellion.nl`. |
|
||||
|
||||
Ansible-variabelen (vault): `entra_tenant_id`, `entra_client_id`,
|
||||
`entra_client_secret`, `entra_admin_emails`.
|
||||
|
||||
## Entra App Registration (eenmalig, buiten de codebase)
|
||||
|
||||
1. **Redirect-URI (Web):** `https://<host>/api/oauth2-redirect`
|
||||
- Labs: `https://learning-platform.labs.respellion.tech/api/oauth2-redirect`
|
||||
- Lokaal dev: `http://localhost:8090/api/oauth2-redirect` (PB-origin)
|
||||
2. **API permissions / scopes:** `openid`, `email`, `profile`.
|
||||
3. Genereer een client-secret en zet de waarden in de Ansible-vault.
|
||||
|
||||
## Uitbreidingspunten
|
||||
|
||||
- **Silent SSO (geen tweede klik):** als bevestigd is dat de perimeter veilige,
|
||||
niet-spoofbare identity-headers doorstuurt, kan een PocketBase-route die headers
|
||||
vertrouwen en direct een token minten (ADR-004-alternatief).
|
||||
- **Least-privilege rules:** schrijf/verwijder op de knowledge-authoring-collecties
|
||||
(`topics`, `relations`, `content`, `sources`, `question_bank`,
|
||||
`curriculum_versions`) verder beperken tot `@request.auth.role = "admin"`. Let op:
|
||||
`micro_learnings` en `theme_sessions` worden door reguliere users geschreven
|
||||
(generate-then-cache). Vereist runtime-verificatie.
|
||||
- **Rol op Entra group-claims** i.p.v. e-mail-allowlist (aanpassing in
|
||||
`pb_hooks/team_members.pb.js` + `src/lib/azureAuth.js`).
|
||||
|
||||
## Bekende aandachtspunten / go-live
|
||||
|
||||
- De OIDC-popup is een top-level navigatie naar `login.microsoftonline.com`; de
|
||||
bestaande Caddy-CSP (`connect-src`) raakt dit niet. Verifieer alsnog bij de
|
||||
eerste deploy.
|
||||
- Verweesde voortgangsrecords van verwijderde (pre-Azure) users zijn acceptabel:
|
||||
zonder geldig Entra-account kan niemand inloggen, dus die records zijn onbereikbaar.
|
||||
- Credential-rotatie: update env + herconfigureer de provider via de PocketBase
|
||||
admin-UI (Settings → Auth providers) of ship een follow-up migratie — de
|
||||
create-migratie draait maar één keer.
|
||||
Reference in New Issue
Block a user