Coloriginz Design Systemshadcn custom registry

Gids — Microsoft Entra ID aansluiten

Hoe je een Coloriginz-app laat inloggen met een Microsoft-werkaccount. Geen component en geen @col/-item: het meeste werk zit in de tenant, niet in code, en juist dat deel is nergens anders vastgelegd.

Deze gids gaat over de aansluiting. Voor de knop op het inlogscherm bestaat wél een item: @col/sso-button.

Wat er draait, en wat niet

App Status NextAuth Provider-id
supplier-onboarding-vercel (Onboarding Portal) Live sinds 28 juli 2026 v4 azure-ad
floriday-app (Floriday middleware) Gebouwd, nooit aangesloten Auth.js v5 microsoft-entra-id
voorraadbeheer (SupplyHub) Gebouwd, nog niet aangesloten v4 azure-ad
growerportal2 Gebouwd, provider uit tot IT levert Auth.js v5 microsoft-entra-id

Begin een nieuwe app op Auth.js v5, niet op v4. De Onboarding Portal staat op v4 omdat hij daar al op stond; die lijn is onderhoudsmodus. Een migratie is voor een draaiende app geen kleinigheid — getServerSession(authOptions) wordt auth() in élke route, de middleware gaat op de schop, en het provider-id verandert, dus IT moet eerst nieuwe redirect URI's registreren voordat er nog iemand kan inloggen. Voor een nieuwe app is dat allemaal gratis, want je bouwt het één keer goed.

Bewijs en voorschrift zijn hier niet dezelfde app. Alleen de Onboarding Portal draait live, dus alles over de tenant, de redirect URI's en admin consent komt daarvandaan. Maar de regels rond accounttoestanden (§3.3) zijn in SupplyHub het verst uitgewerkt: daar is elke route die de toestand van een account schrijft langsgelopen, en daarbij kwamen gaten boven die de portal ook had (§6). Voor de code in §3 en §4 is SupplyHub daarom de referentie; waar de portal of growerportal2 iets anders doet, staat dat erbij.

Checklist voor een nieuwe app

  1. App Registration aanvragen bij IT (§1) — doorlooptijd is dagen, begin hier
  2. Redirect URI's bepalen (§2) en meesturen met de aanvraag
  3. Admin consent aanvragen (§1) — aparte stap, meest voorkomende blokkade
  4. De vier beslissingen nemen (§3) en de signIn-callback bouwen
  5. Elke route die de toestand van een account schrijft langslopen (§3.3)
  6. Bestaande database: schema, backfill, controle — pas dán de env-vars (§3.3)
  7. Env-vars in Vercel op Preview én Production
  8. Testen op preview — lokaal kan niet (§2)

1. Wat je aan IT vraagt

Contact: systems@dfg.nl.

Eén App Registration per app. Niet per omgeving en niet per label: test en productie delen dezelfde registratie, en alle merkdomeinen ook. Ze verschillen alleen in redirect URI, en die mogen naast elkaar bestaan.

Single tenant (AzureADMyOrg). De tenant 29ebd335-b1bc-4b1d-b89b-ea6e27378762 bevat coloriginz.com, freshfromsource.com, mypeonysociety.com én parfumflowercompany.com. Eén registratie bedient dus alle vier de labels. col.com zit in een andere tenant — accounts op dat domein kunnen niet via deze SSO inloggen. Een domein controleren kan zonder IT:

https://login.microsoftonline.com/<domein>/v2.0/.well-known/openid-configuration

De issuer in het antwoord bevat de tenant-id.

Permissies: openid, profile, email, User.Read, allemaal delegated. Meer heb je niet nodig — je vraagt alleen het profiel van de persoon die op dat moment inlogt. Dat is ook het argument waarmee je de aanvraag onderbouwt: geen toegang tot mail, agenda, bestanden of gegevens van anderen.

Admin consent is een aparte stap en de meest voorkomende blokkade. In deze tenant mogen gebruikers zelf geen toestemming geven aan apps. Zonder eenmalige goedkeuring door een Global Admin krijgt iedereen "Need admin approval" — de credentials zijn dan gewoon goed, de app is alleen niet toegelaten. Reken erop dat dit een tweede mail wordt: het is niet vanzelfsprekend dat IT deze stap meeneemt bij het aanmaken.

De status is niet van buitenaf te controleren. De app-credentials mogen oauth2PermissionGrants niet uitlezen (403), dus de enige test is opnieuw inloggen.

Mailsjablonen voor beide aanvragen staan in bijlage A en B.

2. De redirect URI's — hier gaat het mis

De vorm is altijd:

https://<host>/api/auth/callback/<provider-id>

Het provider-id verschilt per NextAuth-versie. Op v4 heet de provider azure-ad, op Auth.js v5 microsoft-entra-id. Neem dus nooit een lijst URI's over van een app die op de andere versie draait — de registratie is dan syntactisch geldig en werkt alsnog niet.

Registreer het custom domein, niet de .vercel.app-URL. Gebruikers komen binnen via onboarding-portal.apps.coloriginz.com, en dát is de host die in de callback belandt.

Eén URI per domein per omgeving. Bij multi-label loopt dat op: de Onboarding Portal heeft twee omgevingen × vier labeldomeinen. Voor parfumflowercompany.com bestaat nog geen DNS-record voor het portaal, dus die twee staan nog open.

Localhost staat er bewust niet bij. SSO is daardoor lokaal niet te testen; gebruik een preview-deployment. Houd de credentials-login werkend zolang er nog ontwikkeld wordt, anders kun je lokaal helemaal niet meer inloggen.

Multi-domein vraagt geen codewijziging. Dit is eerder verkeerd aangenomen: NEXTAUTH_URL blokkeert het niet. Zodra process.env.VERCEL gezet is — altijd, op Vercel — negeert NextAuth die variabele en gebruikt het de x-forwarded-host van het request (next-auth/utils/detect-origin.js). Client-side roept signIn() een relatief pad aan. De app genereert dus vanzelf per domein de juiste redirect_uri. Het enige wat ontbreekt is de registratie in Azure.

3. De vier beslissingen die je zelf moet nemen

3.1 SSO maakt nooit accounts aan

Een geslaagde Entra-login geeft toegang tot een bestaand account. Bestaat er geen gebruiker met dat adres, dan weiger je — ook al is de persoon aantoonbaar een collega.

Zonder die regel kan iedereen in de tenant binnenlopen zodra de koppeling live gaat. Beide apps hanteren hem, en het is de enige beslissing in deze lijst waar niets aan te kiezen valt.

3.2 Welke claim is de identiteit?

for (const raw of [claims.email, claims.preferred_username, claims.upn]) {
  const normalized = raw?.trim().toLowerCase()
  if (normalized) return normalized
}
return null

Azure vult de email-claim alleen als het mail-attribuut van het account gevuld is. Bij accounts waar dat leeg is — en die bestaan — mislukt een login die alleen op email leunt, met een foutmelding die nergens naar wijst. De fallback naar UPN vangt dat op.

Schrijf die keten niet met ??. email ?? preferred_username ?? upn stopt bij een lege string, want '' is niet null. Een account met een leeg mail-attribuut dat als "" binnenkomt, valt dan niet door naar de UPN en wordt geweigerd — precies het geval waar de fallback voor bestaat. Laat leeg en alleen spaties net zo doorvallen als ontbrekend. De Onboarding Portal gebruikt nog ??; SupplyHub (resolveSsoEmail in src/lib/sso.ts) en growerportal2 doen het goed.

Over email_verified: Entra stuurt die claim standaard niet mee. Een fail-closed controle erop weigert daardoor élke login. floriday-app heeft die controle wel (decideEntraSignIn in src/features/auth/entra-linking.ts, reason email-not-verified); dat is veilig, maar het betekent dat die app pas kan inloggen nadat de regel is herzien. Het commentaar in auth-config.ts benoemt dat zelf.

In een single-tenant app is die controle ook niet waar de beveiliging op rust: de adressen worden door IT beheerd, niemand kiest zijn eigen UPN, en §3.1 zorgt dat een onbekend adres sowieso niet binnenkomt. Wil je het harder, vraag dan xms_edov of verified_primary_email aan in de app-registratie in plaats van te vertrouwen op een claim die er niet is.

Vergelijk hoofdletterongevoelig. Alle apps normaliseren naar lowercase en zoeken met Prisma's mode: 'insensitive'. Geef die findFirst wel een orderBy (bijvoorbeeld createdAt: 'asc'): een unieke index in Postgres laat twee adressen toe die alleen in hoofdletters verschillen, en zonder volgorde is de match dan willekeurig.

Leg daarnaast de oid vast, ook als je er nog niet op matcht. Het e-mailadres is de praktische sleutel, maar geen stabiele: mensen veranderen van naam, en een adres kan opnieuw worden uitgegeven aan iemand anders. De oid (object id) is per gebruiker per tenant onveranderlijk en is dus waar je uiteindelijk op wilt koppelen. Schrijf hem bij elke SSO-login weg naar een nullable, unieke kolom; dan kun je later overstappen zonder dat iedereen opnieuw gekoppeld moet worden.

Doe dat wegschrijven wel best-effort, in een try/catch. Zolang er niets op authenticeert mag een botsing op die unieke kolom — bijvoorbeeld een oude rij die de oid nog vasthoudt nadat iemands adres veranderde — nooit een geslaagde login alsnog laten mislukken.

3.3 Eén vlag kan niet twee dingen betekenen

Dit is de valkuil waar de referentie-implementatie zelf in liep, en hij is het waard om uitgeschreven te lezen.

De Onboarding Portal had één boolean, isActive, die drie toestanden dekte: nooit geactiveerd, uitgezet door een beheerder, en soft-deleted. De SSO-callback activeerde bij false het account onvoorwaardelijk — bedoeld voor de eerste toestand, met als gevolg dat de andere twee er ook onder vielen. Een beheerder kon iemand dus uitzetten of verwijderen, waarna diezelfde persoon — nog gewoon lid van de tenant — via "Inloggen met Microsoft" weer binnenkwam met zijn oude rollen. Het wachtwoordpad weigerde hem wel. Klassiek: een tweede inlogroute die een controle overslaat die de eerste wél uitvoert.

Scheid de toestanden voordat je auto-activatie aanzet. De portal doet dat nu met een deactivatedAt DateTime? naast isActive:

isActive deactivatedAt Betekenis SSO
false null uitgenodigd, nooit geactiveerd activeren en toelaten
false gezet uitgezet of verwijderd door een beheerder weigeren
true null gewoon actief toelaten

De vierde combinatie, true met een tijdstempel, hoort niet te bestaan. Kom je hem tegen, dan heeft een route de ene helft geschreven en de andere vergeten: het wachtwoordpad laat zo iemand binnen en SSO weigert hem.

Elke schrijver van het paar

Het veld toevoegen is het makkelijke deel. Het werk zit in elke route die isActive, deactivatedAt of een activatietoken schrijft, want één route die het paar half bijwerkt, heropent de zijdeur die deactivatedAt moest sluiten. Loop ze allemaal langs; in een gewone app zijn het er deze:

Route Moet doen
Uitnodigen isActive: false, deactivatedAt blijft null, activatietoken van dagen
Uitzetten door beheerder deactivatedAt alleen stempelen bij een echte overgang van true naar false, en in dezelfde update elke openstaande activatie- of resettoken wissen
Soft delete Idem: een verwijdering is ook een deactivering
Aanzetten door beheerder isActive: true en deactivatedAt: null
Uitnodiging intrekken deactivatedAt zetten en de token wissen, zonder isActive aan te raken (zie hieronder)
Activatie via de link Weigeren als deactivatedAt gezet is; bij succes isActive: true én deactivatedAt: null
Wachtwoord resetten Weigeren als isActive uit staat
Activatiemail opnieuw sturen Weigeren als deactivatedAt gezet is, niet alleen als er een wachtwoord is
SSO signIn-callback Alleen (false, null) activeren, en daarbij de token wissen

Vier van die regels zijn niet vanzelfsprekend, en de portal miste ze alle vier tot 26 september 2026 (zie §6 voor de stand):

Een uitnodiging intrekken vraagt een eigen actie. Een uitgenodigd account staat al op isActive: false, dus "uitzetten" is voor dat account geen overgang: een gewone bewerking stuurt dezelfde payload. Zonder aparte actie is er geen weg van "uitgenodigd" naar "uitgezet", en blijft auto-activatie via SSO openstaan voor iemand die je niet meer wilt toelaten. SupplyHub heeft daarvoor POST /api/admin/employees/[id]/revoke-invitation.

Laat de beheer-UI die actie ook aanbieden. Een gewone aan/uit-knop biedt bij een uitgenodigd account alleen "aanzetten". Dat is geen fout, maar het betekent wel dat een beheerder die zich bedenkt maar één omweg heeft: eerst aanzetten en dan weer uitzetten. Tussen die twee klikken kan de persoon echt binnenkomen. Toon bij een uitgenodigde rij dus "uitnodiging intrekken", en na het intrekken een rij met de status "uitgezet". Daarvoor heeft de tabel deactivatedAt nodig: isActive en de aanwezigheid van een wachtwoord onderscheiden "uitgenodigd" niet van "ingetrokken".

Heeft jouw app een aparte uitnodigingsflow — zoals floriday-app, dat isActive: false alleen gebruikt voor "uitgezet" — dan is auto-activatie sowieso niet aan de orde en weiger je gewoon.

Invoeren in een bestaande database — de volgorde is een beveiligingsregel

Bij het invoeren hiervan in een bestaande database moet je de zittende rijen alsnog verdelen. De portal gebruikte daarvoor: wie ooit een wachtwoord heeft gezet (passwordHash IS NOT NULL) heeft het account in gebruik gehad en is dus uitgezet; wie er geen heeft en nog een openstaande activatietoken, is nooit begonnen. Dat gaf op productie precies de goede splitsing.

Een nieuwe kolom is null op elke rij, en (false, null) betekent "uitgenodigd". Tussen de schema-push en de backfill staat dus elke uitgezette gebruiker als uitnodiging in de database, en die activeert de SSO-callback. Een oud-medewerker die nog in de tenant zit, kan in dat venster inloggen en zijn account blijvend aanzetten. Per omgeving, in deze volgorde:

  1. Schema pushen
  2. Backfill als dry run draaien en élke naam lezen: iedereen onder "nooit geactiveerd" kan zichzelf via SSO activeren
  3. Backfill toepassen en controleren
  4. Pas dan de Entra-env-vars zetten en redeployen

De heuristiek zit op sommige rijen mis: iemand die is uitgenodigd en daarna uitgezet voordat hij ooit activeerde, komt aan de verkeerde kant uit. Corrigeer die met de hand vóór stap 3.

SupplyHub heeft hier een script voor met een dry run (scripts/backfill-deactivated-at.ts) en het draaiboek in zijn CLAUDE.md. growerportal2 voegt er een vangnet aan toe in de beslisfunctie zelf: een inactief account mét wachtwoord wordt altijd geweigerd, ook als deactivatedAt nog leeg is. Dat maakt een vergeten of mislukte backfill onschadelijk en kost één regel. Neem hem over.

Wat je bij een terechte auto-activatie wél doet: activationToken en activationExpiresAt wissen. Een openstaande activatielink hoort dood te zijn zodra het account langs een andere weg in gebruik is genomen.

Reken erop dat je gebruikers zónder wachtwoord krijgt, en regel hun terugweg. Wie via Entra binnenkomt kiest er nooit een. Dat is prima tot Microsoft-login een keer niet kan — een account buiten de tenant, een storing, een plek waar het geblokkeerd is. In de Onboarding Portal liep zo iemand vast: "wachtwoord vergeten" zoekt op een passwordHash, vond er geen, en gaf het neutrale "als het adres bekend is ontvangt u een mail" terug zonder iets te sturen. Terecht dat die route niet verraadt welke adressen bestaan, maar het effect was een gebruiker die niets kon en niet kon zien waarom.

De oplossing hoeft niet groot te zijn: laat dat geval doorvallen naar de activatiemail in plaats van de resetmail. Naar buiten toe verandert er niets, dus de anti-enumeratie blijft heel. Geef die token wel de korte levensduur van een reset en niet de dagen van een uitnodiging — hij zet een wachtwoord op een account dat al in gebruik is.

Zet in dezelfde beweging beide routes op de activatiepagina zelf. Voor de meeste mensen is die pagina de eerste kennismaking met het systeem, en "log in met Microsoft of maak een wachtwoord aan" is daar een betere eerste indruk dan een formulier dat doet alsof de ene route niet bestaat.

3.4 Hoe geef je een fout terug?

Een signIn-callback kan geen foutmelding tonen; hij kan alleen true, false, of een redirect-URL teruggeven. Het patroon is dus: redirect naar de loginpagina met een code, en die code aan de leeskant vertalen.

Lees de code nooit rechtstreeks uit de querystring in je UI — controleer hem tegen een vaste lijst. De Onboarding Portal doet dat met validSsoErrors in src/app/login/page.tsx.

Kies één taal voor het contract en houd hem vast. De Onboarding Portal gebruikt ?error=AccountNotFound, floriday ?fout=no-account; beide hebben de taal in het contract gebakken, en dat wringt zodra een derde app de andere kant op kiest. Voor nieuwe apps: Engelse parameternaam en Engelse codes, vertalen doe je pas in de UI.

Gebruik deze codes, zodat een later gedeeld item niet eerst twee contracten hoeft te verzoenen:

Code Wanneer
NoEmail geen bruikbare claim na de hele keten uit §3.2
AccountNotFound geen gebruiker met dat adres (§3.1)
AccountDeactivated deactivatedAt gezet, of het vangnet uit §3.3
ServerError de callback gooide een fout, zie hieronder

De portal en SupplyHub gebruiken deze al. growerportal2 noemt de eerste NoEmailClaim en heeft daarnaast AccountNotAllowed voor rollen die nooit in de tenant zitten (leveranciers, transporteurs). Die extra code is een goede toevoeging voor elke app met externe gebruikers.

Zet daarnaast NextAuth's eigen codes op de lijst die je wilt vertalen, in elk geval OAuthCallback, OAuthSignin en Callback. Die gebruikt NextAuth zelf als het inloggen onderweg misgaat, bijvoorbeeld als Azure de redirect URI afwijst.

Vang fouten in de signIn-callback zelf af. Zonder try/catch zet NextAuth de ruwe foutmelding in de URL van zijn eigen foutpagina — bij een databasefout inclusief hostnaam en poort van de database — op een pagina zonder weg terug. Log de fout en stuur /login?error=ServerError terug.

4. Valkuilen

Zet prompt: 'select_account' op de authorization-params. Zonder dat logt Azure stil in met het laatst gebruikte account. Je merkt dat pas als je met twee accounts wilt testen en niet begrijpt waarom je steeds dezelfde sessie krijgt.

Schakel de provider op alle drie de env-vars, niet alleen op de client-id. De Onboarding Portal kijkt naar AZURE_AD_CLIENT_ID en geeft clientSecret en tenantId daarna met ! door. Dat werkt zolang alle drie gezet zijn, maar levert een half geconfigureerde provider op als er één ontbreekt. floriday-app, SupplyHub en growerportal2 doen het goed met één entraEnabled-constante over alle drie.

Rollen komen nooit uit Entra. De provider levert identiteit, meer niet. Rol, label en rechten lees je uit je eigen database — in de jwt-callback, na de lookup op e-mailadres. Zet in de profile() van de provider lege waarden neer en vul ze daar.

Wil je dat intrekken direct werkt, lees de rollen dan bij elk request vers. De Onboarding Portal doet een DB-lookup in elke jwt-callback en geeft een leeg token terug zodra isActive uit staat. Dat kost een query per request; de prijs voor deactivering die niet wacht op het verlopen van een JWT. SupplyHub kiest een tussenweg en leest hooguit elke vijf minuten opnieuw; dat is een verdedigbare afweging, zolang je hem opschrijft. Wat níet kan, is helemaal niet opnieuw lezen — dan werkt uitzetten pas bij de volgende login.

Een leeg token is geen uitgelogde gebruiker, tenzij je dat zelf regelt. Dit is de valkuil die de vorige regel ondermijnt. Een jwt-callback die {} teruggeeft, levert nog steeds een token op dat decodeert tot een object met iat, exp en jti — truthy dus. Twee plekken moeten daar expliciet op testen:

Zonder die twee regels houdt een uitgezette gebruiker een sessie, alleen zonder identiteit — en wat de routes daarmee doen, hangt per route af. SupplyHub doet beide (src/middleware.ts, de session-callback in src/lib/auth.ts).

Secrets nooit in git en nooit in projectmemory. Het secret hoort alleen in Vercel en in je lokale .env.

Client-id en tenant-id mogen wél in documentatie. Ze zijn openbaar van opzet: allebei staan ze in de URL van elke inlogredirect, dus elke gebruiker die ooit heeft ingelogd heeft ze in zijn adresbalk gehad, en de tenant-id is bovendien opvraagbaar via het discovery-endpoint. De client-id zégt wie de app is, het secret bewíjst het — en dat een client-id publiek mag zijn, blijkt uit public clients (SPA's, mobiele apps), die helemaal geen secret hebben en alleen op redirect URI plus PKCE draaien.

Dat maakt de lijst met redirect URI's uit §2 een beveiligingsmaatregel en geen configuratiedetail: hij is de reden dat iemand met jouw client-id een loginflow kan starten maar de code nooit op zijn eigen domein ziet landen.

Zet de vervaldatum van het secret in je agenda. Bijlage A vraagt 24 maanden aan. Verloopt hij, dan valt de SSO om zonder dat er iets aan de app is veranderd.

5. Wat je nu overneemt

Er is bewust geen @col/-item voor de aansluiting zelf. Eén draaiende implementatie is geen patroon, en de apps zitten op verschillende NextAuth-majors. Een gedeeld config-object zou die verschillen moeten verstoppen achter opties, en dat is meer bagage dan het bespaart.

Kopieer daarom uit SupplyHub (voorraadbeheer), de meest complete implementatie van §3:

Uit de Onboarding Portal neem je de activatiepagina met beide routes erop en de terugval van wachtwoord-vergeten naar de activatiemail (§3.3). Uit growerportal2 neem je het vangnet voor een ontbrekende backfill (§3.3), en AccountNotAllowed als je app externe gebruikers heeft (§3.4).

Neem het patroon over, niet de letterlijke code: SupplyHub staat op NextAuth v4 en een nieuwe app hoort op v5 te beginnen. growerportal2 laat zien hoe hetzelfde er op v5 uitziet (src/lib/entra-sign-in.ts). Wat je overneemt is de volgorde van controles in de signIn-callback — adres bepalen, account opzoeken, weigeren als het uitgezet is, pas daarna eventueel activeren — het feit dat rollen uit je eigen database komen, en de lijst schrijvers in §3.3.

Zodra een tweede app écht live is, wordt dit alsnog een item — dan valt er te extraheren uit twee bewezen implementaties in plaats van uit één plus een vermoeden. De vorm ligt voor de hand, want drie apps zijn er al onafhankelijk op uitgekomen: een pure beslissingsfunctie met een getypeerde uitkomst en vaste reason-codes, waar de app zijn eigen lookup in schuift. De beslissingsfunctie is het makkelijke deel. De schrijvers uit §3.3 zitten in de routes van elke app zelf, en die kan een item niet meeleveren — die lijst blijft een checklist.

6. Openstaand


Bijlage A — mail: App Registration aanvragen

Onderwerp: App Registration voor <naam van de app>

Hoi,

Voor <naam van de app> willen we inloggen met het Microsoft-werkaccount, zodat collega's geen apart wachtwoord meer nodig hebben. Daarvoor is een App Registration nodig in onze tenant.

Gevraagd:

  • Naam: <naam van de app>
  • Type: single tenant (accounts alleen in deze organisatie)
  • Platform: Web
  • Redirect URI's: <één regel per domein per omgeving, zie §2>
  • API permissions (Microsoft Graph, delegated): openid, profile, email, User.Read
  • Client secret met een looptijd van 24 maanden

Graag de Application (client) ID, de Directory (tenant) ID en het secret terugkoppelen. Het secret ontvang ik het liefst via een kanaal waar het niet blijft staan.

De app vraagt alleen het profiel op van de gebruiker die inlogt (naam en e-mailadres), om te bepalen wie er binnenkomt. Geen toegang tot mail, agenda, bestanden of gegevens van andere gebruikers.

Groet,

Bijlage B — mail: admin consent aanvragen

Onderwerp: Admin consent nodig voor App Registration <naam van de app>

Hoi,

Bedankt voor de App Registration. De koppeling werkt technisch: client ID, secret en tenant ID kloppen en de redirect URI's staan goed geregistreerd. Bij het inloggen krijg ik alleen nog "Need admin approval" — gebruikers mogen in onze tenant zelf geen toestemming geven aan apps, dus de app moet eenmalig door een Global Admin worden goedgekeurd.

App: <naam van de app> Application (client) ID: <client-id> Tenant ID: 29ebd335-b1bc-4b1d-b89b-ea6e27378762

Wat er nodig is (Azure portal):

  1. Entra ID > App registrations > <naam van de app> > API permissions
  2. Controleer dat onder Microsoft Graph (Delegated) staan: openid, profile, email, User.Read
  3. Klik op "Grant admin consent for <tenant>"

Alternatief in plaats van stap 1-3, deze URL openen als Global Admin: https://login.microsoftonline.com/29ebd335-b1bc-4b1d-b89b-ea6e27378762/adminconsent?client_id=<client-id>

Ter geruststelling: de app vraagt alleen het eigen profiel van de ingelogde gebruiker op (naam + e-mailadres) om te bepalen wie er inlogt. Geen toegang tot mail, agenda, bestanden of gegevens van andere gebruikers.

Groet,