Security Top

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE

1. Overview

Keycloak is an open-source Identity and Access Management solution administered by RedHat, and developed in Java by JBoss.

In this tutorial, we'll learn how to set up a Keycloak server embedded in a Spring Boot application. This makes it easy to start-up a pre-configured Keycloak server.

Keycloak can also be run as a standalone server, but then it involves downloading it and setup via the Admin Console.

2. Keycloak Pre-Configuration

To start with, let's understand how we can pre-configure a Keycloak server.

The server contains a set of realms, with each realm acting as an isolated unit for user management. To pre-configure it, we need to specify a realm definition file in a JSON format.

Everything that can be configured using the Keycloak Admin Console is persisted in this JSON. 

Our Authorization Server will be pre-configured with baeldung-realm.json. Let's see a few relevant configurations in the file:

  • users: our default users would be [email protected] and [email protected]; they'll also have their credentials here
  • clients: we'll define a client with the id newClient
  • standardFlowEnabled:  set to true to activate Authorization Code Flow for newClient
  • redirectUris: newClient‘s URLs that the server will redirect to after successful authentication are listed here
  • webOrigins: set to “+” to allow CORS support for all URLs listed as redirectUris

The Keycloak server issues JWT tokens by default, so there is no separate configuration required for that. Let's look at the Maven configurations next.

3. Maven Configuration

Since we'll embed Keycloak inside of a Spring Boot application, there is no need to download it separately.

Instead, we'll set up the following set of dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>        
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-devtools</artifactId>
</dependency>        

Note that we're using spring-boot-starter-data-jpa and H2 for persistence. The other springframework.boot dependencies are for web support, as we also need to be able to run the Keycloak authorization server as well as admin console as web services.

We'll also need a few dependencies specific to Keycloak:

<dependency>
<groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jaxb-provider</artifactId>
    <version>3.9.0.Final</version>
</dependency>

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jackson2-provider</artifactId>
    <version>3.9.0.Final</version>
</dependency>

<dependency>
    <groupId>org.infinispan</groupId>
    <artifactId>infinispan-core</artifactId>
</dependency>

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-model-infinispan</artifactId>
    <version>8.0.0</version>
</dependency>    

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-services</artifactId>
    <version>8.0.0</version>
    <exclusions>
        <exclusion>
            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
            <groupId>org.jboss.spec.javax.servlet</groupId>
        </exclusion>
    </exclusions>
</dependency> 

<dependency>
    <groupId>org.jboss.modules</groupId>
    <artifactId>jboss-modules</artifactId>
    <version>1.9.2.Final</version>
</dependency>  

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-js-adapter</artifactId>
    <version>8.0.0</version>
</dependency>  

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-themes</artifactId>
    <version>8.0.0</version>
</dependency> 

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-saml-core</artifactId>
    <version>8.0.0</version>
</dependency> 

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-authz-policy-common</artifactId>
    <version>8.0.0</version>
</dependency> 

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-wildfly-extensions</artifactId>
    <version>8.0.0</version>
</dependency> 

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
</dependency> 

Check the Maven site for latest dependencies for Keycloak, resteasy and jboss-modules.

4.  Embedded Keycloak Configuration

Now let's define the Spring configuration for our authorization server:

@Configuration
public class EmbeddedKeycloakConfig {

    @Bean
    ServletRegistrationBean keycloakJaxRsApplication(
      KeycloakServerProperties keycloakServerProperties, DataSource dataSource) throws Exception {
        
        mockJndiEnvironment(dataSource);
        EmbeddedKeycloakApplication.keycloakServerProperties = keycloakServerProperties;
        ServletRegistrationBean servlet = new ServletRegistrationBean<>(
          new HttpServlet30Dispatcher());
        servlet.addInitParameter("javax.ws.rs.Application", 
          EmbeddedKeycloakApplication.class.getName());
        servlet.addInitParameter(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX,
          keycloakServerProperties.getContextPath());
        servlet.addInitParameter(ResteasyContextParameters.RESTEASY_USE_CONTAINER_FORM_PARAMS, 
          "true");
        servlet.addUrlMappings(keycloakServerProperties.getContextPath() + "/*");
        servlet.setLoadOnStartup(1);
        servlet.setAsyncSupported(true);
        return servlet;
    }

    @Bean
    ServletListenerRegistrationBean keycloakSessionDestroyListener() {
        return new ServletListenerRegistrationBean<>(new KeycloakSessionDestroyListener());
    }

    @Bean
    FilterRegistrationBean keycloakSessionManagement(
      KeycloakServerProperties keycloakServerProperties) { 
        FilterRegistrationBean filter = new FilterRegistrationBean<>();
        filter.setName("Keycloak Session Management");
        filter.setFilter(new KeycloakSessionServletFilter());
        filter.addUrlPatterns(keycloakServerProperties.getContextPath() + "/*");
        return filter;
    }

    private void mockJndiEnvironment(DataSource dataSource) throws NamingException {		 
        NamingManager.setInitialContextFactoryBuilder(
          (env) -> (environment) -> new InitialContext() {
            @Override
            public Object lookup(Name name) {
                return lookup(name.toString());
            }
	
            @Override
            public Object lookup(String name) {
                if ("spring/datasource".equals(name)) {
                    return dataSource;
                }
                return null;
            }

            @Override
            public NameParser getNameParser(String name) {
                return CompositeName::new;
            }

            @Override
            public void close() {
            }
        });
    }
}

As we can see here, we first configured Keycloak as a JAX-RS application with KeycloakServerProperties for persistent storage of Keycloak properties as specified in our realm definition file. We then added a session management filter and mocked a JNDI environment to use a spring/datasource, which is our in-memory H2 database.

5. KeycloakServerProperties

Now let's have a look at the KeycloakServerProperties we just mentioned:

@ConfigurationProperties(prefix = "keycloak.server")
public class KeycloakServerProperties {
    String contextPath = "/auth";
    String realmImportFile = "baeldung-realm.json";
    AdminUser adminUser = new AdminUser();

    // getters and setters

    public static class AdminUser {
        String username = "admin";
        String password = "admin";

        // getters and setters        
    }
}

As we can see, this is a simple POJO to set the contextPath, adminUser and realm definition file.

6. EmbeddedKeycloakApplication

Next, let's see the class, which uses the configurations we set before, to create realms:

public class EmbeddedKeycloakApplication extends KeycloakApplication {
    private static final Logger LOG = LoggerFactory.getLogger(EmbeddedKeycloakApplication.class);
    static KeycloakServerProperties keycloakServerProperties;
    public EmbeddedKeycloakApplication() {
        createMasterRealmAdminUser();
        createBaeldungRealm();
    }

    private void createMasterRealmAdminUser() {
        KeycloakSession session = getSessionFactory().create();
        ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
        AdminUser admin = keycloakServerProperties.getAdminUser();
        try {
            session.getTransactionManager().begin();
            applianceBootstrap.createMasterRealmUser(admin.getUsername(), admin.getPassword());
            session.getTransactionManager().commit();
        } catch (Exception ex) {
            LOG.warn("Couldn't create keycloak master admin user: {}", ex.getMessage());
            session.getTransactionManager().rollback();
        }
        session.close();
    }

    private void createBaeldungRealm() {
	KeycloakSession session = getSessionFactory().create();
	try {
  	    session.getTransactionManager().begin();
   	    RealmManager manager = new RealmManager(session);
	    Resource lessonRealmImportFile = new ClassPathResource(
              keycloakServerProperties.getRealmImportFile());
	    manager.importRealm(JsonSerialization.readValue(lessonRealmImportFile.getInputStream(),
              RealmRepresentation.class));
	    session.getTransactionManager().commit();
	} catch (Exception ex) {
	    LOG.warn("Failed to import Realm json file: {}", ex.getMessage());
	    session.getTransactionManager().rollback();
	}

	session.close();
    }
}

Here we extended the KeycloakApplication to create two realms: master and baeldung. Again, these are created as per the properties specified in our realm definition file, baeldung-realm.json.

As we saw, Keycloak has much simplified the required configurations from the application side. There is no need to programmatically define the datasource or any security configurations.

7. Bringing it All Together

To bring it all together, we need to define the configuration for Spring and a Spring Boot Application.

7.1. application.yml

We'll be using a simple YAML for the Spring configurations:

server:
  port: 8083

spring:
  datasource:
    username: sa
    url: jdbc:h2:mem:testdb

keycloak:
  server:
    contextPath: /auth
    adminUser:
      username: bael-admin
      password: ********
    realmImportFile: baeldung-realm.json

7.2. Spring Boot Application

Lastly, here's the Spring Boot Application:

@SpringBootApplication(exclude = LiquibaseAutoConfiguration.class)
@EnableConfigurationProperties(KeycloakServerProperties.class)
public class AuthorizationServerApp {
    private static final Logger LOG = LoggerFactory.getLogger(AuthorizationServerApp.class);
    
    public static void main(String[] args) throws Exception {
        SpringApplication.run(AuthorizationServerApp.class, args);
    }

    @Bean
    ApplicationListener<ApplicationReadyEvent> onApplicationReadyEventListener(
      ServerProperties serverProperties, KeycloakServerProperties keycloakServerProperties) {
        return (evt) -> {
            Integer port = serverProperties.getPort();
            String keycloakContextPath = keycloakServerProperties.getContextPath();
            LOG.info("Embedded Keycloak started: http://localhost:{}{} to use keycloak", 
              port, keycloakContextPath);
        };
    }
}

Notably, here we have enabled the KeycloakServerProperties configuration to inject it into the ApplicationListener bean.

After running this class, we can access the authorization server's welcome page at http://localhost:8083/auth/.

8. Conclusion

In this quick tutorial, we saw how to setup a Keycloak server embedded in a Spring Boot application. The source code for this application is available over on GitHub.

Security bottom

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security 5:

>> CHECK OUT THE COURSE

4
Leave a Reply

avatar
2 Comment threads
2 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
Loredana CrusoveanuAlexandru Tanasescuhurelhuyag Recent comment authors
  Subscribe  
newest oldest most voted
Notify of
hurelhuyag
Guest
hurelhuyag

freemarker 8.0.0? 2.3.30?

Loredana Crusoveanu
Guest

Thanks. The dependency is managed by Boot in this case, so I’ve removed the version.

Alexandru Tanasescu
Guest
Alexandru Tanasescu

The exclusions and exclusion tags are not correctly closed in the example

Loredana Crusoveanu
Guest

Thanks Alex, fixed.