Appearance
ADR-004: Keycloak as Identity Provider over Azure AD B2C
Status
Accepted
Date
2023-Q3
Context
Microtec ERP requires an Identity Provider (IdP) for:
- Single Sign-On (SSO) across 10+ Angular apps and 3 mobile apps
- Multi-tenant user management (each tenant has its own users)
- Custom authentication flows (company/branch selection post-login)
- On-premise deployment capability (some enterprise customers prefer on-prem)
- Role-based and attribute-based access control
Candidates evaluated:
| IdP | Type | Key Properties |
|---|---|---|
| Azure AD B2C | Azure PaaS | Per-MAU pricing, Microsoft-managed, limited extensibility |
| Keycloak | Open-source | Self-hosted, Java SPI extensibility, zero per-MAU cost |
| Auth0 | SaaS | Developer-friendly, per-MAU pricing, limited on-prem |
| IdentityServer | .NET library | Full .NET control, requires building admin UI |
| Azure AD (Entra ID) | Azure PaaS | Primarily for enterprise M365 users, complex for B2C |
Decision
Use Keycloak 24.x as the Identity Provider, hosted on Azure Container Apps.
Reasons for Keycloak over Azure AD B2C
| Criterion | Azure AD B2C | Keycloak (chosen) |
|---|---|---|
| Cost model | Per-MAU ($0.0016–$0.0325/user/month) | Self-hosted, no per-user cost |
| Custom SPI | Limited (custom policies, complex) | Full Java SPI — any logic possible |
| Multi-realm | Single tenant with complex policies | Native multi-realm support |
| On-premise | Not possible (Azure-only) | Supports on-premise deployment |
| Admin API | Limited | Full Admin REST API |
| Community | Microsoft | Large open-source community |
| Extensibility | HTML/JavaScript customization | Java extensions, custom authenticators |
| Vendor lock-in | High (Azure-only) | Low (runs anywhere) |
Why Custom SPIs Were Required
The ERP's authentication requirements could not be met by stock Keycloak:
Company/Branch Selection: After authentication, users must select which company and branch they're operating in. This state must be embedded in the JWT. Stock Keycloak has no concept of "company" or "branch."
ERP Permission Mapping: JWT claims must include ERP module permissions (
erp_policies). These are stored in the ERP database, not in Keycloak. A custom SPI is required to call the ERP API during token issuance.Multi-Account Switcher: Users belonging to multiple companies must switch company/branch without re-authenticating. This requires a custom session management SPI.
Tenant Seeding: When a new tenant is provisioned, their Keycloak realm must be created programmatically with the correct configuration. Azure AD B2C has no equivalent automated provisioning API.
Custom SPIs Developed
| SPI | Language | Purpose |
|---|---|---|
company-branch-mapper | Java | Maps company/branch to JWT claims |
erp-policy-mapper | Java | Maps ERP permissions to JWT claims |
multi-account-switcher | Java | Company switch without re-login |
erp-tenant-mapper | Java | Maps tenant/subdomain to JWT claims |
seeding-authenticator | Java | Automated realm/user provisioning |
All SPIs are in KeycloakProviders/ and deployed as a custom Keycloak Docker image.
Realm Architecture
Two realms to separate ERP users from admin users:
| Realm | Users | Applications |
|---|---|---|
microtec | Tenant employees | All ERP apps + mobile |
businessowner | Tenant admins, resellers | BO portal |
Consequences
Positive
- Zero per-MAU cost: No incremental cost as customer base grows
- Full SPI extensibility: Any authentication/authorization logic implementable in Java
- Multi-realm isolation: ERP users and BO admin users are completely isolated
- On-premise capability: Enterprise customers can request on-prem deployment
- Open standard: OIDC/OAuth 2.0 — frontend code is not Keycloak-specific
- Rich admin API: Full programmatic control for tenant provisioning automation
- Backchannel logout: Keycloak-native session revocation across all clients
Negative
- Self-managed: We own updates, security patches, and high availability
- Java SPI complexity: Custom SPIs require Java knowledge; .NET team must maintain Java code
- Higher operational overhead than B2C: ACA deployment, PostgreSQL/SQL for Keycloak state, monitoring
- SPI HTTP dependency:
erp-policy-mappercalls ERP API synchronously during login — if the ERP service is down, logins fail - Version upgrades: Major Keycloak versions can break SPIs; upgrades require SPI re-testing
Neutral
- Keycloak is deployed on ACA with Azure SQL as its storage backend
- The custom Docker image (
keycloak-with-spis) is built by theKeycloakProviders/pipeline - Flow priorities set to: MAC=10, Cookie=20, IdP=30 (required for correct auth flow ordering)
Deployment Notes
Keycloak on ACA
Container: keycloak-with-spis:latest
Image: Built from KeycloakProviders/Dockerfile
Base: quay.io/keycloak/keycloak:24.x
SPIs: Compiled JARs copied to /opt/keycloak/providers/
Database: Azure SQL (JDBC driver included)
Memory: 1.0 vCPU, 2Gi RAM minimumKnown Configuration
KC_DB=mssql
KC_DB_URL=jdbc:sqlserver://<host>;databaseName=Microtec.SSO
KC_DB_USERNAME=sa
KC_HOSTNAME=auth.<env-domain>
KC_PROXY_HEADERS=xforwarded
KC_HOSTNAME_STRICT=false
KC_FEATURES=persistent-user-sessions,token-exchange,admin-fine-grained-authz:v1
KEYCLOAK_ADMIN=admin
KEYCLOAK_ADMIN_PASSWORD=<from KV>Related ADRs
- ADR-001: Microservices (Keycloak serves all services via standard OIDC)
- ADR-002: Azure Container Apps (Keycloak hosted on ACA)
- ADR-008: Dual-Token Design (Keycloak JWT is one of the two tokens)