Spring Security and Keycloak to Secure a Spring Boot Application - A First Look

Keycloak is a solution for identity and access management. In this article, we're going to secure a Spring Boot application using Spring Security and Keycloak.

Spring Security and Keycloak to Secure a Spring Boot Application  - A First Look

In previous articles, we have explored some cool Keycloak features and used them to secure a Spring Boot application. In this tutorial, we will bring Spring Security in and see how to integrate it with Keycloak seamlessly.

We're going to:

  • Set up a Spring Boot application;
  • Integrate Spring Boot with Keycloak;
  • Configure Spring Security to use Keycloak as an authentication provider;
  • Implement the application logic;
  • Add access policies based on user roles to protect our application endpoints.

1. Prerequisites

To follow along with this tutorial, you will need:

  • JDK 8 or JDK 11+
  • an IDE
  • Gradle 5.5.0+
  • Spring Boot 2.1.0+
  • Keycloak 7.0.0+

If you prefer to use Apache Maven rather than Gradle, feel free to do so.

You will need a Keycloak server installed and configured. If you are just getting started and want to know more about it, refer to my previous articles in this series to learn how to install Keycloak, how to do some basic configuration for authentication and authorization, and how to set up a client for our application.

Throughout this tutorial, I will use a Keycloak server configured in the same way as explained in the mentioned articles.

Should you be interested in having a look at how to secure a Spring Boot application with Keycloak, without using Spring Security, I wrote an article about that, too. But it's not a prerequisite to follow along with this one.

On GitHub, you can find the source code for the application we are building in this tutorial.

Let's get started!

2. The Demo Application

The application we're going to build is for a fictitious public library. There are two groups of users:

  • members can browse the books available in the library;
  • librarians can also manage the books.

For each group of users, we have a corresponding user role defined in Keycloak.

Also, we have two users registered in the system: Sheldon is a member, Irma is both a member and a librarian.

Name Username Roles
Sheldon Cooper sheldon.cooper Member
Irma Pince irma.pince Member, Librarian

3. Set Up A Spring Boot Application

Our application will make use of three main libraries to set Spring up:

  • spring-boot-starter-web, a starter for building web applications with Spring MVC;
  • spring-boot-starter-thymeleaf, a starter to use Thymleaf views for Spring MVC;
  • spring-boot-starter-security, a starter for using Spring Security.

The integration with Keycloak is made possible by the so-called client adapters.

Keycloak client adapters are libraries that make it very easy to secure applications and services with Keycloak.

For this project, we will need:

The keycloak-spring-boot-starter library includes both of them, so we don't need anything else other than that.

We can define all those dependencies in the gradle.build file for our project (or pom.xml if using Maven).

buildscript {
    ext {
        springBootVersion = '2.1.9.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.thomasvitale'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

ext {
    set('keycloakVersion', '7.0.1')
}

dependencies {
    // Spring
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-security'

    // Keycloak
    implementation 'org.keycloak:keycloak-spring-boot-starter'

    // Test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.boot:spring-security-test'
    testImplementation 'org.keycloak:keycloak-test-helper'
}

dependencyManagement {
    imports {
        mavenBom "org.keycloak.bom:keycloak-adapter-bom:${keycloakVersion}"
    }
}
build.gradle

That's it. We have set up a Spring Boot application, ready to leverage Spring Security and Keycloak.

4. Configure Keycloak In Spring Boot

We should now provide some configuration for the integration between Spring Boot and Keycloak.

4.1. Basic Keycloak Configuration

The default configuration file for Keycloak is keycloak.json, but thanks to the Keycloak Spring Boot Adapter we can use the native application.properties file (or application.yml).

keycloak.realm=public-library
keycloak.resource=spring-boot-app
keycloak.auth-server-url=http://localhost:8180/auth
keycloak.ssl-required=external
keycloak.public-client=true
keycloak.principal-attribute=preferred_username
application.properties

Let's look at the properties one by one:

  • keycloak.realm: the name of the realm, required;
  • keycloak.resource: the client-id of the application, required;
  • keycloak.auth-server-url: the base URL of the Keycloak server, required;
  • keycloak.ssl-required:  establishes if communications with the Keycloak server must happen over HTTPS. Here, it's set to external, meaning that it's only needed for external requests (default value). In production, instead, we should set it to all. Optional;
  • keycloak.public-client: prevents the application from sending credentials to the Keycloak server (false is the default value). We want to set it to true whenever we use public clients instead of confidential. Optional;
  • keycloak.principal-attribute: the attribute with which to populate the UserPrincipal name. Optional.

4.2. Use The Keycloak Configuration Defined In application.properties

Starting from Keycloak Spring Boot Adapter 7.0.0, due to some issues, the automatic discovery of the Keycloak configuration from the application.properties (or application.yml) file will not work. To overcome this problem, we need to define a KeycloakSpringBootConfigResolver bean explicitly in a @Configuration class.

@Configuration
public class KeycloakConfig {

    @Bean
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
}
KeycloakConfig.java

Without this bean, we would get an error.

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 1 of method setKeycloakSpringBootProperties in org.keycloak.adapters.springboot.KeycloakBaseSpringBootConfiguration required a bean of type 'org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver' that could not be found.


Action:

Consider defining a bean of type 'org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver' in your configuration.


Process finished with exit code 1
Error thrown when no KeycloakSpringBootConfigResolver bean has been defined.

This problem doesn't affect the Keycloak Spring Boot Adapter versions older than 7.0.0.

5. Implement The Application Logic

We want to use Spring Security and Keycloak to make sure that the users of our application are authenticated and authorized, before accessing some protected endpoints.

5.1. The Controller

Let's define three endpoints in a Spring MVC Controller class:

  • /index will be freely accessible;
  • /books will be accessible only by users with a Member role: they can browse the books available at the library;
  • /manager will be accessible only by users with a Librarian role: they can manage the books.

Later on, we will configure the last two endpoints to require the users to both be authenticated and have the proper role. It's also helpful adding a /logout endpoint for logging out conveniently.

@Controller
public class LibraryController {
	private final HttpServletRequest request;
	private final BookRepository bookRepository;

	@Autowired
	public LibraryController(HttpServletRequest request, BookRepository bookRepository) {
		this.request = request;
		this.bookRepository = bookRepository;
	}

	@GetMapping(value = "/")
	public String getHome() {
		return "index";
	}

	@GetMapping(value = "/books")
	public String getBooks(Model model) {
		configCommonAttributes(model);
		model.addAttribute("books", bookRepository.readAll());
		return "books";
	}

	@GetMapping(value = "/manager")
	public String getManager(Model model) {
		configCommonAttributes(model);
		model.addAttribute("books", bookRepository.readAll());
		return "manager";
	}

	@GetMapping(value = "/logout")
	public String logout() throws ServletException {
		request.logout();
		return "redirect:/";
	}

	private void configCommonAttributes(Model model) {
		model.addAttribute("name", getKeycloakSecurityContext().getIdToken().getGivenName());
	}

	private KeycloakSecurityContext getKeycloakSecurityContext() {
		return (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
	}
}
LibraryController.java

In the last method, we use the KeycloakSecurityContext to retrieve the IdToken, from which we can get the first name of the authenticated user.

As a template engine, we're using Thymeleaf.  We have a template for each resource as well as a unique template to handle unauthorized requests. You can check out the full source code of this demo project on GitHub to see how the templates look like.

5.2. The Repository

The LibraryController class delegates the fetching of  Book entities to a BookRepository class. The repository is implemented hard-coded in memory, for the sake of the example.

@Repository
public class BookRepository {
    private static Map<String, Book> books = new ConcurrentHashMap<>();

    static {
        books.put("B01", new Book("B01", "Harry Potter and the Deathly Hallows", "J.K. Rowling"));
        books.put("B02", new Book("B02", "The Lord of the Rings", "J.R.R. Tolkien"));
        books.put("B03", new Book("B03", "War and Peace", "Leo Tolstoy"));
    }

    public List<Book> readAll() {
        List<Book> allBooks = new ArrayList<>(books.values());
        allBooks.sort(Comparator.comparing(Book::getId));
        return allBooks;
    }
}
BookRepository.java

5.3. The Entity

The Book class is a POJO.

public class Book {
    private String id;
    private String title;
    private String author;

    public Book(String id, String title, String author) {
        this.id = id;
        this.title = title;
        this.author = author;
    }

    // Getters and setters
}
Book.java

6. Configure Spring Security With Keycloak

If we tried to run the application at this point, we would notice that we could navigate through all the pages without any restriction. Let's change that.

We want to configure Spring Security to:

  • delegate the user authentication phase to Keycloak, following the OAuth 2.0/OpenID Connect Authorization Code Flow;
  • leverage the IdToken and AccessToken objects returned by Keycloak to define some access policies for our application endpoints.

6.1 Create a Security Configuration Class

Let's proceed step by step, starting with the creation of a SecurityConfig class.

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
	// Configuration
}

The SecurityConfig class should extend KeycloakWebSecurityConfigurerAdapter and be annotated with @KeycloakConfiguration. This annotation provides a Keycloak-based Spring Security configuration.

It is a stereotype to wrap the two annotations required by a Spring Security configuration class: @Configuration and @EnableWebSecurity. It also packs a third annotation required by Keycloak to scan correctly the beans configured in the Keycloak Spring Security Adapter: @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class).

6.2 Register Keycloak as the Authentication Provider

Keycloak is our identity provider (IdP), so let's register it with the Spring Security authentication manager.

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
    SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
    grantedAuthorityMapper.setPrefix("ROLE_");

    KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
    keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
    auth.authenticationProvider(keycloakAuthenticationProvider);
}

Here, we are registering KeycloakAuthenticationProvider with the authentication manager. In this way, Keycloak will be responsible for providing authentication services.

Spring Security has a convention to handle security roles in a format like ROLE_ADMIN (where ADMIN is the actual security role name). I don't like this convention. In Keycloak, I prefer defining capitalized, lowercase role names.

To solve the mismatch between the way we defined the user roles and the conventions used by Spring Security, we can specify a different configuration. So, we are setting a SimpleAuthorityMapper for the KeycloakAuthenticationProvider to prefix all the roles registered in Keycloak with the ROLE_ prefix. That will help Spring Security handling those roles in the best way.

In Keycloak, we have defined two roles: Member and Librarian. In Spring Security, after the mapping, they will become ROLE_Member and ROLE_Librarian.

We could even make the full role name uppercase by calling the method grantedAuthorityMapper.setConvertToUpperCase(true);, but for this demo application we are not doing that.

6.3 Define a Session Authentication Strategy

The application that we are building, in Keycloak terms, is a public application with user interaction. In this scenario, the recommended session authentication strategy is RegisterSessionAuthenticationStrategy, which registers a user session after successful authentication.

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}

When securing a service-to-service application, instead, we would use a NullAuthenticatedSessionStrategy.

6.4. Define Role-Based Access Security Policies

So far, we have done all the necessary configuration to make Spring Security work seamlessly with Keycloak. Our final step is about defining some security constraints for the application endpoints.

@Override
protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http
        .authorizeRequests()
        .antMatchers("/books").hasAnyRole("Member", "Librarian")
        .antMatchers("/manager").hasRole("Librarian")
        .anyRequest().permitAll();
}

The access policies for our application endpoints are:

  • /books -> user must be authenticated and with at least one of the roles Member and Librarian.
  • /manager -> user must be authenticated and with the role Librarian.
  • any other endpoint will be freely accessible, no role constraint, no authentication required.

6.5 Handle Bean Definition Overriding Issues

Since Spring 2.1.0, the spring.main.allow-bean-definition-overriding property is set to false by default, differently from the previous versions of Spring. It means that it's not allowed anymore to override a bean already defined.

The SecurityConfig class we are working on extends KeycloakWebSecurityConfigurerAdapter, which defines an HttpSessionManager bean. But this bean has already been defined somewhere else in the Keycloak Adapter library. Therefore, it triggers an error in Spring 2.1.0+.

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'httpSessionManager', defined in class path resource [com/thomasvitale/keycloak/config/SecurityConfig.class], could not be registered. A bean with that name has already been defined in URL [.../org/keycloak/adapters/springsecurity/management/HttpSessionManager.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true


Process finished with exit code 1
Error thrown when multiple beans of the same type have been defined.

We could fix the problem by changing the value for spring.main.allow-bean-definition-overriding to true. I prefer adjusting the bean definition to be loaded conditionally only if no other bean of that type has been defined.

@Bean
@Override
@ConditionalOnMissingBean(HttpSessionManager.class)
protected HttpSessionManager httpSessionManager() {
    return new HttpSessionManager();
}

6.7 The Final Spring Security Configuration with Keycloak

We have completed the security configuration. Here it is the final result.

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
        grantedAuthorityMapper.setPrefix("ROLE_");

        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
            .authorizeRequests()
            .antMatchers("/books").hasAnyRole("Member", "Librarian")
            .antMatchers("/manager").hasRole("Librarian")
            .anyRequest().permitAll();
    }
}
SecurityConfig.java

7. Testing The Application

It's time to verify if our code will work as intended. As you may have noticed, when setting up our build.gradle, we have defined a few dependencies for testing:

  • spring-boot-starter-test, a starter for testing Spring Boot applications with JUnit, Hamcrest and Mockito;
  • spring-security-test, which provides utilities to test Spring Security;
  • keycloak-test-helper, which helps testing applications using Keycloak.

Writing autotests for Spring applications secured by Keycloak will be the topic of a future article, so we're not going to use those libraries now. But here you have them, in case you're interested in diving into the topic.

Instead, let's run our application and manually check if it's working correctly.

8. Using The Application

Let's make sure our Keycloak server is up and running, and start our Spring application. By default, it will be available on http://localhost:8080.

Home page - Public

The home page is freely accessible. We are not required to be authenticated nor to have a specific role.

When we navigate to the other pages, the application redirects us to Keycloak for logging in.

Login page provided by Keycloak

After providing the right username and password, Keycloak redirects us back to our application. Depending on our role, we can visit the other pages as well.

If we log in as a Member, like Sheldon Cooper, then we are allowed to see the Browse Books page.

Browse Books - Protected (Member)

Even if we are authenticated, we don't have the rights to navigate to the Manage Library page. If we try to do so, then we are told that we are not allowed in.

If we log in as a Librarian, like Irma Pince, then we can also see the Manage Library page.

Manage Library - Protected (Librarian)

Finally, let's notice the Logout link in the top-right corner, which we can use to log out.

The application is working correctly, and all the security constraints have been enforced.

Well done!

Conclusion

In this tutorial, we have built a Spring Boot application and secured it with Spring Security and Keycloak.

We have configured the integration with Keycloak leveraging the Spring Boot Adapter. Then, we have used the Spring Security Adapter to set Keycloak as the authentication provider for our application and protected some endpoints by defining access control policies based on user roles.

If you are interested in Keycloak, have a look at my articles about this identity and access management solution.

Are you thinking about using Keycloak for your next project? Have you already been using it? I'd like to hear your stories about Keycloak, leave a comment or let me know on Twitter!

Last update: 15/11/2019

Keycloak Series

Keycloak with Spring Series


If you're interested in cloud native development with Spring Boot and Kubernetes, check out my book Cloud Native Spring in Action.