End-to-End IAM: Från HR till Entra ID med Elytra och Ascendra
Bakgrund
end-to-end-iam är ett sidoprojekt där jag utforskar hur ett modernt IAM-landskap kan byggas. Projektet kretsar kring ett fiktivt företag, Legatus Corp, och består av två separata system.
- Elytra: ett HR-system som är primär källa för anställningsdata. Det betyder att om någon annan del av systemet visar en annan uppgift om en anställd, är det alltid Elytra som har rätt.
- Ascendra: en IGA-plattform (Identity Governance & Administration) som hanterar identiteter, roller, provisionering, SSO-anslutningar och granskningsloggar (audit).
Jag satte riktningen för projektet: att systemen skulle vara helt separata, att Convex skulle användas som plattform, och att IGA-systemet skulle kopplas mot en riktig Entra ID-tenant. Convex är en backend-plattform med databas, serverfunktioner och realtidsuppdateringar samlat på ett ställe, vilket gjorde den bra för två fristående system som ändå behöver reagera snabbt på händelser. Själva app-registreringen, behörigheterna och client secret (en hemlig nyckel som appen använder för att autentisera sig) i Entra ID satte jag upp själv. Implementationen i kod byggdes med AI-assisterad utveckling.
Det är samma problem som alla organisationer med fler än en handfull anställda behöver lösa. När någon börjar ska rätt konton skapas automatiskt. När någon byter roll ska behörigheter uppdateras. När någon slutar ska åtkomsten försvinna snabbt och säkert. Det är precis det Joiner-Mover-Leaver-flödet (JML) det här projektet modellerar.
Till en början var kopplingen mot identitetsleverantören helt simulerad. “Test connection” skrev bara en rad i granskningsloggen oavsett vad som faktiskt var konfigurerat. Ingen Entra ID, inga Graph-anrop och ingen riktig provisionering. Jag ville se hur lösningen fungerade mot en riktig katalog.
Det jag byggde
Arkitektur
Elytra och Ascendra är medvetet helt separata system.
- Separata Convex-deployments
- Ingen delad databas
- Ingen direkt kommunikation mellan systemen
Den enda kopplingen är en outbox som man bara lägger till i, aldrig ändrar eller tar bort ifrån (append-only). Elytra publicerar:
employee.createdemployee.updatedemployee.activatedemployee.terminated
Ascendra läser regelbundet av händelseloggen (pollar den) och kommer ihåg var den senast läste med hjälp av en cursor, en markör som pekar på den senaste händelsen den redan har hämtat. Nästa gång Ascendra läser loggen hämtar den bara det som tillkommit sedan dess, i stället för att läsa om allt från början. Utifrån de här händelserna bygger Ascendra upp sin egen identitetsmodell. Det här liknar hur många större organisationer separerar HR från sin IGA-plattform.
Provisionering till Entra ID
Det som saknades var nästa steg. Jag ville inte bara skapa identiteter inne i Ascendra utan faktiskt skapa, uppdatera och inaktivera riktiga användare i Entra ID.
Första tanken var SCIM. Det visade sig vara fel väg. När Entra ID är målkatalog finns ingen inkommande SCIM-endpoint att provisionera mot. SCIM används i stället av Entra när den själv provisionerar användare vidare till andra SaaS-tjänster.
Den korrekta lösningen blev därför Microsoft Graph. Microsoft Graph är Microsofts API för Entra ID och övriga Microsoft 365-tjänster: ett gränssnitt som andra program pratar med för att läsa och ändra data, till exempel för att skapa en användare eller inaktivera ett konto. Varje sådan begäran kallar jag ett Graph-anrop.
Ascendra pratar med Microsoft Graph via applikationsbehörigheter (application permissions) och Client Credentials-flödet, ett sätt för program att autentisera sig mot varandra utan att en människa loggar in. Appregistreringen i Entra ID använder:
- Single Tenant: appen får bara användas mot vår egen Entra ID-tenant, inte av andra organisationer.
- Ingen Redirect URI: behövs bara vid interaktiv inloggning, vilket inte är fallet här.
User.ReadWrite.All: behörighet att läsa och skriva alla användarkonton i tenanten.Organization.Read.All: behörighet att läsa organisationsinformation, till exempel verifierade domäner.- Applikationsbehörigheter (Application Permissions): behörigheter som tillhör appen själv, inte en inloggad person. De gäller alltid, oavsett vem (om ens någon) som kör koden.
- Admin Consent: en administratör måste godkänna behörigheterna innan appen får använda dem.
- Client Secret: en hemlig nyckel appen använder för att bevisa sin identitet mot Entra ID.
Det jag testade
- Verifiering mot en riktig Entra-tenant via Microsoft Graph
- Automatisk onboarding
- Uppdatering av användare
- Offboarding
- Retry av misslyckade provisioneringar (ett automatiskt nytt försök när ett Graph-anrop misslyckats, till exempel vid ett tillfälligt nätverksfel)
- Manuell Resync (en knapp som tvingar fram en ny synk av en identitet mot Entra ID, se förklaring under Utmaningar nedan)
- Periodisk reconciliation mellan HR och Entra (en regelbunden avstämning som jämför de två systemen och rättar till skillnader, till exempel om en händelse skulle ha tappats bort)
Resultat
Provisioneringen är nu verklig hela vägen. När en HR-händelse skapas i Elytra materialiseras den i Ascendra som sedan skapar, uppdaterar eller inaktiverar ett riktigt konto i Entra ID.
Utöver den händelsestyrda provisioneringen finns även:
- Reconciliation var 15:e minut
- Retry av misslyckade Graph-anrop
- SLA-varningar om onboarding eller offboarding tar för lång tid
Auth0 och den generiska SCIM-integrationen är fortfarande simulerade, eftersom de inte behövdes för just den här delen av projektet.
Utmaningar
Det intressanta började först när integrationen blev verklig.
Resync återaktiverade en uppsagd användare
Resync är en knapp i Ascendra som manuellt tvingar fram en ny synk mot Entra ID, till exempel om en tidigare synk misslyckats. Den läser inte något nytt från HR-systemet, utan tar det Ascendra redan vet om identiteten och skickar det till Entra igen via Graph.
Den allvarligaste buggen: koden bakom Resync skickade alltid accountEnabled = true till Graph, oavsett om personen fortfarande var anställd. Konkret hände det här: en anställd sades upp, kontot inaktiverades korrekt i Entra. Jag klickade Resync igen på samma person, bara för att testa, och kontot återaktiverades i Entra, trots att HR-systemet fortfarande visade personen som uppsagd.
Lösningen blev att alltid utgå från HR-systemets aktuella anställningsstatus när accountEnabled sätts, oavsett vilken funktion som gör Graph-anropet. Det var ett fel som hade kunnat ge en tidigare anställd åtkomst igen, bara genom en enda extra knapptryckning.
UPN och e-post är inte samma sak
Min tenant hade endast sin standarddomän verifierad. Lösningen blev att använda tenantens verifierade domän för userPrincipalName, och behålla användarens riktiga e-postadress i mail. Det är också hur många företag arbetar i praktiken.
Övriga problem
- Test Connection gav ingen feedback i gränssnittet
- SSO-sidan var tom eftersom tabellen aldrig fylldes med testdata
- Misslyckade Entra-synkar försvann ur livscykelvyn i stället för att visas som fel
Reflektion
Det här projektet lärde mig framför allt en sak. Så länge en integration är simulerad är det lätt att tro att allt fungerar. Först när den kopplas mot ett riktigt system dyker de verkliga problemen upp. Det var också då projektet började kännas som ett riktigt IAM-system i stället för en demo.
Teknisk anteckning
AI användes för att bygga stora delar av kodimplementationen. Mitt fokus låg på riktningen och IAM-koncepten bakom lösningen: att separera HR och IGA, koppla ett IGA-system mot en riktig Entra ID-tenant, sätta upp app-registrering, behörigheter och client secret i Entra ID, samt att testa flödet tillräckligt hårt för att hitta buggar som Resync-problemet ovan.

