Monday, June 6, 2022

 

[Practice] - Secure Spring Boot REST APIs using Keycloak

This tutorial walks you through the steps of securing Spring Boot REST APIs using Keycloak.

Keycloak is an open source Identity and Access Management tool that uses standard protocols such as OAuth 2.0, OpenID Connect, and SAML to secure web applications and web services.

In this example, we will build a simple Spring Boot Application and integrate the application with Keycloak to protect the REST APIs from unauthorized calls. You will also learn to create users programmatically in Keycloak, login and generate JWT token to access the secured REST APIs.

Follow the steps below to complete this example:

Set Up Keycloak

The first step will be to download, setup, and run the Keycloak Server. If you have already set up the Keycloak server then continue from step two. Otherwise, go to our Keycloak quickstart tutorial to set up the Keycloak Server for using it with this example.

Create a Spring Boot Application

  1. Go to Spring Initializr at https://start.spring.io and create a Spring Boot application with details as follows:
    • Project: Choose Gradle Project or Maven Project.
    • Language: Java
    • Spring Boot: Latest stable version of Spring Boot is selected by default. So leave it as is.
    • Project Metadata: Provide group name in the Group field. The group name is the id of the project. In Artifact field, provide the name of your project. In the package field, provide package name for your project. Next, select your preferred version of Java that is installed on your computer and is available on your local environment.
    • Dependencies: Add dependencies for Spring WebSpring Boot DevTools, and Spring Security.

    Refer to the image below for example:

  2. Click the GENERATE button and save/download the project zip bundle.
  3. Extract the project to your preferred working directory.
  4. Import the project to your preferred Java development IDE such as Eclipse or IntelliJ IDEA.

Add Keycloak Spring Dependencies

Add Keycloak Spring Boot Starter, and Keycloak Admin REST Client dependencies to your application. The Keycloak Spring Boot Starter takes benefit of Spring Boot's auto-configuration and Keycloak Admin REST Client provides Keycloak admin functionalities which will help to create users programmatically in Keycloak.

For Gradle

Add the following dependencies to the build.gradle file:

keycloak-sample/build.gradle
implementation group: 'org.keycloak', name: 'keycloak-spring-boot-starter', version: '12.0.4'
implementation group: 'org.keycloak', name: 'keycloak-admin-client', version: '12.0.4'

For Maven

Add the following dependencies to the pom.xml file:

keycloak-sample/pom.xml
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
    <version>12.0.4</version>
</dependency>
<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-admin-client</artifactId>
    <version>12.0.4</version>
</dependency>

Find the latest version of keycloak-spring-boot-starter in the Maven Repository .

You can find the latest version of Keycloak Admin REST Client in the Maven Repository .

Add Application Configurations

The Keycloak Spring Boot needs some extra configurations which can be added via Spring Boot configuration properties file. Add the following configuration properties in the application.properties file and do not forget to replace the values of server.portkeycloak.realmkeycloak.resource with values that is relevant to your project:

src/main/resources/application.properties
#port on which the application would run
server.port = 8081
keycloak.realm = tutorialsbuddy-example
keycloak.auth-server-url = http://localhost:8080/auth
keycloak.ssl-required = external
#keycloak resource is the client ID
keycloak.resource = tutorialsbuddy-app
keycloak.use-resource-role-mappings = true
#The line below will prevent redirect to login page
keycloak.bearer-only: true
server.connection-timeout=6000

Create a SecurityConfig class

This SecurityConfig class file must extend the KeycloakWebSecurityConfigurerAdapter abstract class. KeycloakWebSecurityConfigurerAdapter is a convenient base class provided by Keycloak for creating a WebSecurityConfigurer instance.

The SecurityConfig class must be annotated with the following annotations:

  • @Configuration - This annotation indicates that the class is a configuration class containing bean definitions for the application context.
  • @EnableWebSecurity - This annotation indicates that the class is a Spring Security configuration with information telling how to authenticate users. It provides security configuration via HttpSecurity which is provided as a method parameter in a method called configure and allows you to configure accessibility based on the url-patterns, handlers and authentication endpoints.
  • @EnableGlobalMethodSecurity(prePostEnabled = true) - This annotation enables Spring Security global method security. The use of prePostEnabled = true enables @PreAuthorize and @PostAuthorize annotations.
  • @ComponentScan(basePackageClasses = KeycloakSecurityComponents.class) - The @ComponentScan tells Spring to scan the packages assigned to basePackageClasses .
src/main/java/com/keycloak/sample/config/SecurityConfig.java
package com.keycloak.sample.config;

import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticatedActionsFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter;
import org.keycloak.adapters.springsecurity.management.HttpSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

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

        http.cors().and().csrf().disable().sessionManagement().

                sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
                .antMatchers("/users/unprotected-data").permitAll()
                .antMatchers("/users/create").permitAll()
                .antMatchers("/users/signin").permitAll()
                .anyRequest().authenticated();

    }

    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {

        /**
         * Returning NullAuthenticatedSessionStrategy means app will not remember session
         */

        return new NullAuthenticatedSessionStrategy();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider =
                keycloakAuthenticationProvider();

        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());

        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    public FilterRegistrationBean<?> keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {

        FilterRegistrationBean<?> registrationBean = new FilterRegistrationBean<>(filter);

        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<?> keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {

        FilterRegistrationBean<?> registrationBean = new FilterRegistrationBean<>(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<?> keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {

        FilterRegistrationBean<?> registrationBean = new FilterRegistrationBean<>(filter);

        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean<?> keycloakSecurityContextRequestFilterBean(
            KeycloakSecurityContextRequestFilter filter) {

        FilterRegistrationBean<?> registrationBean = new FilterRegistrationBean<>(filter);

        registrationBean.setEnabled(false);

        return registrationBean;
    }

    @Bean
    @Override
    @ConditionalOnMissingBean(HttpSessionManager.class)
    protected HttpSessionManager httpSessionManager() {
        return new HttpSessionManager();
    }
}

Create a Data Transfer Object

Create a UserDTO.java Java class. This class should only contains getter/setter methods with serialization and deserialization mechanism but should not contain any business logic.

src/main/java/com/keycloak/sample/dto/UserDTO.java
package com.keycloak.sample.dto;

public class UserDTO {
    private String email;
    private String password;
    private String firstname;
    private String lastname;
    private int statusCode;
    private String status;

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFirstname() {
        return firstname;
    }

    public void setFirstname(String firstname) {
        this.firstname = firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public void setLastname(String lastname) {
        this.lastname = lastname;
    }


    public int getStatusCode() {
        return statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

}

Create a Web Controller

Create a simple web controller with the following REST methods:

  • /users/create - to create new users.
  • /users/signin - to authenticate and retrieve access token.
  • /users/unprotected-data - this method is not protected by Keycloak so anybody can access it.
  • /users/protected-data - this method is protected by Keycloak and can be accessed only by using a valid user access token.
src/main/java/com/keycloak/sample/controller/UserController.java
package com.keycloak.sample.controller;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.Response;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.CreatedResponseUtil;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.keycloak.sample.dto.UserDTO;


@RequestMapping(value = "/users")
@RestController
public class UserController {

    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    private String authServerUrl = "http://localhost:8080/auth";
    private String realm = "tutorialsbuddy-example";
    private String clientId = "tutorialsbuddy-app";
    private String role = "student";
    //Get client secret from the Keycloak admin console (in the credential tab)
    private String clientSecret = "7b768120-ef4e-4100-8c99-85bb9d4dc5c3";

    @PostMapping(path = "/create")
    public ResponseEntity<?> createUser(@RequestBody  UserDTO userDTO) {

        Keycloak keycloak = KeycloakBuilder.builder().serverUrl(authServerUrl)
                .grantType(OAuth2Constants.PASSWORD).realm("master").clientId("admin-cli")
                .username("admin").password("Testing123")
                .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(10).build()).build();

        keycloak.tokenManager().getAccessToken();


        UserRepresentation user = new UserRepresentation();
        user.setEnabled(true);
        user.setUsername(userDTO.getEmail());
        user.setFirstName(userDTO.getFirstname());
        user.setLastName(userDTO.getLastname());
        user.setEmail(userDTO.getEmail());

        // Get realm
        RealmResource realmResource = keycloak.realm(realm);
        UsersResource usersRessource = realmResource.users();

        Response response = usersRessource.create(user);

        userDTO.setStatusCode(response.getStatus());
        userDTO.setStatus(response.getStatusInfo().toString());

        if (response.getStatus() == 201) {

            String userId = CreatedResponseUtil.getCreatedId(response);

            log.info("Created userId {}", userId);


            // create password credential
            CredentialRepresentation passwordCred = new CredentialRepresentation();
            passwordCred.setTemporary(false);
            passwordCred.setType(CredentialRepresentation.PASSWORD);
            passwordCred.setValue(userDTO.getPassword());

            UserResource userResource = usersRessource.get(userId);

            // Set password credential
            userResource.resetPassword(passwordCred);

            // Get realm role student
            RoleRepresentation realmRoleUser = realmResource.roles().get(role).toRepresentation();

            // Assign realm role student to user
            userResource.roles().realmLevel().add(Arrays.asList(realmRoleUser));
        }
        return ResponseEntity.ok(userDTO);
    }

    @PostMapping(path = "/signin")
    public ResponseEntity<?> signin(@RequestBody  UserDTO userDTO) {

        Map<String, Object> clientCredentials = new HashMap<>();
        clientCredentials.put("secret", clientSecret);
        clientCredentials.put("grant_type", "password");

        Configuration configuration =
                new Configuration(authServerUrl, realm, clientId, clientCredentials, null);
        AuthzClient authzClient = AuthzClient.create(configuration);

        AccessTokenResponse response =
                authzClient.obtainAccessToken(userDTO.getEmail(), userDTO.getPassword());

        return ResponseEntity.ok(response);
    }


    @GetMapping(value = "/unprotected-data")
    public String getName() {
        return "Hello, this api is not protected.";
    }


    @GetMapping(value = "/protected-data")
    public String getEmail() {
        return "Hello, this api is protected.";
    }

}

To get client secret, login to your Keycloak realm admin console at http://localhost:8080/auth/admin and go to the Clients settings page under the Credentails tab as shown in the image below:

Create keycloakConfigResolver Reference Method

Create a keycloakConfigResolver reference method in the Spring Boot main application class. This method must be annotated with @Bean annotation and also must return an instance of KeycloakSpringBootConfigResolver class. The use of this keycloakConfigResolver reference will make the application to use Spring Boot configuration properties file instead of the Keycloak default keycloak.json file.

src/main/java/com/keycloak/sample/KeycloakSampleApplication.java
package com.keycloak.sample;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class KeycloakSampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(KeycloakSampleApplication.class, args);
    }

    //Creating bean keycloakConfigResolver
    @Bean
    public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }
}

Run the Application

Run the application and do the following tests:

1. Sign up/Create a user using POST request via any HTTP requesting Tool as shown in the image below:

2. Retrieve Access token by signing in - Make a sign-in POST request using the email and password of the as shown in the image below:

3. After you received a token in login response. Call the secured REST API. Example shown in the image below:

4. You can also retrieve access token directly from the Keycloak Server

Summary

Congratulations! you have learned how to integrate Spring Boot REST APIs with Keycloak.

Note: When deploying Keycloak in the Production environment, it is recommnended to choose an operating mode between Standalone and Domain mode. In Production environment, you may also need to configure an external shared database like PostgreSQL, MySQL, Oracle for Keycloak storage to run in a cluster and also configure securities such as encryption and https. To learn how to configure Keycloak with MySQL database, we recommend you to read Keycloak with MySQL

No comments:

Post a Comment

So sánh các GitFlow model và áp dụng với CICD

https://medium.com/oho-software/so-s%C3%A1nh-c%C3%A1c-gitflow-model-v%C3%A0-%C3%A1p-d%E1%BB%A5ng-v%E1%BB%9Bi-cicd-b6581cfc893a