Skip to content

feat(identity): pluggable identity providers — Keycloak + OpenIddict#216

Merged
antosubash merged 5 commits into
mainfrom
feature/keycloak-identity
May 25, 2026
Merged

feat(identity): pluggable identity providers — Keycloak + OpenIddict#216
antosubash merged 5 commits into
mainfrom
feature/keycloak-identity

Conversation

@antosubash
Copy link
Copy Markdown
Owner

@antosubash antosubash commented May 25, 2026

Summary

  • Pluggable identity providers via Identity:Provider config key — supports both OpenIddict (default, self-hosted) and Keycloak (external IdP) in the same binary
  • New Identity.Contracts module with provider-agnostic ISessionContracts, IIdentityProvider, SessionDto, and RevokeSessionResult — all consuming modules depend on contracts, not a specific provider
  • New Keycloak module with OIDC auth code + PKCE flow, JWT Bearer validation, SmartAuth policy scheme, Admin REST API client with singleton token cache, claims transformation (realm_access → roles), and JIT shadow user provisioning
  • Users module dual-mode: local mode (UserManager via ASP.NET Identity) vs external mode (direct EF Core, read-only admin) — auto-selected by config
  • Aspire AppHost conditionally starts Keycloak 26.2 Docker container with pre-imported realm, test users, and configured clients via --launch-profile keycloak
  • Admin module refactored to depend on Identity.Contracts instead of OpenIddict.Contracts for session management
  • Role sync: Keycloak roles are JIT-synced to local ASP.NET Identity roles so permissions resolve correctly

Architecture

Identity.Contracts (ISessionContracts, IIdentityProvider)
    ├── OpenIddict module (implements when Identity:Provider != Keycloak)
    ├── Keycloak module  (implements when Identity:Provider == Keycloak)
    └── Admin module     (consumes contracts, provider-agnostic)

Users module
    ├── Local mode:    UserService → UserManager
    └── External mode: ExternalUserService → direct EF Core

How to switch providers

# OpenIddict (default)
dotnet run --project SimpleModule.AppHost

# Keycloak
dotnet run --project SimpleModule.AppHost --launch-profile keycloak

Screenshots — OpenIddict mode (default)

Landing Page

Landing page

Login Page

Login page

Authenticated Dashboard

Dashboard

Admin Hub

Admin hub

Admin Users

Admin users

User Sessions (ISessionContracts)

User sessions

Screenshots — Keycloak OIDC flow

1. Click "Log in" → Redirects to Keycloak

Keycloak login form

2. Enter Keycloak credentials

Keycloak credentials

3. OIDC callback → Authenticated Dashboard (Admin link visible)

Keycloak authenticated dashboard

4. Admin Hub (Keycloak mode — roles synced from Keycloak)

Keycloak admin hub

5. Admin Users (JIT-provisioned user visible)

Keycloak admin users

6. User dropdown

Keycloak user dropdown

Test plan

  • All 1,230 unit tests pass (0 failures)
  • All 172 e2e tests pass (OpenIddict mode)
  • Build succeeds with 0 warnings, 0 errors
  • Biome lint + format check passes
  • Docker image builds successfully
  • Browser smoke check: landing page, login, dashboard, admin users all render correctly
  • Keycloak OIDC flow verified end-to-end (redirect → Keycloak login → callback → authenticated dashboard)
  • Keycloak roles correctly mapped — Admin sidebar visible, permissions working
  • Reviewer runs dotnet run --project SimpleModule.AppHost --launch-profile keycloak and verifies Keycloak login flow
  • CI is green

Support both Keycloak and OpenIddict as identity providers via a single
`Identity:Provider` configuration key. Both modules coexist in the same
binary; only the configured one activates at startup.

- Add Identity.Contracts module with provider-agnostic ISessionContracts,
  IIdentityProvider, SessionDto, and RevokeSessionResult
- Add Keycloak module with OIDC auth code flow, JWT Bearer validation,
  SmartAuth policy scheme, Admin REST API client, and claims transformation
- Add JIT shadow user provisioning (Keycloak sub → local ApplicationUser)
- Add Users module dual-mode: local (UserManager) vs external (direct EF)
- Add Aspire AppHost Keycloak container with realm import and test users
- Refactor Admin module to depend on Identity.Contracts instead of OpenIddict
- Add OpenIddict stub adapter for Keycloak mode compatibility
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 25, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6455a8d
Status: ✅  Deploy successful!
Preview URL: https://8490fb80.simplemodule-website.pages.dev
Branch Preview URL: https://feature-keycloak-identity.simplemodule-website.pages.dev

View logs

The restore stage explicitly copies each module's .csproj for layer
caching. The new Identity.Contracts, Keycloak.Contracts, and Keycloak
projects were missing, causing dotnet restore to fail in CI.
Three issues prevented roles from working in Keycloak mode:

1. OIDC RoleClaimType was set to "roles" but role claims were added
   as ClaimTypes.Role — IsInRole() never matched. Fixed by setting
   RoleClaimType to ClaimTypes.Role.

2. realm_access roles were only parsed in ClaimsTransformation (runs
   per-request) but not at OIDC token validation time. Added
   KeycloakOidcEvents.OnTokenValidated to extract roles from the
   realm_access JWT claim and bake them into the cookie identity.

3. Keycloak roles had no local ApplicationRole records, so
   PermissionClaimsTransformation couldn't resolve permissions.
   Extended KeycloakUserSyncService to JIT-create local roles
   and assign them to shadow users.
@antosubash antosubash merged commit ffe49fe into main May 25, 2026
6 checks passed
@antosubash antosubash deleted the feature/keycloak-identity branch May 25, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant