One identity, two stacks. A working Single Sign-On demo where a .NET 8 app and a Java 21 / Spring Boot app authenticate against the same Keycloak (OIDC) identity provider — log in once, access both; log out once, you're out of both.
Captured from the running docker compose stack — log in once at Keycloak, then the Spring Boot app shows your OIDC claims without a second login (true SSO):
| 1 · Keycloak login (OIDC IdP) | 2 · ASP.NET Core 8 app | 3 · Java claims — signed in via SSO |
|---|---|---|
![]() |
![]() |
![]() |
SSO across heterogeneous stacks is a real enterprise need and a common interview topic. This repo shows it end-to-end with idiomatic code on both platforms — not pseudo-code — and runs with a single docker compose up.
- 🔐 Keycloak as the OIDC Identity Provider — a pre-seeded realm (
keycloak/realm-export.json) with two clients and demo users, imported on startup. - 🟣 ASP.NET Core 8 web app — OIDC login (
Microsoft.AspNetCore.Authentication.OpenIdConnect), protected pages, claims view, and a button that calls the Java API with the access token. - ☕ Spring Boot 3 / Java 21 app — Spring Security as an OAuth2 resource server exposing
/api/profile, plus an OIDC-login web view; validates the same Keycloak-issued JWTs. - 🔁 True SSO — single login at Keycloak grants access to both apps; single logout ends both sessions. Bearer token propagated .NET → Java to prove cross-stack trust.
- 🐳 Docker Compose — Keycloak + Postgres + both apps, one command.
flowchart LR
U[Browser] -->|OIDC login| KC[(Keycloak IdP)]
U --> NET[ASP.NET Core 8 app]
U --> JV[Spring Boot 3 app]
NET -->|OIDC code flow| KC
JV -->|OIDC code flow / JWT validation| KC
NET -->|Bearer token| API[Java OAuth2 Resource Server /api/profile]
JV --- API
A deeper write-up of the SSO/single-logout flow and the localhost-vs-keycloak
issuer split is in docs/sso-flow.md.
git clone https://github.com/AntoniRomera/cross-stack-sso.git
cd cross-stack-sso
# Create your local env file from the template (placeholders are local-demo only)
cp .env.example .env
docker compose up -d --build
# Keycloak admin: http://localhost:8080 (admin/admin)
# .NET app: http://localhost:5000
# Java app: http://localhost:8081
# Demo user: demo / demoLog into the .NET app, then open the Java app in the same browser — you're already authenticated. Hit "logout" on either; both sessions end.
A
Makefilewraps the common commands:make up,make down,make logs,make test.
.NET 8 (ASP.NET Core, OpenIdConnect middleware) · Java 21 (Spring Boot 3, Spring Security OAuth2) · Keycloak (OIDC) · PostgreSQL · Docker Compose · GitHub Actions CI (xUnit + JUnit 5).
All configuration is supplied via environment variables. Copy .env.example
to .env and adjust if needed; the committed example contains only
clearly-marked local-demo placeholders.
| Variable | Description | Demo default |
|---|---|---|
KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD |
Keycloak bootstrap admin | admin / admin |
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD |
Keycloak persistence store | keycloak |
KEYCLOAK_REALM |
Realm name | portfolio |
KEYCLOAK_ISSUER |
Browser-facing OIDC issuer (the validated iss) |
http://localhost:8080/realms/portfolio |
KEYCLOAK_INTERNAL_BASE |
Container-facing base for back-channel calls | http://keycloak:8080 |
DOTNET_CLIENT_ID / DOTNET_CLIENT_SECRET |
.NET app OIDC client | dotnet-app / (demo secret) |
JAVA_CLIENT_ID / JAVA_CLIENT_SECRET |
Java app OIDC client | java-app / (demo secret) |
The split between KEYCLOAK_ISSUER (front-channel, what tokens are signed with)
and KEYCLOAK_INTERNAL_BASE (back-channel discovery / token / JWKS inside the
compose network) is the central correctness detail — see docs/sso-flow.md.
Both apps ship with meaningful, runnable tests:
- .NET — xUnit (
dotnet-app/tests/DotnetApp.Tests)ProfileResponsecontract parsing of the JSON the Java API returns.- A
WebApplicationFactoryintegration test that boots the real app and asserts/healthzis public and protected pages are not served to anonymous callers (the OIDC pipeline is genuinely enforced).
- Java — JUnit 5 (
java-app/src/test/java)KeycloakRealmRoleConverterTest— realm/client roles → SpringROLE_*.SecurityConfigTest— a full-context test asserting/actuator/healthis public,/api/profileis401without a token and returns the principal's claims for a valid JWT (exercised through the real security filter chains).
Run them locally without installing any toolchain (everything in Docker):
make test # both suites
make test-dotnet # xUnit only
make test-java # JUnit onlyGitHub Actions CI runs on every push / PR:
actions/setup-dotnet (8.0) builds and tests the .NET solution,
actions/setup-java (Temurin 21) builds and tests the Java app, and a third
job validates the Docker Compose file (docker compose config).
- SAML client example alongside OIDC
- Refresh-token rotation demo
- Screenshots / demo GIF
MIT © 2026 Antoni Romera Luis
⚠️ Demo credentials are for local use only. Never reuse them or commit real Keycloak admin secrets / client secrets. A real.envis gitignored — only.env.example(placeholders) is tracked.


