Skip to content

AntoniRomera/cross-stack-sso

Repository files navigation

Cross-Stack SSO — Keycloak · ASP.NET Core · Spring Boot

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.

.NET Java Keycloak Docker License

Screenshots

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
Keycloak login .NET app home Java claims view

Why

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.

What's inside

  • 🔐 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.

Architecture

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
Loading

A deeper write-up of the SSO/single-logout flow and the localhost-vs-keycloak issuer split is in docs/sso-flow.md.

Getting started

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 / demo

Log 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 Makefile wraps the common commands: make up, make down, make logs, make test.

Tech stack

.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).

Configuration

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.

Testing & CI

Both apps ship with meaningful, runnable tests:

  • .NET — xUnit (dotnet-app/tests/DotnetApp.Tests)
    • ProfileResponse contract parsing of the JSON the Java API returns.
    • A WebApplicationFactory integration test that boots the real app and asserts /healthz is 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 → Spring ROLE_*.
    • SecurityConfigTest — a full-context test asserting /actuator/health is public, /api/profile is 401 without 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 only

GitHub 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).

Roadmap

  • SAML client example alongside OIDC
  • Refresh-token rotation demo
  • Screenshots / demo GIF

License

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 .env is gitignored — only .env.example (placeholders) is tracked.

About

Single Sign-On across .NET 8 and Java 21 (Spring Boot) via Keycloak OIDC — login once, access both

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors