avatarIvan Franchin

Summary

The provided content is a comprehensive tutorial on securing a "StarVote" Spring Boot application using Keycloak or Okta as an Identity Provider (IdP), which includes adding security dependencies, creating login pages, updating UI components, and configuring Web Security settings.

Abstract

The article "Building a Single Spring Boot App with Keycloak or Okta as IdP: Adding Security" offers a step-by-step guide on how to secure the StarVote application, a Spring Boot application, using either Keycloak or Okta as an Identity Provider. It begins by outlining the necessary steps to add security features, including the addition of specific dependencies such as spring-boot-starter-oauth2-client and spring-boot-starter-oauth2-resource-server in the pom.xml file. The tutorial then instructs on creating a login.html page and modifying existing UI components like header.html and stars-list.html to integrate security features. It also covers the implementation of security configurations through the WebSecurityConfig class, which involves setting up security filter chains, OAuth2 login configurations, and JWT token validation. The article acknowledges potential startup failures and promises to address these issues in subsequent articles, which will delve into enabling Keycloak or Okta as the IdP. The tutorial series aims to provide a thorough understanding of implementing a secure single Spring Boot application.

Opinions

  • The author emphasizes the importance of securing applications and provides clear instructions on integrating OAuth2 security with Spring Boot.
  • The tutorial is designed to be accessible to developers familiar with Spring Boot, Thymeleaf, and Spring Security, with the expectation that readers will follow along and implement the security measures

Spring Boot | Star Vote

Building a Single Spring Boot App with Keycloak or Okta as IdP: Adding Security

A step-by-step guide on how to secure the StarVote application

Photo by Spenser H on Unsplash

This article is part of a series that explores the implementation of a Single Spring Boot application called StarVote. The application will use Keycloak or Okta as Identity Provider.

In the introductory article, we outline the sections we will cover:

Here’s a sneak peek of how the StarVote application will be at the end!

In this particular article, we will explore the necessary steps to secure the application. This includes adding the required dependencies, making UI modifications, and configuring the Web Security settings.

So, let’s get started!

Adding Security Dependencies

In pom.xml, add the following dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
  • The spring-boot-starter-oauth2-client dependency provides the necessary components to enable OAuth 2.0 client functionality in the Spring Boot application;
  • The spring-boot-starter-oauth2-resource-server dependency allows the Spring Boot application to act as an OAuth 2.0 resource server, validating and processing incoming requests from OAuth 2.0 clients;
  • The thymeleaf-extras-springsecurity6 dependency is an extension for Thymeleaf that integrates with Spring Security version 6, providing additional features and utilities for secure web application development.

Implementing Security

Create the login.html

In the resources/templates folder, create the login.html file with the following content:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>StarVote</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.css">
</head>

<div th:insert="~{header :: header}"></div>

<body>

<div class="ui center aligned basic segment">
    <h2 class="ui center aligned icon header">
        <i class="circular users icon"></i>
        Sign in
    </h2>
    <div class="ui compact labeled icon menu">
        <a class="item">
            <i class="openid alternate icon"></i>
            OpenID
        </a>
    </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.5.0/semantic.min.js"></script>

</body>
</html>

The login.html page features an <a> tag for the Identity Provider that users can use to log in to the StarVote application. In the upcoming tutorials, we will provide the URL links to be included in the href attribute of the <a> tags.

Update the header.html

Let’s apply the following changes (highlighted in bold) to the header.html file.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<header>
    <div class="ui massive menu">
        <div class="item">
            <i class="video camera icon"></i>
            StarVote
        </div>
        <a class="item" th:href="@{/}">
            Home
        </a>
        <a class="item" th:href="@{/add-stars}"
           sec:authorize="hasRole('STAR-VOTE-ADMIN')">
            Add Star
        </a>
        <a class="item" th:href="@{/stars-list}"
           sec:authorize="hasAnyRole('STAR-VOTE-ADMIN','STAR-VOTE-USER')">
            Star's List
        </a>
        <a class="item" th:href="@{/stars-rank}">
            Star's Rank
        </a>
        <div class="right menu">
            <a class="ui item" th:href="@{/login}" sec:authorize="!isAuthenticated()">
                Login
            </a>
            <a class="ui item" sec:authentication="name" sec:authorize="isAuthenticated()">
            </a>
            <a class="ui item" th:href="@{/logout}" sec:authorize="isAuthenticated()">
                Logout
            </a>
        </div>
    </div>
    <div class="ui divider"></div>
</header>
</body>
</html>
  • The xmlns:sec="http://www.thymeleaf.org/extras/spring-security" XML namespace declaration has been added to enable the usage of Thymeleaf-Spring Security tags;
  • The sec:authorize="hasRole('STAR-VOTE-ADMIN')" attribute is added to the "Add Star" menu item, allowing only individuals with the role STAR-VOTE-ADMIN to see and access this link;
  • The sec:authorize="hasAnyRole('STAR-VOTE-ADMIN','STAR-VOTE-USER')" attribute is added to the "Star's List" menu item, allowing individuals with either the role STAR-VOTE-ADMIN or STAR-VOTE-USER to see and access this link;
  • Inside the right-aligned portion of the menu, a new <a> tag has been added to display the authenticated user's name. It uses the sec:authentication=”name” as the text value to display the name of the authenticated user.

Update the stars-list.html

Let’s go to the stars-list.html file and apply the following changes (highlighted in bold).

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
...

<main>
    ...
    <div class="ui container">
        <div class="ui basic segment">
            <div class="ui five stackable doubling centered cards">
                <div class="card" th:each="star:${stars}">
                    ...
                    <div class="extra content">
                        <div class="ui large circular black label">
                            Votes
                            <div class="detail" th:text="${star.votes}">214</div>
                        </div>
                        <a class="ui right floated primary basic blue button"
                           th:href="@{/vote-stars/{id}(id = ${star.id})}"
                           sec:authorize="hasRole('STAR-VOTE-USER')">Vote</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</main>

...
</body>
</html>
  • Added the Thymeleaf-Spring Security XML namespace declaration, xmlns:sec="http://www.thymeleaf.org/extras/spring-security";
  • Added the sec:authorize="hasRole(‘STAR-VOTE-USER’)" attribute to the <a> tag for voting, specifying that only users with the role STAR-VOTE-USER can see and interact with the "Vote" button.

Update the StarUIController class

Open StarUIController class and add the following method:

...
public class StarUIController {
   ...

    @GetMapping("/login")
    public String login() {
        return "login";
    }
...
}

This login() method is responsible for handling the GET request to the “/login” endpoint and returning the “login” view.

Create the WebSecurityConfig class

In security package, let’s create the WebSecurityConfig class with the following content:

package com.example.singlestarvoteapp.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    private static final String STAR_VOTE_ADMIN = "STAR-VOTE-ADMIN";
    private static final String STAR_VOTE_USER = "STAR-VOTE-USER";

    @Value("${jwt.auth.converter.principal-attribute:sub}")
    private String principalAttribute;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                        .requestMatchers(HttpMethod.GET, "/api/stars").hasAnyRole(STAR_VOTE_ADMIN, STAR_VOTE_USER)
                        .requestMatchers(HttpMethod.POST, "/api/stars").hasRole(STAR_VOTE_ADMIN)
                        .requestMatchers(HttpMethod.GET, "/", "/login", "/stars-rank").permitAll()
                        .requestMatchers("/oauth2/**").permitAll()
                        .anyRequest().authenticated())
                .oauth2Login(oauth2Login -> oauth2Login.loginPage("/login").defaultSuccessUrl("/"))
                .oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer.jwt(
                        jwt -> jwt.jwtAuthenticationConverter(jwtAbstractAuthenticationTokenConverter())))
                .logout(logout -> logout.logoutSuccessUrl("/").permitAll())
                .csrf(AbstractHttpConfigurer::disable)
                .build();
    }

    @Bean
    public OAuth2UserService<OidcUserRequest, OidcUser> oAuth2UserService(@Autowired JwtDecoder jwtDecoder) {
        OidcUserService delegate = new OidcUserService();
        return (userRequest) -> {
            OidcUser oidcUser = delegate.loadUser(userRequest);

            Jwt jwt = jwtDecoder.decode(userRequest.getAccessToken().getTokenValue());
            Collection<? extends GrantedAuthority> authorities = extractRoles(jwt);

            return new DefaultOidcUser(authorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), principalAttribute);
        };
    }

    private Converter<Jwt, AbstractAuthenticationToken> jwtAbstractAuthenticationTokenConverter() {
        return new Converter<>() {
            private static final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter =
                    new JwtGrantedAuthoritiesConverter();

            @Override
            public AbstractAuthenticationToken convert(Jwt jwt) {
                Collection<GrantedAuthority> authorities =
                        Stream.concat(
                                jwtGrantedAuthoritiesConverter.convert(jwt).stream(),
                                extractRoles(jwt).stream()
                        ).collect(Collectors.toSet());
                return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
            }
        };
    }

    private String getPrincipalClaimName(Jwt jwt) {
        String claimName = principalAttribute == null ? JwtClaimNames.SUB : principalAttribute;
        return jwt.getClaim(claimName);
    }

    // TODO: The implementation will be covered in the upcoming articles
    private Collection<? extends GrantedAuthority> extractRoles(Jwt jwt) {
        return Collections.emptySet();
    }
}

Here’s an overview of the WebSecurityConfig class:

  • Annotated with @Configuration and @EnableWebSecurity, indicating that this class is a configuration class for web security;
  • Defines two constants: STAR_VOTE_ADMIN and STAR_VOTE_USER, representing the role names used in the application.
  • The @Value annotation is used to retrieve the value of a configuration property named jwt.auth.converter.principal-attribute. This value is assigned to the variable principalAttribute in the class. If the property is not defined, the variable will be assigned a default value of "sub".
  • Contains a securityFilterChain method, which configures the security filter chain for the application using the provided HttpSecurity object. This method defines various security rules and configurations, such as authorization rules for different endpoints, login configuration, JWT token validation, logout configuration, and disabling CSRF protection.
  • Defines a oAuth2UserService bean, which configures the OAuth2 user service used for loading user information from the OIDC provider. It overrides the default behavior of OidcUserService to customize the user details returned.
  • Provides a jwtAbstractAuthenticationTokenConverter method, which returns a converter that converts a JWT token to an AbstractAuthenticationToken. This converter extracts the granted authorities from the JWT and combines them with the resource roles obtained from the JWT to create the authentication token.
  • Contains helper methods, such as extractRoles and getPrincipalClaimName, which are used to extract information from the JWT token. The implementation of the extractRoles method will be covered in the upcoming articles, as it is specific to how Keycloak and Okta construct the JWT token payload.

StarVote application fails to start!

If you attempt to start the StarVote application after implementing the changes outlined in this article, you may encounter an exception, as shown below:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of method setFilterChains in org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration required a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' in your configuration.

Don’t panic! We will solve it in the next articles.

Up Next

We will delve into enabling Keycloak as the Identity Provider for the StarVote application.

However, if you prefer to use Okta instead, you can directly proceed to the article that explains how to enable Okta as the Identity Provider.

We hope you are enjoying the StarVote Tutorial series!

Support and Engagement

If you enjoyed this article and would like to show your support, please consider taking the following actions:

  • 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
  • 🌐 Share my story on Social Media;
  • 🔔 Follow me on: Medium | LinkedIn | Twitter | GitHub;
  • ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.
Spring Boot
Keycloak
Okta
Technology
Software Development
Recommended from ReadMedium