DEV Community

Thomas
Thomas

Posted on • Originally published at bootify.io

Connecting Spring Boot to Keycloak via OAuth / OIDC

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
Enter fullscreen mode Exit fullscreen mode

  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();
    }

}
Enter fullscreen mode Exit fullscreen mode

  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();
    }

}
Enter fullscreen mode Exit fullscreen mode

  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());
    }

}
Enter fullscreen mode Exit fullscreen mode

  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.

» See Features and Pricing

Top comments (0)