A demonstration project showing Single Sign-On (SSO) implementation using Spring Boot applications and Keycloak as the identity provider.
This project contains:
- Keycloak: Identity and access management server (port 8083)
- App-A: Spring Boot application (port 8081)
- App-B: Spring Boot application (port 8082)
Both applications use OAuth2/OpenID Connect to authenticate users via Keycloak SSO.
┌─────────────┐
│ Keycloak │
│ (port 8083)│
└──────┬──────┘
│
│ OAuth2/OIDC
│
────┴────
│ │
┌──▼───┐ ┌─▼────┐
│App-A │ │App-B │
│ 8081 │ │ 8082 │
└──────┘ └──────┘
- Java 17 or higher
- Maven 3.6+
- Docker and Docker Compose
- Spring Boot 3.x
- Spring Security
- Spring OAuth2 Client
- Thymeleaf (for templates)
- Keycloak 24.0
docker-compose up -dKeycloak will be available at: http://localhost:8083
- Admin username:
admin - Admin password:
admin
- Access Keycloak Admin Console at
http://localhost:8083 - Login with admin credentials
- Click on the dropdown in the top-left (says "master")
- Click "Create Realm"
- Set Realm name:
sso-demo - Click "Create"
- In the
sso-demorealm, go to Clients → Create client - General Settings:
- Client type:
OpenID Connect - Client ID:
app-a-client - Click Next
- Client type:
- Capability config:
- Client authentication:
ON - Authorization:
OFF - Authentication flow: Check
Standard flowandDirect access grants - Click Next
- Client authentication:
- Login settings:
- Valid redirect URIs:
http://localhost:8081/* - Valid post logout redirect URIs:
http://localhost:8081/* - Web origins:
http://localhost:8081 - Click Save
- Valid redirect URIs:
- Go to Credentials tab
- Copy the Client secret and update it in
app-a/src/main/resources/application.properties
Repeat the same steps as App-A but with:
- Client ID:
app-b-client - Valid redirect URIs:
http://localhost:8082/* - Valid post logout redirect URIs:
http://localhost:8082/* - Web origins:
http://localhost:8082 - Update the client secret in
app-b/src/main/resources/application.properties
- Go to Users → Add user
- Set Username:
testuser - Click Create
- Go to Credentials tab
- Click Set password
- Set password (e.g.,
password) - Turn off Temporary
- Click Save
Update the client secrets in the application properties files:
app-a/src/main/resources/application.properties:
spring.security.oauth2.client.registration.keycloak.client-secret=YOUR_APP_A_CLIENT_SECRETapp-b/src/main/resources/application.properties:
spring.security.oauth2.client.registration.keycloak.client-secret=YOUR_APP_B_CLIENT_SECRET# Build the project
mvn clean install
# Run App-A (in one terminal)
cd app-a
mvn spring-boot:run
# Run App-B (in another terminal)
cd app-b
mvn spring-boot:run# Build and run with docker-compose
docker-compose up --build- Open browser and go to
http://localhost:8081 - You'll be redirected to Keycloak login page
- Login with
testuser/password - You'll be redirected back to App-A and see your profile
- Open another tab and go to
http://localhost:8082 - You should be automatically logged in (SSO!) without entering credentials again
- You'll see the same user profile in App-B
App-A (http://localhost:8081)
/- Profile page (protected, requires authentication)/profile- User profile information/public/**- Public resources (no authentication required)/logout- Logout endpoint
App-B (http://localhost:8082)
/- Profile page (protected, requires authentication)/profile- User profile information/public/**- Public resources (no authentication required)/logout- Logout endpoint
- User clicks logout link or navigates to
/logout - Application executes:
CustomLogoutHandler- Clears custom session data and JSESSIONIDSecurityContextLogoutHandler- Clears Spring Security context
- Session cookie (JSESSIONID) is invalidated
- User needs to login again to access protected resources
- After logout, the session is completely cleared
- Accessing any protected page will redirect to Keycloak login
- Both applications maintain independent sessions with Keycloak
- The logout is local to the application - it doesn't trigger Keycloak logout
- For global SSO logout (logout from all apps), you would need to implement OIDC RP-Initiated Logout
- Back-channel logout is disabled:
.backChannelLogoutEnabled(false)
Both applications use the following OAuth2 settings:
# Client registration
spring.security.oauth2.client.registration.keycloak.client-id=<CLIENT_ID>
spring.security.oauth2.client.registration.keycloak.client-secret=<CLIENT_SECRET>
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
# Provider configuration
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8083/realms/sso-demo
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_usernameThe security configuration:
- Allows public access to
/public/**paths - Requires authentication for all other endpoints
- Uses OAuth2 login with Keycloak
- Implements custom logout with session clearing
Cause: Application cannot reach Keycloak at the issuer URI
Solution:
- Ensure Keycloak is running:
docker-compose ps - Check Keycloak is accessible:
curl http://localhost:8083/realms/sso-demo/.well-known/openid-configuration - If using Docker for apps, use internal network name instead of
localhost
Cause: No default page configured after logout
Solution:
- The logout clears the session correctly
- Navigate to
/to trigger login again - Or add a logout success URL in SecurityConfig:
.logout(logout -> logout .logoutSuccessUrl("/") // ... )
Cause: Session cookie settings or logout handlers not configured properly
Solution:
- Verify
CustomLogoutHandlerinvalidates the session - Verify
SecurityContextLogoutHandleris added - Check browser dev tools to confirm cookie is removed after logout
Cause: The redirect URI is not whitelisted in Keycloak client configuration
Solution:
- Go to Keycloak Admin Console
- Navigate to your client (app-a-client or app-b-client)
- Add the redirect URI to Valid redirect URIs and Valid post logout redirect URIs
- Make sure to include
/*at the end (e.g.,http://localhost:8081/*)
Cause: Missing OAuth2 configuration or security misconfiguration
Solution:
- Check application.properties has correct OAuth2 settings
- Verify Keycloak realm and clients are created
- Check logs for errors:
logging.level.org.springframework.security=DEBUG - Ensure
.oauth2Login(Customizer.withDefaults())is in SecurityConfig
Cause: Controller or template issues
Solution:
- Verify
ProfileControllerexists and is mapped correctly - Check
profile.htmltemplate exists insrc/main/resources/templates/ - Verify Thymeleaf dependency is in pom.xml
java-sso-keykloak/
├── app-a/
│ ├── src/main/java/com/example/app1/
│ │ ├── App1Application.java # Main application class
│ │ ├── SecurityConfig.java # Spring Security configuration
│ │ ├── CustomLogoutHandler.java # Custom logout handler
│ │ └── ProfileController.java # Profile page controller
│ ├── src/main/resources/
│ │ ├── application.properties # App-A configuration
│ │ └── templates/profile.html # Profile page template
│ ├── Dockerfile # Docker configuration for App-A
│ └── pom.xml # Maven dependencies for App-A
├── app-b/
│ ├── src/main/java/com/example/app2/
│ │ ├── App2Application.java # Main application class
│ │ ├── SecurityConfig.java # Spring Security configuration
│ │ ├── CustomLogoutHandler.java # Custom logout handler
│ │ └── ProfileController.java # Profile page controller
│ ├── src/main/resources/
│ │ ├── application.properties # App-B configuration
│ │ └── templates/profile.html # Profile page template
│ ├── Dockerfile # Docker configuration for App-B
│ └── pom.xml # Maven dependencies for App-B
├── docker-compose.yml # Docker Compose for Keycloak
├── pom.xml # Parent POM
└── README.md # This file
Keycloak data is persisted in a Docker volume:
volumes:
keycloak_data:
driver: localThis ensures your realm, clients, and users are preserved across container restarts.
- Change default passwords: Don't use
admin/adminfor Keycloak - Use HTTPS: Configure SSL/TLS for all services
- Secure client secrets: Use environment variables or secret management
- Configure CORS properly: Restrict web origins to trusted domains
- Enable CSRF protection: Ensure CSRF tokens are validated
- Use secure session cookies: Configure
SecureandHttpOnlyflags - Implement proper logout: Consider RP-Initiated Logout for global logout
This is a demonstration project for educational purposes.