This assumes you have Docker installed and the Docker host IP is 127.0.0.1. If you use a different IP, consider find/replace across the entire repository.
Run ./keycloak-setup.sh
to run and provision a Keycloak server using Docker. Then, start both the clients and the resource application in their own terminal.
# Terminal 1: Run Keycloak on http://localhost:8080/auth
./keycloak-setup.sh
# Terminal 2: Run resource server (API) on http://localhost:8081/resource
cd resource
./gradlew bootRun
# Terminal 3: Run Grails 3 based client on http://localhost:8082/client
cd client
./gradlew bootRun
# Terminal 4: Run JavaScript based client on http://localhost:8083
cd vue
./start.sh
Open your browser to http://localhost:8082/client. You will be redirected to Keycloak where you can login using user
/user
or admin
/admin
. Next, select the client.ExampleController
or navigate to it directly at http://localhost:8082/client/example.
The example controller uses KeycloakRestTemplate
to request a resource from the resource
application running on http://localhost:8081/resource which is secured using bearer-only authentication.
Open your browser to http://localhost:8083/. You will be redirected to Keycloak where you can login using user
/user
or admin
/admin
. There you will see a HTML table rendered using Vue containing 1.000 persons.
The example uses axios
and Keycloak's JavaScript adapter to request a resource from the resource
application running on http://localhost:8081/resource which is secured using bearer-only authentication.
- CentOS 7
- OpenJDK 8
- Keycloak 3.0.0.Final (standalone)
- Grails 3.1.3 (with Gradle 2.9)
- Spring Boot 1.3.3.RELEASE
TODO
This section explains how to arrive at a working setup from scratch, assuming you have JDK 8 installed.
Follow the instructions on grails.org or, preferrably, use sdkman:
sdk install grails
grails create-app client
Inside your dependencies
block, add the following:
compile "org.keycloak:keycloak-spring-security-adapter:3.0.0.Final"
runtime "org.jboss.logging:jboss-logging:3.3.0.Final"
The Keycloak adapter has transitive dependencies on Spring Security, so you don't need to include it explicitly. The runtime dependency on jboss-logging
was not in the Keycloak documentation, but is required if you're not deploying to a JBoss/Wildfly server.
I removed the Hibernate/GORM dependencies to make the client a bit more lightweight, but the point is that it's a stock Grails 3 application.
The keycloak.json
file contains configuration data the adapter uses to communicate with Keycloak, such as the realm's public key (to verify JWT signatures), the root URL of the Keycloak server, the OAuth2 client-id to use, etc.
The Keycloak adapter automatically looks for it in WEB-INF/keycloak.json
, which needs to be created inside src/main/webapp/
. You can generate the contents of the file at the client's Installation tab inside the Keycloak admin console; choose Keycloak OIDC JSON format and double-check the value of auth-server-url
.
{
"realm": "grails",
"realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Q2QN4cCxc10Azb7CC4uas7efiPZOwwUHykxLIfHtc8Oxw2PkoBrSeMyA/skCewDvMJZHmc2SOdnA6DpqZ6rlXe7R6yzM4ytHIg4ijE4+PYotiP0lUTTTgIf3mbvaAiLUTZV1sz4TjmcSk95v4mb4CsqaDMGE+2EPyp0DW0NBqaynaE4aVKuGOxJAosJBBZodAOlthpMU59hL7JqBXQJKNvQsyxnJYHgJnLWaGUn8D8+Y1MMJgVjSrmtxqv/2coVaSJrRqSYirn+GmYgRGRR/BUyIikUROyGLo5Y/LtUvuTQM1vetHkcd3LJP7MpWqG9PDzvx412FbPw0sISzCLgaQIDAQAB",
"auth-server-url": "http://localhost:8080/auth",
"ssl-required": "external",
"resource": "grails-client",
"credentials": {
"secret": "25d75d68-993b-4719-8380-dafc77566c95"
}
}
This is perhaps the most daunting piece of boilerplate, but bare with me. You can enable Spring Security by simply annotating a class with @EnableWebSecurity
. This locks down your entire application with HTTP Basic Authentication using a random password generated during startup (written to stdout). However, we want the Keycloak adapter to act as an authentication provider, and we can extend the abstract KeycloakWebSecurityConfigurerAdapter
to configure Spring Security to achieve this.
Create the file grails-app/init/client/SecurityConfig.groovy
, and we'll add code to it step by step:
package client
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents
import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory
import org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.config.ConfigurableBeanFactory
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Scope
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.core.session.SessionRegistryImpl
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy
import org.springframework.security.web.session.HttpSessionEventPublisher
@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider())
}
}
The Keycloak documentation states:
You must provide a session authentication strategy bean which should be of type
RegisterSessionAuthenticationStrategy
for public or confidential applications andNullAuthenticatedSessionStrategy
for bearer-only applications.
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl())
}
When choosing RegisterSessionAuthenticationStrategy
you must also add a HttpSessionEventPublisher
according to the Spring documentation:
HttpSessionEventPublisher
causes anApplicationEvent
to be published to the SpringApplicationContext
every time aHttpSession
commences or terminates. This is critical, as it allows theSessionRegistryImpl
to be notified when a session ends. Without it, a user will never be able to log back in again once they have exceeded their session allowance, even if they log out of another session or it times out.
@Bean
ServletListenerRegistrationBean<HttpSessionEventPublisher> getHttpSessionEventPublisher() {
new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher())
}
To learn more about Spring Security's session authentication strategies, check the official documentation.
By default your application is not secured. If you want to lock down your entire application you need to do override the configure
method:
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure http
http
.logout()
.logoutSuccessUrl("/sso/login") // Override Keycloak's default '/'
.and()
.authorizeRequests()
.antMatchers("/assets/*").permitAll()
.anyRequest().hasAnyAuthority("USER", "ADMIN")
}
This does three things:
- Change the logoutSuccess URL from
/
to the SSO login page, otherwise it prevents us from securing the home page - Allow anyone to access the
/assets
folder, otherwise your layout breaks on error pages - Require USER or ADMIN role for anything else
If you want to secure only specific paths, you can do this:
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure http
http
.authorizeRequests()
.antMatchers("/admin/*").hasAuthority("ADMIN")
.antMatchers("/app/*").hasAnyAuthority("USER", "ADMIN")
.anyRequest().permitAll()
}
This leaves the home page and assets folder unsecured, but requires the ADMIN role to access /admin/
for example.
The Keycloak documentation states:
To simplify communication between clients, Keycloak provides an extension of Spring's
RestTemplate
that handles bearer-token authentication for you. To enable this feature your security configuration must add theKeycloakRestTemplate
bean. The bean must be scoped as a prototype to function correctly.
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory)
}
You can inject this bean and use it to transparently access a protected resource without dealing with access tokens, for example:
class RemoteStuffService {
private static final STUFF_ENDPOINT = "http://server/api/stuff"
KeycloakRestTemplate keycloakRestTemplate
JsonNode getStuff() {
keycloakRestTemplate.getForObject(STUFF_ENDPOINT, JsonNode.class)
}
}
As a sidenote, you can access the token and other interesting things related to the authentication via
org.springframework.security.core.context.SecurityContextHolder.context.authentication
Make a small change to grails-app/init/client/Application.groovy
to include the client
package in scanning for Spring components such as @Configuration
classes. Since it's short, I've included the entire class below:
package client
import grails.boot.GrailsApp
import grails.boot.config.GrailsAutoConfiguration
import org.springframework.context.annotation.ComponentScan
@ComponentScan(basePackages = "client")
class Application extends GrailsAutoConfiguration {
static void main(String[] args) {
GrailsApp.run(Application, args)
}
}
TODO