Keycloak Embedded in a Spring Boot Application

 1. Overview

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

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 john@test.com and mike@other.com; 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>

       

Note that we're using Spring Boot's 2.2.6.RELEASE version here. The dependencies spring-boot-starter-data-jpa and H2 have been added 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 couple of dependencies for Keycloak and RESTEasy:

<dependency>
    <groupId>org.jboss.resteasy</groupId>
    <artifactId>resteasy-jackson2-provider</artifactId>
    <version>3.12.1.Final</version>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-dependencies-server-all</artifactId>
    <version>11.0.2</version>
    <type>pom</type>
</dependency> 

Check the Maven site for latest versions of Keycloak and RESTEasy.


And finally, we have to override the <infinispan.version> property, to use the version declared by Keycloak instead of the one defined by Spring Boot:

<properties>
    <infinispan.version>10.1.8.Final</infinispan.version>
</properties>

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
    FilterRegistrationBean keycloakSessionManagement(
      KeycloakServerProperties keycloakServerProperties) {
        FilterRegistrationBean filter = new FilterRegistrationBean<>();
filter.setName("Keycloak Session Management");
filter.setFilter(new EmbeddedKeycloakRequestFilter());
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() {
            }
        });
    }
}

Note: don't worry about the compilation error, we'll define the EmbeddedKeycloakRequestFilter class later on.


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;
 
    protected void loadConfig() {
        JsonConfigProviderFactory factory = new RegularJsonConfigProviderFactory();
        Config.init(factory.create()
          .orElseThrow(() -> new NoSuchElementException("No value present")));
    }
    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();
    }
}

7. Custom Platform Implementations

As we said, Keycloak is developed by RedHat/JBoss. Therefore, it provides functionality and extension libraries to deploy the application on a Wildfly server, or as a Quarkus solution.


In this case, we're moving away from those alternatives, and as a consequence, we have to provide custom implementations for some platform-specific interfaces and classes.


For example, in the EmbeddedKeycloakApplication we just configured we first loaded Keycloak's server configuration keycloak-server.json, using an empty subclass of the abstract JsonConfigProviderFactory:


public class RegularJsonConfigProviderFactory extends JsonConfigProviderFactory { }

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


As you can see, we use a KeycloakSession to perform all the transactions, and for this to work properly, we had to create a custom AbstractRequestFilter (EmbeddedKeycloakRequestFilter) and set up a bean for this using a KeycloakSessionServletFilter in the EmbeddedKeycloakConfig file.


Additionally, we need a couple of custom providers so that we have our own implementations of org.keycloak.common.util.ResteasyProvider and org.keycloak.platform.PlatformProvider and do not rely on external dependencies.


Importantly, information about these custom providers should be included in the project's META-INF/services folder so that they are picked up at runtime.

8. Bringing It All Together

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.

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

8.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

8.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/.

9. Conclusion

In this quick tutorial, we saw how to setup a Keycloak server embedded in a Spring Boot application. 

Comments

Post a Comment

Popular Posts