Building an Enterprise IAM System with Keycloak, Django, Entra ID, Authentik, and eIDAS with a Live Interactive Simulator
Why this post exists.
Most developers build authentication exactly once, and spend years paying for it you know what I mean 🕶️.
The problem is not a lack of tools Keycloak, Django, Microsoft Entra ID, and the eIDAS regulation are all well-documented individually. The problem is that no one shows you how they fit together into a coherent system. How do tokens actually flow from a browser through an identity broker to your API? What does a real JWT look like when it carries eIDAS Level of Assurance claims? What happens to your Authentik(self hosted) fallback when Entra goes down?
I have been working in identity and biometrics systems for years, and I wanted to build something that answered those questions visually not just with architecture diagrams, but with something you can actually click through.
So I built an interactive IAM simulator. It covers the full stack: Keycloak as the identity broker, Django as the API layer, Microsoft Entra ID and Authentik as federated identity providers, and eIDAS for EU citizen identity at Level of Assurance High.
This post explains what each piece does, why the architecture is designed this way, and where the real complexity lives. The simulator at the bottom lets you explore every layer yourself.
The real problem with enterprise identity
The naive approach is to let every application handle its own login. It feels simpler at first one Django app, one users table, one set of passwords. Then you add a second service. Then a third. Then someone asks for SSO. Then the compliance team needs an audit trail of every login across all systems. Then your company acquires a team that runs on Azure AD. Then an EU public sector client needs you to accept citizen identity via eIDAS. Oh at some point you wanna design a multi-tenant identity system.
At that point, the naive approach collapses.
What you actually need is a centralized identity broker, a single system that issues tokens, federates with external identity providers, enforces authentication policies, and gives every application one thing to trust: a signed JWT from a known issuer.
Keycloak is that broker. It sits between your users (and their identity providers like Microsoft, Google, national eID schemes) and your applications. Your Django API never talks to Entra directly. It never talks to the eIDAS node directly. It trusts exactly one thing: a JWT signed with Keycloak's RSA private key, verifiable against a public JWKS endpoint.
This is not just cleaner architecture. It is the only architecture that survives the inevitable additions: new IdPs, new applications, MFA policies, LoA requirements, token exchange between microservices.
What each component does and why?
Keycloak is the hub. It manages realms (isolated namespaces for users, clients, and roles), runs authentication flows, and federates with external identity providers. Every application in your ecosystem registers as a Keycloak client and trusts only Keycloak's JWKS endpoint. You add a new IdP — say, a national eID scheme — once in Keycloak, and every downstream application benefits automatically.
Django with mozilla-django-oidc is the resource server. It validates JWTs from Keycloak, maps realm_access.roles to Django permission groups via a custom ClaimsMiddleware, and enforces eIDAS Level of Assurance on protected views with a @require_loa('high') decorator. The application code never sees raw SAML or OAuth exchanges — it just sees a verified user object with claims attached.
Microsoft Entra ID is federated into Keycloak as an OIDC identity provider. Corporate users who already have Microsoft 365 accounts log in via their Entra credentials, but the token Django receives still comes from Keycloak. One issuer, one signing key, one trust anchor — regardless of where the user actually authenticated.
Authentik is the self-hosted fallback. It runs as a parallel identity provider with identical claim mappings, synced via SCIM. If Entra is unreachable — air-gapped deployment, network partition, VPN failure — Authentik takes over transparently. This is particularly relevant for EU public sector deployments where cloud dependency must be minimized.
The eIDAS broker is the gateway to notified national identity providers: BundID in Germany, FranceConnect+ in France, Cl@ve in Spain, BankID in Sweden. When a user needs to authenticate at Level of Assurance High...a legal requirement for certain public services.. the broker handles the SAML2 exchange with the national IdP and returns a verified identity assertion that Keycloak maps into your OIDC token.
Nginx sits at the edge, terminating TLS, rate-limiting authentication endpoints, and routing /auth/* to Keycloak and /api/* to Django. Clients never touch backend services directly.
Rather than explaining each flow in prose, I built an interactive simulator. You can explore the architecture diagram, run any of the five auth flows step by step, inspect real JWT payloads (including eIDAS attribute claims), watch a live traffic monitor, and read the verified facts and disclaimers tab ...which is honest about what this tool simplifies versus what production reality looks like.
View the full experience → https://kimmonzon-core.vercel.app/embeds/iam-interactive.html
Keycloak: the decisions that matter
The first decision is token lifetime. Keycloak defaults are fine for development but wrong for production. Access tokens should live five minutes... not five hours. Refresh tokens should rotate on every use (revokeRefreshToken: true, refreshTokenMaxReuse: 0). A stolen refresh token used once immediately invalidates itself, and you can detect the duplicate use attempt.
The second decision is algorithm. Always RS256 — asymmetric. The private key never leaves Keycloak. Your applications verify signatures using the public key from the JWKS endpoint. Symmetric HS256 means every application that verifies tokens also has the ability to forge them. That is not acceptable.
The third decision is what you put in the realm versus what you put in the client. Realm-level roles (realm_access.roles) are for coarse-grained access control shared across applications. Client-specific roles (resource_access.django-app.roles) are for per-application permissions. Use both deliberately — do not dump everything into realm roles because it is easier.
The fourth decision is how you handle the JWKS cache. Your application should cache the public keys (five-minute TTL is reasonable) and implement retry-with-refresh on signature validation failure. Keycloak rotates keys automatically — there is a brief window where tokens signed with the old key are still valid but the JWKS endpoint serves only the new key. Cache too aggressively and you reject valid tokens. Do not cache at all and you add a network round-trip to every request.
The simulator's Architecture tab shows how these components fit together. The Token Lab tab lets you generate and inspect tokens for each sample user.
Django: claims, roles, and the middleware chain
mozilla-django-oidc handles the OAuth dance — redirects, code exchange, token storage. What it does not handle is your business logic around claims, and that is where most implementations go wrong.
The first mistake is filtering users by email. Email addresses change. Keycloak's sub claim is a stable UUID tied to that user's record in the realm. Filter your user lookup by profile__keycloak_sub = claims['sub'] — not by email. This is the bug that creates duplicate accounts when someone gets married, changes their name, or switches corporate email domains.
The second mistake is validating tokens by calling the userinfo endpoint on every request. That is a synchronous network call that serializes your entire request pipeline. Validate the JWT locally against the cached JWKS public key. Call userinfo only when you need genuinely fresh claims — which is rarely.
The third mistake is mapping roles directly to Django's is_staff or is_superuser. Map roles to Django permission groups instead. Groups are composable, auditable, and do not require code changes when your role taxonomy evolves.
The ClaimsMiddleware pattern shown in the simulator maps realm_access.roles to Django groups on every authenticated request, stores the eIDAS acr claim on the user profile, and makes both available to view decorators. The Auth Flows tab in the simulator shows exactly where this middleware fires in the request lifecycle.
Entra ID vs Authentik — when to use each
The question is not which one to use. If you have Microsoft 365 users, Entra is already your authoritative directory — use it. The question is what happens when Entra is not available.
Authentik runs entirely on your own infrastructure. No cloud dependency, no per-seat pricing, no Microsoft API rate limits. It supports the same OIDC and SAML2 protocols as Entra, syncs users via SCIM, and — critically — can be configured with identical claim mappings so Keycloak treats both sources the same way.
The pattern that works: Entra as primary, Authentik as hot standby. Keycloak has an identity provider priority order. When Entra returns a 503 or your VPN is down, Keycloak falls over to Authentik automatically. Your Django application never knows the difference because it only ever talks to Keycloak.
What Authentik does not replace: Entra's Conditional Access risk engine, Defender for Identity threat signals, and Privileged Identity Management. For organizations with strict Conditional Access policies — block legacy auth, require compliant device, enforce MFA for all admin roles — Entra is not optional. Authentik covers the identity protocol layer, not the security policy enforcement layer.
The Users & Claims tab in the simulator shows sample users authenticated via different IdPs and what their JWT claims look like in each case.
eIDAS: what Level of Assurance actually means
eIDAS is not a product you install. It is a legal framework — Regulation (EU) No 910/2014 — that defines how EU member states must operate electronic identification schemes and how those schemes can be used cross-border.
Level of Assurance High does not just mean strong authentication at login time. It means the identity was provisioned in person, by a government authority, with physical document verification. A chip-based national ID card. A passport. This cannot be achieved purely digitally. If your onboarding flow is entirely online, you can reach LoA Substantial at most.
The technical implementation runs through the CEF eIDAS-Node — a Java/Tomcat reference implementation published by the European Commission (EUPL 1.2 license). In practice, connecting to the live eIDAS network requires your organization to register as a service provider with the relevant national supervisory body, pass a conformity assessment, and complete a procurement or notification process. The simulator models the technical protocol accurately. The governance layer — which takes months — is out of scope.
What you get in the token when eIDAS succeeds: a PersonIdentifier in the format ORIGIN/DESTINATION/OPAQUE_ID (e.g., ES/DE/12345678A), the user's legal name and date of birth, and an acr claim set to http://eidas.europa.eu/LoA/high. The PersonIdentifier is pseudonymous by design — stable across sessions at the same service provider, but constructed to prevent cross-site correlation.
The eIDAS tab in the simulator lets you configure LoA, select a member state, and watch the full 10-step authentication flow animate in real time.
Reading a JWT: what every claim means
A JWT is three base64url-encoded JSON objects separated by dots. The header names the algorithm and key ID. The payload carries the claims. The signature proves the token was issued by the key owner — in this case, Keycloak.
The claims that matter most in this stack:
iss — the issuer. Should always be your Keycloak realm URL. If it is anything else, reject the token.
sub — the subject. A stable, opaque user identifier. This is your primary key for user lookup, not email.
aud — the audience. Your application's client ID must appear here. If it does not, the token was not issued for your application — reject it.
acr — the Authentication Context Class Reference. For eIDAS, this is a URI like http://eidas.europa.eu/LoA/high. For standard MFA, it follows your Keycloak flow configuration.
realm_access.roles — the Keycloak realm roles. This is what your ClaimsMiddleware reads to populate Django groups.
exp — expiry. Five minutes from issue. Your application must check this. Libraries do it automatically — do not disable that check.
The Token Lab tab lets you switch between Access Token, ID Token, and Refresh Token for any sample user and see exactly how the payload changes.
What this architecture does not cover
This stack is a solid foundation for most teams. It is not the only valid architecture, and there are real gaps depending on your context.
Scale. Docker Compose with a single Keycloak instance works for development and small deployments. Production at scale needs Keycloak in HA mode with Infinispan clustering, a connection-pooled PostgreSQL setup, and ideally the Keycloak Operator running on Kubernetes. None of that changes the concepts — only the operations.
Alternative stacks. If your team is Java-native, Spring Boot with spring-boot-starter-oauth2-resource-server is a cleaner fit than Django. If you are AWS-first, Cognito can replace Keycloak for simpler use cases — though its OIDC compliance has quirks and federation options are more limited. If you need zero operational overhead, Okta or Auth0 provide the same flows as a managed service at significantly higher cost per seat.
eIDAS in production. The simulator shows the technical protocol accurately. Connecting to the live eIDAS network requires organizational registration, conformity assessment, and in many cases a public procurement process. Plan months, not days.
Token security. The JWTs in the Token Lab are illustrative — the signatures are descriptive text, not actual RS256 signatures. Do not copy payloads verbatim. Always validate against your live Keycloak realm's JWKS endpoint.
The Facts & Refs tab inside the simulator goes into more detail on each of these points, with sources.
What comes next
The next post in this series replaces Django with Quarkus and the Kubernetes-native Java stack built on top of the same identity primitives.
The quarkus-oidc extension handles JWT validation with zero boilerplate. Panache ORM adds role-based data filtering at the query level. GraalVM native compilation brings the whole service down to a sub-50MB binary with sub-10ms cold starts — which matters for EU public sector cloud deployments where container density and startup time are procurement criteria.
After that: Keycloak SPI (Service Provider Interface) on how to write your own authenticators, event listeners, and protocol mappers in Java when the admin UI is not enough.
If you want to follow along, I publish it here. The whole setup will be publish soon in github.
Identity infrastructure is one of those problems that looks simple from the outside and reveals its real complexity the moment you try to make it production-ready, auditable, and federated across organizational boundaries. I hope this post and the simulator give you a cleaner mental model of how the pieces fit together.
If you are implementing any part of this stack ...or if something in the simulator is wrong or misleading, leave a comment or reach out directly at kimmonzon.com. The Facts & Refs tab inside the simulator already flags the major simplifications, but real systems always surface new edge cases.