ADR 0002: SCIM-auth via Service Account Token en tenant-uuid in URL
- Status: geaccepteerd
- Datum: 2026-05-15
- Beslissers: Roy Milder
Context
De eerste implementatie van het SCIM-endpoint (/scim/v2/...) draaide op de
public-router en had alleen een check op tenant.scimConfig.enabled. Er was
geen tokenverificatie. De tenant werd afgeleid via expressTenant
(host/referer/header/default), wat voor Azure-SCIM-verkeer onbruikbaar is:
Azure stuurt verzoeken met alleen een Bearer-token, zonder host- of
referer-context. In de praktijk viel de tenant-resolutie terug op de
default-tenant, waardoor schrijfacties op de verkeerde tenant terecht konden
komen.
Daarnaast adverteerden we in de setup-instructies een “JWT token van een tenant-gebruiker” als secret. Gebruikers-JWT’s verlopen na enkele uren tot dagen, terwijl Azure Provisioning de secret nooit roteert. Dat zou kort na elke inlogcyclus stuk gaan.
We hebben dus twee koppelingsproblemen tegelijk: hoe authenticeren we het verzoek, en hoe weten we welke tenant het betreft?
Beslissing
We gebruiken een Service Account Token (SAT) als bearer en zetten de
tenant-uuid in het URL-pad: /scim/v2/:tenantUuid/....
- SAT’s bestonden al voor andere server-to-server-integraties
(
apps/api/src/api/service-account-token/). Een SAT is per tenant, 365 dagen geldig, intrekbaar via de bestaande UI, en wordt gehasht (SHA-256) opgeslagen. Hergebruiken voorkomt een tweede token-type. - De tenant-uuid in het URL-pad volgt de Microsoft-conventie voor multi-tenant SCIM-aanbieders. Het zorgt dat de tenant-resolutie deterministisch en expliciet is, zonder afhankelijk te zijn van host- of referer-headers.
Een nieuwe middleware scimAuthMiddleware doet beide checks: hij leest
:tenantUuid uit de URL, valideert het token via
serviceAccountTokenService.validateToken, en mount de tenant-context via
asyncLocalStorage zodat downstream-repositories op de juiste mongoose-
connection draaien. De scimRouter wordt gemount op app.use vóór
expressTenant(), zodat SCIM zijn eigen tenant-resolutie afdwingt.
Gevolgen
- Positief:
- Eén token-systeem (SAT) voor alle server-to-server-integraties.
- Geen rotatie- of expiratie-verrassingen voor Azure-klanten (365 dagen, intrekbaar wanneer dat past).
- Tenant-resolutie is expliciet en niet afhankelijk van header-heuristiek; een verkeerd token op de URL van tenant B faalt deterministisch met 401, omdat SAT’s per tenant in de tenant-DB staan.
- Alle 4xx/5xx-antwoorden gebruiken de SCIM-error envelope, zodat Azure-foutmeldingen leesbaar blijven voor de admin.
- Negatief:
- Klanten moeten in Azure de Tenant-URL configureren met de tenant-uuid
erin, niet de generieke
/scim/v2. Onze setup-instructies en de admin-UI zijn daarop aangepast. - SCIM is daarmee de enige route die
expressTenantoverslaat. Dat vraagt om expliciete documentatie zodat nieuwe collega’s niet zoeken naar eenreq.tenantdie er via de normale flow had moeten zijn.
- Klanten moeten in Azure de Tenant-URL configureren met de tenant-uuid
erin, niet de generieke
- Risico’s:
- Iemand kopieert de URL zonder uuid in een Azure-config. Mitigatie: de admin-UI laat altijd de tenant-specifieke URL zien met copy-knop; de setup-instructies bevatten de uuid in elke stap.
- Een SAT lekt. Mitigatie: SAT’s zijn intrekbaar via de bestaande UI en
worden alleen gehasht opgeslagen. We loggen SCIM-auth-fouten op
loggerService.errorzodat repeated failed attempts zichtbaar zijn.
Alternatieven
Dedicated SCIM-token-type. Een nieuw token alleen voor SCIM, opgeslagen
in tenant.scimConfig. Afgewezen omdat SAT al volstaat en een tweede
token-systeem alleen maar complexiteit toevoegt (eigen creatie-flow, eigen
rotatie, eigen UI, eigen storage).
JWT van een tenant-user. De oorspronkelijke documentatie-intentie. Afgewezen omdat user-JWT’s te kort leven voor Azure’s never-rotate-model. Bovendien zou de identiteit van de gebruiker (audit-trail, RBAC) niet overeenkomen met wat SCIM doet: provisioning is een server-to-server-actie, niet een actie namens een specifieke admin.
Tenant-uuid in een custom token-claim. Vermijdt het URL-pad-element. Vraagt om een nieuw token-type met embedded tenant en is daarmee strikt zwakker dan de URL-variant: de URL-resolutie is leesbaar in toegangslogs en in elke proxy of WAF, terwijl een claim alleen na decoding zichtbaar wordt.
Verwante documenten
- Spec:
docs/superpowers/specs/2026-05-15-scim-fix-design.md - Plan:
docs/superpowers/plans/2026-05-15-scim-fix.md - Explanation: SCIM-provisioning in Tapster
- How-to: SCIM-koppeling configureren