Keycloak is an open source application for centralized identity and access management. Numerous authentication methods are provided and can be customized to your own preferences. How can we connect our Spring Boot application with Thymeleaf frontend to Keycloak?
The setup of Keycloak in the current version 21.1.1
is explained in this separate article. After this, our Keycloak server is already running on port 8085
, contains a realm and client, and is ready to be connected to our Spring Boot application. OAuth / OIDC is naturally our first choice - the user is redirected to the external login page and comes back to our web application after successful authentication.
Previously, there was a separate Spring Boot adapter for Keycloak, but this is deprecated. Instead, we can directly use the board tools for OAuth from Spring Security. With the library spring-boot-starter-oauth2-client
we can set up our application as an OAuth / OIDC client of Keycloak. This approach could be applied to other identity managers like Okta or OneLogin as well.
Preparation of our app
We quickly create a simple Spring Boot application with Thymeleaf in the Bootify Builder. Open your project with one click and pick the preferred frontend. Additionally we create an entity User
with two String fields externalId
and email
, where we will store the logged in users - more about that later on. Our initial project can now directly be downloaded and executed.
Creating a user table for our simple Spring Boot application
Now we can start making the additions for Keycloak. Besides the dependency org.springframework.boot:spring-boot-starter-oauth2-client
(version is provided automatically via the BOM), our application needs a number of settings that we add to our application.yml
/ application.properties
.
spring:
security:
oauth2:
client:
provider:
keycloak-bootify:
issuer-uri: http://localhost:8085/realms/bootify
registration:
keycloak-bootify:
client-id: testapp
client-secret: ${KEYCLOAK_CLIENT_SECRET:<<YOUR_CLIENT_SECRET>>}
client-name: Testapp
authorization-grant-type: authorization_code
redirect-uri: http://localhost:8080/login/oauth2/code/testapp
scope: openid,profile,email,offline_access
Registration of our OAuth provider
In the area behind provider
we add keycloak-bootify
to our application. Via the OIDC issuer-uri
Spring Boot will retrieve all required information for the OAuth integration. You may open it in the browser to see what's provided.
Behind registration
we add a client to our provider. There we use the client ID as well as the secret we received when configuring our Keycloak server. If we're using a frontend with DevServer, we specify port 8081
for the redirect URL. After this preparation we can already define our central configuration for Spring Security.
@Configuration
public class OAuthSecurityConfig {
private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
final OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}?logoutsuccess=true");
return oidcLogoutSuccessHandler;
}
@Bean
public SecurityFilterChain configure(final HttpSecurity http) throws Exception {
return http.cors(withDefaults())
.csrf((csrf) -> csrfwithDefaults())
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/css/**", "/js/**", "/images/**", "/webjars/**").permitAll()
.anyRequest().hasAuthority(UserRoles.ROLE_USER))
.oauth2Login(withDefaults())
.logout((logout) -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
.deleteCookies("JSESSIONID"))
.build();
}
}
Central setup of our application for OAuth / OIDC
Since Spring Boot 3.0, the configurations must be provided as a SecurityFilterChain
, configured with the HttpSecurity
class. After enablding CORS protection, we define some exceptions (e.g. "/"
for http://localhost:8080
) and expect the role "ROLE_USER"
for everything else. Authentication is done using OAuth 2 login. In addition, we have already defined our own logout handler, which redirects us back to the homepage after a successful logout.
Role Mapping
The connection to Keycloak should work by now, but the protected areas will still not be accessible after login. According to our Keycloak setup, the roles are already provided as part of the ID token, but not yet read out. Therefore we have to extend our configuration with the role mapping.
public class OAuthSecurityConfig {
// ...
/**
* Custom mapper to use OIDC claims as Spring Security roles.
*/
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
final Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach((authority) -> {
if (authority instanceof OidcUserAuthority oidcAuth) {
mappedAuthorities.addAll(mapAuthorities(oidcAuth.getIdToken().getClaims()));
} else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
mappedAuthorities.addAll(mapAuthorities(oauth2Auth.getAttributes()));
}
});
return mappedAuthorities;
};
}
/**
* Read claims from attribute realm_access.roles as SimpleGrantedAuthority.
*/
private List<SimpleGrantedAuthority> mapAuthorities(final Map<String, Object> attributes) {
final Map<String, Object> realmAccess = ((Map<String, Object>)attributes.getOrDefault("realm_access", Collections.emptyMap()));
final Collection<String> roles = ((Collection<String>)realmAccess.getOrDefault("roles", Collections.emptyList()));
return roles.stream()
.map((role) -> new SimpleGrantedAuthority(role))
.toList();
}
}
Providing a GrantedAuthoritiesMapper
This provides a GrantedAuthoritiesMapper
bean via our config, which is automatically picked up by Spring Security. This reads the roles from the realm_access.roles
field and transforms them into SimpleGrantedAuthority
. If we start our application now and go to a protected area, there should be an automatic redirect to Keycloak. After registratoin / log in we're send back to our application, where we are successfully authenticated and possess the required role.
Syncronization of OAuth users with the database
Authenticated users mostly have other business logic connected to them, such as storing addresses or personal data. Therefore it makes sense to synchronize the users with the database after each login. For this we add a UserSynchronizationService
to our Spring Boot application.
@Service
public class UserSynchronizationService {
// ...
private void syncWithDatabase(final OidcUserInfo userInfo) {
User user = userRepository.findByExternalId(userInfo.getSubject());
if (user == null) {
log.info("adding new user after successful login: {}", userInfo.getSubject());
user = new User();
user.setExternalId(userInfo.getSubject());
} else {
log.info("updating existing user after successful login: {}", userInfo.getSubject());
}
user.setEmail(userInfo.getEmail());
userRepository.save(user);
}
@EventListener(AuthenticationSuccessEvent.class)
public void onAuthenticationSuccessEvent(final AuthenticationSuccessEvent event) {
final OidcUser oidcUser = ((OidcUser)event.getAuthentication().getPrincipal());
syncWithDatabase(oidcUser.getUserInfo());
}
}
Creating or updating the users in the database
This service responds to the AuthenticationSuccessEvent
that is automatically triggered after a user logged in via OAuth. Each user is uniquely identified by the "subject"
field, even if other user data has changed. The new or existing User
object is then filled with the current email and persisted.
In the Free plan of Bootify, Spring Boot apps in the current version 3.1.0
can be initialized with their own database schema and CRUD functions. In the Professional plan, Spring Security can also be configured - here including Keycloak as an option. This will provide the setup described here out-of-the-box, customized to the chosen settings.
Top comments (0)