Friday, July 6, 2018

Integrate Keycloak with Spring Security

Keycloak can be very easily integrate with Spring Security. In this post I'm going to demonstrate, How to integrate Keycloak with Spring Security and How it works with Single Sign on.

If you are familiar with Spring Security, I think you may know about the Security Configuration file which extends WebSecurityConfigurerAdapter Similar to this, we can use KeycloakWebSecurityConfigurerAdapter to integrate Keycloak with Spring Security. If you follow Keycloak documentation, you will be able to find the Spring Security Adapter in Securing Apps tab. We have to follow few steps.
  • Add Keycloak Spring Security dependency in pom.xml
  • Add Keycloak configuration file which annotated @KeycloakConfiguration
  • Add Keycloak properties to application.properties file.
Lets create a very simple Spring Boot application as a web client. Here I use FreeMarker templates.

Project Structure




pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>

 <groupId>com.example</groupId>
 <artifactId>product-app</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <packaging>jar</packaging>

 <name>product-app</name>
 <description>Demo project for Spring Boot</description>

 <parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.5.3.RELEASE</version>
  <relativePath/>
 </parent>

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <java.version>1.8</java.version>
  <keycloak.version>3.4.2.Final</keycloak.version>
 </properties>

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

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
 </dependencies>

 <dependencyManagement>
  <dependencies>
   <dependency>
    <groupId>org.keycloak.bom</groupId>
    <artifactId>keycloak-adapter-bom</artifactId>
    <version>${keycloak.version}</version>
    <type>pom</type>
    <scope>import</scope>
   </dependency>
  </dependencies>
 </dependencyManagement>

 <build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
  </plugins>
 </build>

</project>


ProductController.java

package com.example.productapp.controller;

import com.example.productapp.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

@Controller
public class ProductController {

    @Autowired
    private ProductService productService;

 @GetMapping(path = "/products")
 public String getProducts(Model model){
  model.addAttribute("products", productService.getProducts());
  return "products";
 }

 @GetMapping(path = "/logout")
 public String logout(HttpServletRequest request) throws ServletException {
  request.logout();
  return "/";
 }
}


ProductService.java

package com.example.productapp.service;

import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;

@Service
public class ProductService{

    public List getProducts(){
        return Arrays.asList("Mazda","Toyota","Audi");
    }
}


SecurityConfig.java

package com.example.productapp.security;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {

 @Autowired
 public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
  KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
  keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
  auth.authenticationProvider(keycloakAuthenticationProvider);
 }

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

 @Bean
 public KeycloakConfigResolver keycloakConfigResolver(){
  return new KeycloakSpringBootConfigResolver();
 }

 @Override
 protected void configure(HttpSecurity http) throws Exception{
  super.configure(http);
  http.authorizeRequests()
    .antMatchers("/products*").hasRole("user")
    .anyRequest().permitAll();
 }
}


application.properties file

server.port=8090
keycloak.enabled=true
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.realm=api
keycloak.resource=client1
keycloak.public-client=true

index.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>

<body class="text-center">

<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">
    <main role="main" class="inner cover">
        <h1 class="cover-heading">Apache Keycloak SSO demonstration</h1>
        <p class="lead">
        </p>
        <p class="lead">
            <a href="/products" class="btn btn-lg btn-secondary">Search products</a>
        </p>
    </main>
</div>

</body>
</html>


products.ftl

<#import "/spring.ftl" as spring>

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Keycloak</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
          integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">

</head>

<body class="text-center">
<div class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column">

    <main role="main" class="inner cover">
        <h1 class="cover-heading">Apache Keycloak SSO demonstration</h1>

        <div style="width: 18rem; padding: 10px; display: block; margin-left: auto;margin-right: auto">
            <ul class="list-group">
                <#list products as product>
                    <li class="list-group-item">${product}</li>
                </#list>
            </ul>
        </div>

        <p class="lead">
            <a href="/logout" class="btn btn-lg btn-secondary">Logout</a>
        </p>
    </main>
</div>

</body>
</html>



Configure Keycloak server

I already configured Keycloak in this post. Since this is a client web application, we have to use Access type as public in Keycloak client configuration settings. Public clients do not require a client secret. 

Click to enlarge 

You have to do the same thing here,you can use any Realm name and client name for Keycloak configurations and you can use same data in Keycloak properties in application.properties file.

Run your application

Start the Keycloak server and run Spring Boot application. In my application Keycloak server is running on port 8080 and Spring Boot application is running on port 8090. 





Once you click on Search products, you will be redirected to Keycloak login screen. Because we have secured /products* URL pattern.


Once you provide authentication details, you will be able to access the secured content. 



Now you can see how Keycloak works with Spring Security. Lets see how we can use it on Single Sign On (SSO).

Keycloak with Single Sign On

You can create same kind of another application which is running on another port. application.property file and service file are different.

application.properties file

server.port=8092
keycloak.enabled=true
keycloak.auth-server-url=http://localhost:8080/auth
keycloak.realm=api
keycloak.resource=client2
keycloak.public-client=true


As you can see keycloak.realm is same for both applications (api), but keycloak.resource is different (client1, client2)

ProductService.java

package com.example.productapp.service;

import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;

@Service
public class ProductService{
    public List getProducts(){
        return Arrays.asList("Apple","Sony","Nokia", "Mi");
    }
}

And here Service is also different. Then run this Spring Boot application parallel with last application. Now two applications are running on port 8090 and 8092. Keycloak server is also running on port 8080.

Then you can try http://localhost:8092/products and you will be able to access this secured endpoint without going to Keycloak login page.  




Now you can see how SSO works with Keycloak and Spring Security. Here is the Git repository related to this project.

git clone https://github.com/raviyasas/SpringBoot-Keycloak-SpringSecurity-demo.git