Connecting Spring Boot to Keycloak via OAuth / OIDC
Tags: springboot, oauth, keycloak, java
Published: 2023-06-14 09:04:30

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](https://bootify.io/next-steps/keycloak-configuration-for-spring-boot.html). 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](https://bootify.io/) 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.

![](https://s3-eu-central-1.amazonaws.com/bootify-prod/ext/img/securityKeycloak/createUserInEntitiesTab.png "Creating a user table for our simple Spring Boot application")
_&nbsp; 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`.

```yml
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
```
_&nbsp; 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.


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

}
```
_&nbsp; 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**.

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

}
```
_&nbsp; 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.

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

}
```
_&nbsp; 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**.

[**&raquo; See Features and Pricing**](https://bootify.io/pricing.html)