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 expressTenant overslaat. Dat vraagt om expliciete documentatie zodat nieuwe collega’s niet zoeken naar een req.tenant die er via de normale flow had moeten zijn.
  • 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.error zodat 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