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.
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 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.6.7 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.15.1.Final</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-dependencies-server-all</artifactId>
<version>18.0.0</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>13.0.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;
} else if (name.startsWith("java:jboss/ee/concurrency/executor/")) {
return fixedThreadPool();
}
return null;
}
@Override
public NameParser getNameParser(String name) {
return CompositeName::new;
}
@Override
public void close() {
}
});
}
@Bean("fixedThreadPool")
public ExecutorService fixedThreadPool() {
return Executors.newFixedThreadPool(5);
}
@Bean
@ConditionalOnMissingBean(name = "springBootPlatform")
protected SimplePlatformProvider springBootPlatform() {
return (SimplePlatformProvider) Platform.getPlatform();
}
}
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")));
}
@Override
protected ExportImportManager bootstrap() {
final ExportImportManager exportImportManager = super.bootstrap();
createMasterRealmAdminUser();
createBaeldungRealm();
return exportImportManager;
}
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;DB_CLOSE_ON_EXIT=FALSE
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. The source code for this application is available over on GitHub.
The original idea for this implementation was developed by Thomas Darimont and can be found in the project embedded-spring-boot-keycloak-server.
No comments:
Post a Comment