Secure Spring Boot Application With Keycloak
Tags: springboot, webdev, java, microservices
Published: 2022-01-07 12:13:14


After the announcement of spring security team, spring is no longer supporting its own authorization server. Spring security OAuth2 officially deprecated all of its classes. Therefore it is recommended to use existing authorization server such as Keycloak or Okta.

In this tutorial we will be using Keycloak as authorization server with spring boot.

What is Keycloak?
Keycloak is an open source software product to allow single sign-on with Identity and Access Management aimed at modern applications and services

Downloading and Installing Keycloak
Before getting started, you will need to installed and setup a Keycloak server. Visit this url https://www.keycloak.org/ to download Keycloak  version 15.0.2.

After this go to your local directory    keycloak->keycloak-15.0.2->bin

Run standalone.bat file.

Go to http://localhost:8080/auth/ and create admin user. Set admin /admin for username and password.


Now you will be able to view the admin console.

In this section, we setup the Keycloak configuration.

1. Create a Realm
A realm is a security domain that manages a collection of users, roles, credentials and groups. The protected resources on server can divided into different realm where each realm manages its own authentication and authorization.

Now select, Add realm from Master dropdown menu.

Set the name of realm to test-realm and click create.


2. Create a Client
Clients are entities that request to Keycloak for authentication. Mostly clients are application and services that uses Keycloak to secure themselves and provide single sign on solution.

Clients request access token or identity information to use services that are secured by Keycloak.

From the left sidebar select client and click create client.


Now set app-client in client-id  and client-protocal-> openid-connect. Set root url to application url http://localhost:8080.


Now click on save button. Then you will see app-client setting page. Here we will modify some properties for authentication. First we change access type to confidential then  the Authorization Enabled to ON , Service Account Enabled to ON and click on Save button.


From credential tab you can view the client-secret which would be required later.


3. Create client roles
Client Roles are only accessible from a specific client and the role of client cannot be accessible from a different client.

Here we will create two roles admin and student.

From the client tab, click on roles tab and click on Add Role.


Now Add Role.

Set Role Name to admin


Now create other role and set Role Name to student. Then press the save button to save the role



4. Create Realm Roles
Realm roles are global roles shared by all clients. Realm role only belongs to specific realm. Role of one realm can’t be accessible by other realm.

From the sidebar menu click on Roles. List of available roles listed here.


Now Add Realm Role -> test-admin and save role.


Enable composite roles. Here we will assign client and client roles to realm role.

Select the app-client from client roles dropdown.

From available Roles click admin and press Add-Selected.


Now add second Realm Role -> test-student and save role


Enable composite roles.

Select the app-client from client roles dropdown for test-student role.

From available Roles click student and press Add-Selected.


5. Create Users
Users are entities that are able to log into your system. User can have attributes username ,password, email etc. Users access resources through specific clients.

Select Users from sidebar and Add User -> student-1. Press save.


Click the credential tab to set a password for user.

Set password -> student123

Switch off the temporary password and press save.


From the tabs, select Role Mapping. Here we will assign realm roles to users.

In Available roles of Realm roles, select test-student and press Add selected


Now create second user for admin .

Set Username to admin-1


After saving the admin user, set credential for user admin-1

Set password -> admin123 and switch off temporary password


From Role Mapping tab, assign test-admin realm to user.


Below diagram gives the complete understanding about model. Every Realm can have one or more client and each client can have multiple users.


Keycloak-configuration-model
Generate Tokens for Users
Go to Realm settings and click on OpenID Endpoint Configuration to view OpenID Endpoint details.


It will show you the application related endpoint.

To generate a token select token-endpoint


Copy the above highlighted url and create a POST request in POSTMAN to get access token.

Add the following detail in x-www-form-urlencoded

client_id -> app-client

username -> admin-1

password -> admin123

grant_type -> password

Click on Clients from the sidebar and get client_secret.



Curl Command

curl –location –request POST ‘http://localhost:8080/auth/realms/test-realm/protocol/openid-connect/token’ \
–header ‘Content-Type: application/x-www-form-urlencoded’ \
–data-urlencode ‘client_id=app-client’ \
–data-urlencode ‘client_secret=a4c29854-acad-423a-90a7-cec27032443a’ \
–data-urlencode ‘username=admin-1’ \
–data-urlencode ‘password=admin123’ \
–data-urlencode ‘grant_type=password’

Create a spring boot project.
Prerequisite

Java 11
Spring Boot 2.5.6 stable version
Add Spring Web and Spring Security dependency

Now open your pom.xml in your IDE.

Add the following dependency under dependencies section.

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
    <version>15.0.2</version>
</dependency>

Then add the following dependency in dependency management.

<dependency>
    <groupId>org.keycloak.bom</groupId>
    <artifactId>keycloak-adapter-bom</artifactId>
    <version>15.0.2</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
After adding the dependencies your pom file should look this.

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>keyCloakSpringBoot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>keyCloakSpringBoot</name>
    <description>keyCloakSpringBoot</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-spring-boot-starter</artifactId>
            <version>15.0.2</version>
        </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>15.0.2</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>
Add the following properties in your spring boot project application.properties file

server.port=8181

keycloak.realm=test-realm
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.ssl-required= external
keycloak.resource=app-client
keycloak.credentials.secret=a4c29854-acad-423a-90a7-cec27032443a
keycloak.use-resource-role-mappings = true
keycloak.bearer-only= true

Create a package dto. Add the following class

Student.java
package com.example.keycloakspringboot.dto;

public class Student {
    private String name;
    private String age;
    private String semester;

    public Student(String name, String age, String semester) {
        this.name = name;
        this.age = age;
        this.semester = semester;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }

    public String getSemester() {
        return semester;
    }

    public void setSemester(String semester) {
        this.semester = semester;
    }
}
Create a package named controller.

UserController.java

package com.example.keycloakspringboot.controller;

import com.example.keycloakspringboot.dto.Student;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping(value = "/access-by-all")
    public ResponseEntity<String> getAccess() {
        return ResponseEntity.ok("This endpoint is not authenticated ");
    }

    @GetMapping(value = "/student")
    public ResponseEntity<Student> getStudent() {

        Student student=new Student("Emily","16","1");

        return  ResponseEntity.ok(student);
    }

    @GetMapping(value = "/all-students")
    public ResponseEntity<List<Student>> getAllStudents() {

        List<Student> studentList=new ArrayList<>();

        studentList.add(new Student("Emily","16","1"));
        studentList.add(new Student("John","18","2"));
        studentList.add(new Student("Sam","15","1"));

        return ResponseEntity.ok(studentList);
    }
}
SecurityConfig.java

In Security Config class, we are extending KeycloakWebSecurityConfigurerAdapter as a convenient base class for creating WebSecurityConfigurer Instance

By default, Spring Security Adapter looks for configuration file keycloak.json .So by adding keycloakConfigResolver bean it will look at the configuration provided by Spring Boot Adapter.

RegisterSessionAuthenticationStrategy bean register user session after authentication.

SimpleAuthorityMapper in configure global method make sure that role are not prefixed with ROLE_

package com.example.keycloakspringboot.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
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.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);

        http
                .authorizeRequests()

                .antMatchers("/user/all-students").hasRole("admin")
                .antMatchers("/user/access-by-all").permitAll()
                .anyRequest().authenticated();

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

        KeycloakAuthenticationProvider keycloakAuthenticationProvider = new          
                                     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();
    }
}
users with admin Role are only authorized to access this endpoint /user/all-students.

Endpoint /user/access-by-all is accessible without any auth token.

Run Spring Boot Project.

Test Application

In our application, we have not authenticated this endpoint http://localhost:8181/user/access-by-all. So it should work without token


Next we will access the authenticated endpoint http://localhost:8181/user/all-students


Now get the authorization access token through admin-1 user and add it to your authorization header as shown in the fig below.


Now get the access token through student-1 user.


Add this token in Authorization bearer and access this url http://localhost:8181/user/all-students. It will send you a forbidden response. Because the user with admin role is only authorized to access this endpoint.

Access this url http://localhost:8181/user/student from student-1  token.


Access this url http://localhost:8181/user/student from student-1  token.