Introduction

What you will learn

In this tutorial you will learn
  • What hashing is

  • The theory behind Spring Security

    • Blowfish

    • BCrypt

    • JWT

  • Implement a realm (User, Authority) in an MySQL database for usage in a secured application

  • Implementing Spring Security in a REST api

Conventions
  • <username> means username is a variable and you have to replace <username> to a username like jdoe. So DO REMOVE THE TRIANGLE BRACKETS

  • $ before a line means that a terminal / windows command is invoked.

    • to invoke that line start your Linux terminal, or ⇒

    • to invoke that line start your Windows command shell (programm: cmd)

  • mysql> before a line means that you should invoke that command using your MySQL client

Roadmap and Steps
  • I prefer to use a roadmap and steps during this document

    • Roadmap: a coarse grained way (not very detailed) way to accomplish something

    • Steps: a find grained way (very detailed) way to accomplish something. (these steps should be executed to follow along)

Definitions

Hashing

Hashing is a way to encrypt a string (plain text) in a way that the resulting encryped text (cypher text) cannot be reverted to the original text. It is a one way traffic

Encryption

Hashing where there is a kind of key which makes the reverting from cypher text to plain text possible. One way, trap door.

Authentication - validation of saying who you are
  • Weak

    • Password

  • Strong

    • Passport

Authorization - expressing what you as a logged in user are authorized to do
  • Role

  • Authority (we will be used this)

Confidentiality
  • Encrypting text to be sure that no one can read the text which is send

Integrity
  • Encrypting text to be sure that no one can change the text which is send

Start Learning Spring Security

Topic: Blowfish

Introduction

In this section you will learn the basics of Blowfish, an algorithm which is used to hash a password stored to the database

What you will learn

In the next section of this tutorial you will learn
  • What Blowfish is

  • A basic understanding of how it is used

  • How to implement Blowfish in an application

Why: Blowfish

A better question is: "Why hash a password for a user in a database"

We use Blowfish during this tutorial to hash a password in a database. The reason is that reading a password directly from a database is a security leak.

When: Blowfish

You can use Blowfish when you have to hash a password and it is not necessary to revert the process

What: Blowfish

Blowfish is a symmetric-key block cipher, designed in 1993 by Bruce Schneier and included in many cipher suites and encryption products. Blowfish provides a good encryption rate in software and no effective cryptanalysis of it has been found to date.

How: Blowfish

The algorithm

Blowfish has a 64-bit block size and a variable key length from 32 bits upto 448 bits.[3] It is a 16-round Feistel cipher and uses large key-dependent S-boxes. In structure it resembles CAST-128, which uses fixed S-boxes.

For more detail: Read this wikipedia article

The article contains a lot of complex technical details

Conclusion

During this section we learned the basic principles of Blowfish

In the following section we will learn that we can use the Blowfish algorith with BCrypt.

Follow-up: Blowfish

Below you find for this topic some extra resources to watch after the week the topic Blowfish is introduced during the training

Topic: Bcrypt

Introduction

In the previous section we learned the basics of Blowfish since that is the hashing algorithm we will be using.

The Blowfish algorith is used in Bcrypt which is the subject of this section.

What you will learn

In the next section of this tutorial you will learn
  • What Bcrypt offers

  • How we use Bcrypt

What: Bcrypt

bcrypt is a password hashing function designed by Niels Provos and David Mazières, based on the Blowfish cipher, and presented at USENIX in 1999.[1] Besides incorporating a salt to protect against rainbow table attacks, bcrypt is an adaptive function: over time, the iteration count can be increased to make it slower, so it remains resistant to brute-force search attacks even with increasing computation power.

The bcrypt function is the default password hash algorithm for OpenBSD[2] and other systems including some Linux distributions such as SUSE Linux.[3]

There are implementations of bcrypt for C, C++, C#, Go,[4] Java,[5][6] JavaScript,[7] Elixir,[8] Perl, PHP, Python,[9] Ruby and other languages.

Background

Blowfish is notable among block ciphers for its expensive key setup phase. It starts off with subkeys in a standard state, then uses this state to perform a block encryption using part of the key, and uses the result of that encryption (which is more accurate at hashing) to replace some of the subkeys. Then it uses this modified state to encrypt another part of the key, and uses the result to replace more of the subkeys. It proceeds in this fashion, using a progressively modified state to hash the key and replace bits of state, until all subkeys have been set.

Provos and Mazières took advantage of this, and took it further. They developed a new key setup algorithm for Blowfish, dubbing the resulting cipher "Eksblowfish" ("expensive key schedule Blowfish"). The key setup begins with a modified form of the standard Blowfish key setup, in which both the salt and password are used to set all subkeys. There are then a number of rounds in which the standard Blowfish keying algorithm is applied, using alternatively the salt and the password as the key, each round starting with the subkey state from the previous round. In theory, this is no stronger than the standard Blowfish key schedule, but the number of rekeying rounds is configurable; this process can therefore be made arbitrarily slow, which helps deter brute-force attacks upon the hash or salt.

Description

The prefix "$2a$" or "$2b$" (or "$2y$") in a hash string in a shadow password file indicates that hash string is a bcrypt hash in modular crypt format.[10] The rest of the hash string includes the cost parameter, a 128-bit salt (Radix-64 encoded as 22 characters), and 184 bits of the resulting hash value (Radix-64 encoded as 31 characters).[11] The Radix-64 encoding uses the unix/crypt alphabet, and is not 'standard' Base-64.[12][13] The cost parameter specifies a key expansion iteration count as a power of two, which is an input to the crypt algorithm.

For example, the shadow password record $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy specifies a cost parameter of 10, indicating 210 key expansion rounds. The salt is N9qo8uLOickgx2ZMRZoMye and the resulting hash is IjZAgcfl7p92ldGxad68LJZdL17lhWy. Per standard practice, the user’s password itself is not stored.

Versioning history

$2$ (1999)

The original Bcrypt specification defined a prefix of $2$.

This follows the Modular Crypt Format format used when storing passwords in the OpenBSD password file:
  • $1$: MD5-based crypt ('md5crypt')

  • $2$: Blowfish-based crypt ('bcrypt')

  • $sha1$: SHA-1-based crypt ('sha1crypt')

  • $5$: SHA-256-based crypt ('sha256crypt')

  • $6$: SHA-512-based crypt ('sha512crypt')

$2a$

The original specification did not define how to handle non-ASCII character, nor how to handle a null terminator.

The specification was revised to specify that when hashing strings:
  • the string must be UTF-8 encoded

  • the null terminator must be included With this change, the version was changed to $2a$

$2x$, $2y$ (June 2011)

In June 2011, a bug was discovered in crypt_blowfish, a PHP implementation of BCrypt. It was mis-handling characters with the 8th bit set. They suggested that system administrators update their existing password database, replacing $2a$ with $2x$, to indicate that those hashes are bad (and need to use the old broken algorithm). They also suggested the idea of having crypt_blowfish emit $2y$ for hashes generated by the fixed algorithm.

Nobody else, including canonical OpenBSD, adopted the idea of 2x/2y. This version marker change was limited to crypt_blowfish.

$2b$ (February 2014)

A bug was discovered in the OpenBSD implementation of bcrypt. They were storing the length of their strings in an unsigned char (i.e. 8-bit Byte).

If a password was longer than 255 characters, it would overflow and wrap at 255.

BCrypt was created for OpenBSD. When they had a bug in their library, they decided to bump the version number.

Conclusion

During this section we learned a very basic understanding of Bcrypt.

In the upcoming sections we will make use of them in the secure Spring Boot application for storing a password to a database

Resources: Bcrypt

Below you find for this topic some extra resources to read and watch after the week the topic Bcrypt is introduced during the training

Straight from ChatGPT

Hashing is a process of converting a plain text password or message into a fixed-length code that is unique and virtually irreversible.

A hash function takes the plain text as input and generates a fixed-size string of characters that represents the original input data. The output of a hash function is referred to as a hash value or a digest.

One important property of a good hash function is that it is one-way, meaning that it is virtually impossible to reverse engineer the original input from the hash value. A good hash function is also deterministic, meaning that given the same input, it always produces the same output.

Salt is an additional piece of data that is added to the input of a hash function to make it more difficult to crack the hash. Salt is a random value that is generated and appended to the input data before the hash function is applied. The salt value is usually stored alongside the hashed value in a database.

When a user attempts to log in, the salt value is retrieved from the database and added to the input data before the hash function is applied. This makes it much more difficult for attackers to use precomputed hash tables or rainbow tables to crack the hash. The use of salt is an important security measure to protect against various types of attacks, such as dictionary attacks, where an attacker uses a precomputed list of common passwords to try and crack the hash.

Salt ensures that even if two users have the same password, their hash values will be different because the salt values will be different.

Overall, the use of salt and hashing is an important practice in securing sensitive information, such as user passwords, and preventing unauthorized access.

Links

Topic: JWT

Introduction

During this section you will learn everything you need to know regarding JWT to use it in a secure Spring Boot application.

What you will learn

In the next section of this tutorial you will learn
  • Why we have to use JWT

  • What it is

  • How to use it

What: JWT

JSON Web Token (JWT]) is an Internet standard for creating JSON-based access tokens that assert some number of claims.

For example, a server could generate a token that has the claim "logged in as admin" and provide that to a client. The client could then use that token to prove that it is logged in as admin.

The tokens are signed by one party’s private key (usually the server’s), so that both parties (the other already being, by some suitable and trustworthy means, in possession of the corresponding public key) are able to verify that the token is legitimate. The tokens are designed to be compact, URL-safe, and usable especially in a web-browser single-sign-on (SSO) context.

JWT claims can be typically used to pass identity of authenticated users between an identity provider and a service provider, or any other type of claims as required by business processes.

Diagram showing the content of a JWT

jwt 2

How: JWT

Summary of this link
  • A JWT is send in the Authorization header of the request and starts with Bearer (a space after Bearer) and then the token

    • Example: Bearer ey2…​..

  • Claims, see below ⇒

Claims explained

JSON Web Token (JWT) claims are pieces of information asserted about a subject.

For example, an ID Token (which is always a JWT) may contain a claim called name that asserts that the name of the user authenticating is "John Doe".

In a JWT, a claim appears as a name/value pair where the name is always a string and the value can be any JSON value.

Generally, when we talk about a claim in the context of a JWT, we are referring to the name (or key). For example, the following JSON object contains three claims (sub, name, admin):

Resources: JWT

Straight from ChatGPT

The signature in a JWT is located in the last part of the token, after the header and payload.

To be more specific, a JWT is made up of three parts separated by dots: The header The payload The signature The header and payload are base64-encoded JSON strings that contain information about the token and the claims being made.

The signature is generated by taking the encoded header and payload, and signing them with a secret key that is only known to the server. This signature is also base64-encoded and appended to the end of the token, separated by another dot.

When a client sends a JWT to a server, the server can verify the signature by re-generating it using the same secret key and comparing it to the signature in the token. If the signatures match, it means that the token has not been tampered with and can be trusted.

The signature is an essential part of JWTs because it ensures the integrity and authenticity of the token. Without the signature, anyone could modify the contents of a JWT and impersonate another user or gain unauthorized access to protected resources.

Topic: CORS

Introduction

During this small section we will introduce some small items regarding CORS

What you will learn

In the next section of this tutorial you will learn
  • What CORS is

  • How to protect it

  • How to use it in a JavaScript / Angular application

Why and How: CORS

Read this

Using CORS with Spring Boot REST Controller

Add the annotation @CrossOrigin to the class of your controller

When using Java EE (JAX-RS) the @CrossOrigin annotation will not rock. You have to resort to create a Java EE filter. See Tips and Tricks

Congratulations: You have now setup a complete user and authority realm to continue implementing Security!!!

Start Implementing Spring Security

for using spring security we need three types for implementing authentication and authorization
  • user - for modelling a logged in user

  • authority - for modelling the roles we have in our application

  • enum authorityname - for expressing the role names

Add enum AuthorityName

Target
  • To have an enum for expressing the role names

Details
  • We will use this enum in the Authority class to express the role names

for modelling AuthorityNames
1
2
3
4
5
package nl.acme.carapp.model;

public enum AuthorityName {
    USER, ADMIN
}

Add class Authority

Target
  • To have a class for modelling the Authority and to be able to persist Authorities in the database

Details
  • We will use this class in the User class to express the roles of a user

for modelling the Authority
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package nl.acme.carapp.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;

import java.util.List;

@Entity
@Getter
@Setter
public class Authority implements GrantedAuthority {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private AuthorityName name;

    @ManyToMany(mappedBy = "authorities", fetch = FetchType.LAZY)
    private List<User> users;

    @Override
    public String toString() {
        return "Authority{" +
                "id=" + id +
                ", name=" + name +
                '}';
    }

    @Override
    public String getAuthority() {
        return this.name.name();
    }
}

Add interface AuthorityRepository

Target
  • To have a repository for persisting Authorities in the database

Details
  • We will use this repository to persist Authorities in the database

AuthorityRepository
1
2
3
4
5
6
7
8
9
10
11
package nl.acme.carapp.repository;

import nl.acme.carapp.model.Authority;
import nl.acme.carapp.model.AuthorityName;
import org.springframework.data.repository.ListCrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AuthorityRepository extends ListCrudRepository<Authority, Long> {
    Authority findByName(AuthorityName name);
}

Add class User

Target
  • To have a class for modelling the User and to be able to persist Users in the database

Detail
  • This class represents the PERSISTED user in the database

for modelling the User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package nl.acme.carapp.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Entity
@EqualsAndHashCode(of = {"id"})
public class User implements UserDetails {

    @Getter
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Getter
    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(
            name = "USER_AUTHORITY",
            joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "ID")},
            inverseJoinColumns = {@JoinColumn(name = "AUTHORITY_ID", referencedColumnName = "ID")})
    private final List<Authority> authorities = new ArrayList<>();

    public User() {}

    public User(String username, String password, Authority authority) {
        this.username = username;
        this.password = password;
        this.authorities.add(authority);
    }

    @JsonIgnore
    public String getPassword() {
        return password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @JsonIgnoreProperties("users")
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

Add interface UserRepository

Target
  • To have a repository for persisting Users in the database

UserRepository
1
2
3
4
5
6
7
8
9
10
package nl.acme.carapp.repository;

import nl.acme.carapp.model.User;
import org.springframework.data.repository.ListCrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends ListCrudRepository<User, Long> {
    User findByUsername(String username);
}
Introduction

During this section we will add a user to the database. The password of the user will be hashed using Bcrypt.

Using the InitService

Target
  • To create the security realm (users etc.) using the InitService

Details
  • The InitService is a fast way to create the security realm (users etc.) without hassle

The InitService below will create the users, realm and coupling between them using JPA statements
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package nl.acme.carapp.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nl.acme.carapp.config.InitProperties;
import nl.acme.carapp.dto.SignUpRequest;
import nl.acme.carapp.model.Authority;
import nl.acme.carapp.model.AuthorityName;
import nl.acme.carapp.model.Car;
import nl.acme.carapp.model.User;
import nl.acme.carapp.repository.AuthorityRepository;
import nl.acme.carapp.repository.CarRepository;
import nl.acme.carapp.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
public class InitService implements CommandLineRunner {

    private final AuthorityRepository authorityRepository;
    private final CarRepository carRepository;
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final InitProperties initProperties;

    @Override
    @Transactional
    public void run(String... args) {
        this.userRepository.deleteAll();
        this.authorityRepository.deleteAll();

        initAuthorities();
        initUsers();
        initCars();
    }

    private void initAuthorities() {
        for (AuthorityName authorityName : AuthorityName.values()) {
            Authority authority = new Authority();
            authority.setName(authorityName);
            this.authorityRepository.save(authority);
        }
    }

    public void initUsers() {
        for (SignUpRequest signUpRequest : this.initProperties.getSignUpRequests()) {
            User user = new User(signUpRequest.username(), passwordEncoder.encode(signUpRequest.password()), this.authorityRepository.findByName(signUpRequest.role()));
            this.userRepository.save(user);
        }
    }

    public void initCars() {
        this.carRepository.deleteAll();
        List<String> carNames = this.initProperties.getCars();
        for (int i = 1; i <= 3; i++) {
            Car car = new Car();
            car.setBrand(carNames.get(i - 1));
            car.setMileage(1000 * i);
            car.setLicensePlate("%s9-BB-CC".formatted(i));
            this.carRepository.save(car);
        }
    }
}
InitProperties used in InitService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package nl.acme.carapp.config;

import lombok.Getter;
import lombok.Setter;
import nl.acme.carapp.dto.SignUpRequest;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@ConfigurationProperties(prefix="realm")
@Getter
@Setter
public class InitProperties {
    private List<SignUpRequest> signUpRequests;
    private List<String> cars;
}

Upgrade

Upgrade your Spring Boot project to the current version

Set parent project
1
2
3
4
5
6
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

Add the dependencies

Target
  • Add the dependencies in the project

Your pom.xml should look like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?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>

    <groupId>nl.acme</groupId>
    <artifactId>carapp</artifactId>
    <version>3.3.0</version>
    <name>carapp</name>
    <description>Scaffolded carapp application by Carpago Software</description>

    <parent>
        <groupId>nl.carpago.acme.security</groupId>
        <artifactId>api-and-ui-security</artifactId>
        <version>3.3.0</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <!-- for the JWT token generation -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <!-- Handy for autocompletion in application.yaml -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-docker-compose</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-failsafe-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Add a UserDetailsService implementation

Add this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package nl.acme.carapp.service;

import lombok.RequiredArgsConstructor;
import nl.acme.carapp.model.User;
import nl.acme.carapp.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MyUserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("No user found with username '%s'.".formatted(username));
        } else {
            return user;
        }
    }
}

Add WebSecurityConfig

Somewhere below your Spring Boot config-package add this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package nl.acme.carapp.config;

import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nl.acme.carapp.service.MyUserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.Assert;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true,
        prePostEnabled = false)
@Slf4j
@RequiredArgsConstructor
public class SecurityConfig {

    private final RsaKeyProperties rsaKeys;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
    }

    @Bean
    public JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));

        return new NimbusJwtEncoder(jwks);
    }

    @Bean
    public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        Assert.isTrue(userDetailsService instanceof MyUserDetailsServiceImpl, "userDetailsService should be of type MyUserDetailsServiceImpl");
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);

        return provider;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, @Value("${jwt.route.authentication.path}") String authenticationPath) throws Exception {
        log.info("Authenticationpath is: [{}]", authenticationPath);
        return http.
                httpBasic(withDefaults())
                .oauth2ResourceServer(config -> config.jwt(customizer -> customizer.decoder(this.jwtDecoder())))
                .csrf(AbstractHttpConfigurer::disable) (1)
                .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(authenticationPath).permitAll() (2)
                        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() (3)
                        .requestMatchers("/images/**", "/js/**", "/webjars/**").permitAll() (4)
                        .requestMatchers(HttpMethod.GET, "/api/**").hasAnyAuthority("SCOPE_USER", "SCOPE_ADMIN") (5)
                        .requestMatchers( "/api/**").hasAuthority("SCOPE_ADMIN") (6)
                        .anyRequest().authenticated()) (7)
                .build();
    }
}
1 We don’t need CSRF because our JWT token is invulnerable
2 Always permit the access to the api auth endpoint
3 Allow CORS option calls
4 Because spring security prefixes the JWT claim with the name 'scope' with SCOPE. So USER becomes here: SCOPE_USER and ADMIN becomes SCOPE_ADMIN. Nothing further needs to be adjusted in the Authorities since it is only from TOKEN to SECURITY. The term scope and scp is a convention used by Spring, that’s why we also set it in the TokenService with "scope" when creating the token.
5 Allow Authority USER and ADMIN to GET from api
6 Allow Authority ADMIN to every request to api
7 To be able to open an .html file e.g. http://localhost:8080/index.html

Introduction

Diagram showing the flow using JWT

jwt 1

Create and Use keys for self signing the JWT

Create certs for self signing the JWT
##Steps for creating the certs in src/main/resources/certs

## run this in src/main/resources/certs

#Create a keypair
openssl genrsa -out keypair.pem 2048

#Extract the public key
openssl rsa -in keypair.pem -pubout -out public.pem  

#Since the private key must be encoded in PKCS8 format
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in keypair.pem -out private.pem

#Delete keypair.pem
delete keypair.pem

###Do not check those files in in git in production
Add this in application.yaml
rsa.private-key=classpath:certs/private.pem
rsa.public-key=classpath:certs/public.pem
Create the RsaKeyProperties file
1
2
3
4
5
6
7
8
9
package nl.acme.carapp.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

@ConfigurationProperties(prefix = "rsa")
public record RsaKeyProperties(RSAPublicKey publicKey, RSAPrivateKey privateKey) {}
Add TokenService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package nl.acme.carapp.service;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class TokenService {

    private final JwtEncoder encoder;
    private @Value("${jwt.expiration}") int expires; // @Value => Not final or else injection fails


    public Jwt generateToken(Authentication authentication) {

        Instant now = Instant.now();
        String roles = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));

        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("http://localhost:8080/issuer")
                .audience(Arrays.asList("cars"))
                .claim("scope", roles)
                .subject(authentication.getName())
                .issuedAt(now)
                .expiresAt(now.plus(this.expires, ChronoUnit.SECONDS))
                .build();

        JwtEncoderParameters params = JwtEncoderParameters.from(claims);
        final Jwt token = this.encoder.encode(params);

        return token;
    }
}
Add this line above your Spring Boot Application starting class
1
@EnableConfigurationProperties(RsaKeyProperties.class);
So your starter should look like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package nl.acme.carapp;

import nl.acme.carapp.config.RsaKeyProperties;
import nl.acme.carapp.config.InitProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties({InitProperties.class, RsaKeyProperties.class})
public class Application {
    public static void main(String[] args) {
                SpringApplication.run(Application.class, args);
    }
}

Add AuthController

This class is added to accomplish the following goal
  • To be able to login and validate the username and password, set a token and return it

For authentication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package nl.acme.carapp.api;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nl.acme.carapp.dto.SignInRequest;
import nl.acme.carapp.dto.SignUpRequest;
import nl.acme.carapp.model.User;
import nl.acme.carapp.service.AuthService;
import nl.acme.carapp.service.TokenService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;

@RestController
@CrossOrigin
@RequestMapping("api/auth")
@Slf4j
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;
    private final TokenService tokenService;

    // Authenticate by posting a username and password in a JSON body
    @PostMapping("signin")
    public ResponseEntity<Jwt> token(@RequestBody SignInRequest signInRequest) {
        var authentication = authService.signin(new UsernamePasswordAuthenticationToken(signInRequest.username(), signInRequest.password()));

        return token(authentication);
    }

    @PostMapping("signup")
    public ResponseEntity<User> createUser(@RequestBody SignUpRequest signUpRequest) {
        return ResponseEntity.ok(this.authService.signUp(signUpRequest));
    }

    // Authenticate by using Basic Authentication
    @GetMapping("signin")
    public ResponseEntity<Jwt> token(Authentication authentication) {
        log.info("Token requested for user [{}]", authentication.getName());
        Jwt jwtAuthenticationToken = this.tokenService.generateToken(authentication);
        log.debug("Token granted: [{}]", jwtAuthenticationToken.getTokenValue());

        return ResponseEntity.ok(jwtAuthenticationToken);
    }

}
For signing up
1
2
3
4
5
package nl.acme.carapp.dto;

import nl.acme.carapp.model.AuthorityName;

public record SignUpRequest(String username, String password, AuthorityName role) {}
For signing in
1
2
3
package nl.acme.carapp.dto;

public record SignInRequest(String username, String password) {}
AuthService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package nl.acme.carapp.service;

import lombok.RequiredArgsConstructor;
import nl.acme.carapp.dto.SignUpRequest;
import nl.acme.carapp.model.Authority;
import nl.acme.carapp.model.AuthorityName;
import nl.acme.carapp.model.User;
import nl.acme.carapp.repository.AuthorityRepository;
import nl.acme.carapp.repository.UserRepository;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthService {

    private final AuthenticationProvider authenticationProvider;
    private final UserRepository userRepository;
    private final AuthorityRepository authorityRepository;
    private final PasswordEncoder passwordEncoder;

    public Authentication signin(Authentication authentication) throws AuthenticationException {
        return authenticationProvider.authenticate(authentication);
    }

    public User signUp(SignUpRequest signUpRequest) {
        Authority userAuthority = this.authorityRepository.findByName(AuthorityName.USER);
        var user = new User(signUpRequest.username(), passwordEncoder.encode(signUpRequest.password()), userAuthority);

        return userRepository.save(user);
    }
}

Update application.yaml

Add this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
server:
  port: '8080'
spring:
  datasource:
    url: 'jdbc:mysql://localhost:3306/carapp?serverTimezone'
    driverClassName: com.mysql.cj.jdbc.Driver
    username: rloman
    password: rloman
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false
    properties:
      hibernate:
        format_sql: true
  docker:
    compose:
      lifecycle-management: start_only
'# for a week': '604800, for 2 minutes: 120, for 15 minutes: 900'
jwt:
  expiration: '900'
  route:
    authentication:
      path: /api/auth/**
      refresh: refresh
rsa:
  private-key: 'classpath:certs/private.pem'
  public-key: 'classpath:certs/public.pem'

realm:
  signUpRequests:
    - username: johndoe
      password: passwordjohn
      role: ADMIN
    - username: janedoe
      password: passwordjane
      role: USER
  cars:
    - bmw
    - mercedes
    - kia

Consequences for integration-tests

This only applies if you already made an integration test!
This file is added to accomplish the following goal
  • This file is read during the integrationtest phase and the properties are used in the @Value annotation during the code

    • The keys are also in the application.yaml file on the server or on your local development environment, following Spring Boot’s principle regarding externalized configuration

Create a file src/test/resource/application-integrationtest.yaml which looks like this
1
2
3
4
5
6
7
8
9
10
spring:
  datasource:
    url: 'jdbc:mysql://localhost:3307/carapp?serverTimezone'
  jpa:
    hibernate:
      ddl-auto: create-drop
jwt:
  header: Authorization
  secret: mySecret
  expiration: '604800'
Add @ActiveProfiles to your IT-tests since you now have a application-integrationtest.yaml file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package nl.acme.carapp;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("integrationtest")
public class CarApplicationIT {

    @Test
    public void contextLoads() {
    }
}

Testing

Start the application

We are using the spring-boot-compose-feature which starts the docker containers using this compose.yaml
version: '3'
services:
  mysql:
    image: mysql/mysql-server:8.0.28
    container_name: carapp_mysql-security
    environment:
      - MYSQL_ROOT_PASSWORD=mysecretPassword2022!
      - MYSQL_DATABASE=carapp
      - MYSQL_USER=rloman
      - MYSQL_PASSWORD=rloman
    volumes:
      - mysql01:/var/lib/mysql
    ports:
      - 3306:3306
  mysql_test:
    image: mysql/mysql-server:8.0.28
    container_name: carapp_mysql-security_test
    environment:
      - MYSQL_ROOT_PASSWORD=mysecretPassword2022!
      - MYSQL_DATABASE=carapp
      - MYSQL_USER=rloman
      - MYSQL_PASSWORD=rloman
    volumes:
      - mysql02:/var/lib/mysql
    ports:
      - 3307:3306
volumes:
  mysql01: {}
  mysql02: {}
# To drop volume invoke: docker-compose down --volumes
Steps
  • Open Postman

    • GET:: http://localhost:8080/api/cars (should fail)

    • POST::http://localhost:8080/token

      • Authentication Basic: username:<username> and password:<password>

      • Example: Authencation Basic: username {username}, password {password}

The result should be a response with a token in it like this …​
{
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJybG9tYW4iLCJjcmVhdGVkIjoxNTEyNTY0MjI5MTE4LCJleHAiOjE1MTMxNjkwMjl9.Dt9kvto2fCULqqKxzfHt80Gc3gZ7VCnu5LxTOR3YDnp3jPut7R-J_UzPbU_C_klTuS2HL-kkXkKQrV_Ae33oSg"
}

The resulting token should now be added to the Authorization header when you perform a request

postman using token

Corresponding sourcecode

Car
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package nl.acme.carapp.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;


@Entity
@Getter
@Setter
public class Car implements Serializable {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;

        @Column(nullable = false)
        private String brand;

        private String licensePlate;
        private int mileage;
}
CarController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package nl.acme.carapp.api;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nl.acme.carapp.model.Car;
import nl.acme.carapp.service.CarService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.util.Optional;

@CrossOrigin
@RestController
@RequestMapping("api/cars")
@Slf4j
@RequiredArgsConstructor
public class CarController {

        private final CarService carService;

        @GetMapping
        public ResponseEntity<Iterable<Car>> list() {
                return ResponseEntity.ok(carService.findAll());
        }

        @PostMapping
        public ResponseEntity<Car> create(@RequestBody Car car, Principal principal) {
                if (car.getBrand() == null) {
                        car.setBrand(principal.getName());
                        log.info("Car [{}] created by [{}]", car, principal.getName());
                }

                return ResponseEntity.ok(this.carService.save(car));
        }

        @GetMapping("{id}")
        public ResponseEntity<Car> findById(@PathVariable long id) {
                Optional<Car> optionalCar = this.carService.findById(id);
        return optionalCar.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
        }

        @PutMapping("{id}")
        public ResponseEntity<Car> updateById(@PathVariable long id, @RequestBody Car source) {
                Optional<Car> optionalCar = this.carService.findById(id);
                if(optionalCar.isPresent()) {
                        Car target = optionalCar.get();
                        target.setBrand(source.getBrand());
                        target.setLicensePlate(source.getLicensePlate());
                        target.setMileage(source.getMileage());

                        return ResponseEntity.ok(this.carService.save(target));
                }
                else {
                        return ResponseEntity.notFound().build();
                }
        }

        @DeleteMapping("{id}")
        public ResponseEntity<Void> deleteById(@PathVariable long id) {
                Optional<Car> optionalCar = this.carService.findById(id);
                if(optionalCar.isPresent()) {
                        this.carService.deleteById(id);

                        return ResponseEntity.noContent().build();
                }
                else {
                        return ResponseEntity.notFound().build();
                }
        }
}
CarService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package nl.acme.carapp.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import nl.acme.carapp.model.Car;
import nl.acme.carapp.repository.CarRepository;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class CarService {

        private final CarRepository carRepository;

        public Iterable<Car> findAll() {
                return carRepository.findAll();
        }

        @Transactional
        public Car save(Car car) {
                return carRepository.save(car);
        }

        public Optional<Car> findById(long id) {
                return carRepository.findById(id);
        }

        public void deleteById(long id) {
                carRepository.deleteById(id);
        }
}
CarRepository
1
2
3
4
5
6
package nl.acme.carapp.repository;

import nl.acme.carapp.model.Car;
import org.springframework.data.repository.ListCrudRepository;

public interface CarRepository extends ListCrudRepository<Car, Long> {}

Resources

Code on Github
Links regarding using BCrypt when adding your username and password in the DB

Tips and Tricks

If you are using Spring Boot REST you can use the annotation @CrossOrigin to implement CrossOrigin to be able to use your controller with JavaScript clients
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package nl.acme.carapp.api;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import nl.acme.carapp.model.Car;
import nl.acme.carapp.service.CarService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.security.Principal;
import java.util.Optional;

@CrossOrigin
@RestController
@RequestMapping("api/cars")
@Slf4j
@RequiredArgsConstructor
public class CarController {

        private final CarService carService;

        @GetMapping
        public ResponseEntity<Iterable<Car>> list() {
                return ResponseEntity.ok(carService.findAll());
        }

        @PostMapping
        public ResponseEntity<Car> create(@RequestBody Car car, Principal principal) {
                if (car.getBrand() == null) {
                        car.setBrand(principal.getName());
                        log.info("Car [{}] created by [{}]", car, principal.getName());
                }

                return ResponseEntity.ok(this.carService.save(car));
        }

        @GetMapping("{id}")
        public ResponseEntity<Car> findById(@PathVariable long id) {
                Optional<Car> optionalCar = this.carService.findById(id);
        return optionalCar.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
        }

        @PutMapping("{id}")
        public ResponseEntity<Car> updateById(@PathVariable long id, @RequestBody Car source) {
                Optional<Car> optionalCar = this.carService.findById(id);
                if(optionalCar.isPresent()) {
                        Car target = optionalCar.get();
                        target.setBrand(source.getBrand());
                        target.setLicensePlate(source.getLicensePlate());
                        target.setMileage(source.getMileage());

                        return ResponseEntity.ok(this.carService.save(target));
                }
                else {
                        return ResponseEntity.notFound().build();
                }
        }

        @DeleteMapping("{id}")
        public ResponseEntity<Void> deleteById(@PathVariable long id) {
                Optional<Car> optionalCar = this.carService.findById(id);
                if(optionalCar.isPresent()) {
                        this.carService.deleteById(id);

                        return ResponseEntity.noContent().build();
                }
                else {
                        return ResponseEntity.notFound().build();
                }
        }
}
Else, if you are using Java EE (using JAX-RS) you have to add this configuration file with creates a Java EE Filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class RestConfig {

   @Bean
   public CorsFilter corsFilter() {

       UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
       CorsConfiguration config = new CorsConfiguration();

       config.setAllowCredentials(true);
       config.addAllowedOrigin("*");
       config.addAllowedHeader("*");
       config.addAllowedMethod("OPTIONS");
       config.addAllowedMethod("GET");
       config.addAllowedMethod("POST");
       config.addAllowedMethod("PUT");
       config.addAllowedMethod("DELETE");
       source.registerCorsConfiguration("/**", config);

       return new CorsFilter(source);
   }
}
The RSA algorithm for dummies
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package nl.acme.carapp.rsa;

import java.math.BigInteger;

public class Rsa {

    private BigInteger n;
    private BigInteger publicKey; // When encrypting, the public key. Also used for validating the signature
    private BigInteger privateKey; // When decrypting; the private key. Also used for creating the signature

    private Rsa(RsaBuilder builder) {
        this.n = builder.n;
        this.publicKey = builder.publicKey;
        this.privateKey = builder.privateKey;
    }

    // Use the public key to encrypt the message
    public BigInteger encrypt(BigInteger message) {
        return message.modPow(publicKey, n);
    }

    // Use the private key to descrypt the cypherText to the original
    public BigInteger decrypt(BigInteger cypherText) {
        return cypherText.modPow(this.privateKey, n);
    }

    // Use the PRIVATE(!) key to sign the digest
    public BigInteger createDigitalSignature(BigInteger digest) {
        return this.decrypt(digest);
    }

    public static class RsaBuilder {
        private BigInteger p;
        private BigInteger q;
        private BigInteger n;
        private BigInteger nAccent;

        private BigInteger publicKey;
        private BigInteger privateKey;

        public RsaBuilder() {
            this(java.math.BigInteger.valueOf(1270).nextProbablePrime(), java.math.BigInteger.valueOf(2110).nextProbablePrime());
        }

        public RsaBuilder(BigInteger p, BigInteger q) { // p and q should be primes here
            this.p = p;
            this.q = q;
        }

        public Rsa build() {
            createModulus();
            createModulusAccent();
            createPublicKey();
            createPrivateKey();

            return new Rsa(this);
        }

        public void createModulus() {
            this.n = p.multiply(q);
        }

        public void createModulusAccent() {
            // Subtracting One From The Two Primes And Multiply Them
            this.nAccent = p.subtract(java.math.BigInteger.valueOf(1)).multiply(q.subtract(java.math.BigInteger.valueOf(1)));
        }

        public void createPublicKey() {
            BigInteger startPointForGettingARandomPrime = this.nAccent.divide(java.math.BigInteger.valueOf(2));
            this.publicKey = startPointForGettingARandomPrime.nextProbablePrime();
        }

        public void createPrivateKey() {
            this.privateKey = publicKey.modInverse(this.nAccent);
        }
    }
}
A helper class (unrelated to RSA)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package nl.acme.carapp.rsa;

import java.math.BigInteger;

public final class Digest {

    public static BigInteger createDigest(String message) {
        var hashedValue = String.valueOf(message.hashCode());
        hashedValue = hashedValue.substring(hashedValue.length() - 6); // if you remove more the hashedValue is too long

        return BigInteger.valueOf(Integer.parseInt(hashedValue));
    }

    private Digest() {}
}
A unittest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package nl.acme.carapp.rsa;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.math.BigInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;

@Slf4j
public class RsaTest {

    private Rsa rsa = new Rsa.RsaBuilder().build();

    @Test
    public void testEncryptionAndDecryption() {

        // Arrange
        BigInteger originalMessage = BigInteger.valueOf(10237);
        BigInteger cypherText = rsa.encrypt(originalMessage);
        log.info("Encrypting: [{}] to [{}]",originalMessage, cypherText);

        // Act
        BigInteger message = rsa.decrypt(cypherText);
        log.info("Decrypting: [{}] to [{}]",cypherText, originalMessage);

        // Assert
        assertEquals(originalMessage, message);
    }

    @Test
    public void testDigitalSignature() {
        // Arrange
        // At the sender
        String message = "Hello world!";
        // Create the / a hash
        BigInteger mDigest = Digest.createDigest(message);
        // Create the digital signature
        BigInteger signature = rsa.createDigitalSignature(mDigest); // NB: The other way around as encryption / decryption

        // At the receiver
        // accepts m, the original message. Accepts signature, the signed with private key message
        // first calculate the digest, so

        // Act
        String receivedMessage = new String(message);
        // create the hash
        BigInteger mDigestReceiver = Digest.createDigest(receivedMessage);
        BigInteger mDigestDecrypted = rsa.encrypt(signature); // NB: The other way around as encryption / decryption

        // Assert
        log.info("Message: [{}], Digest: [{}]", message, mDigest);
        log.info("Message: [{}], Digest: [{}]", receivedMessage, mDigestDecrypted);

        assertEquals(message, receivedMessage);
        assertEquals(mDigest, mDigestDecrypted);
        assertEquals(mDigestReceiver, mDigestDecrypted);
    }
}

Backlog

Refresh the token
1
2
# the path where the user can refresh his token (if it is not expired yet) (to do)
jwt.route.authentication.refresh=refresh

Release notes

v3.3.0 (08-05-2024)
  • Still use Lombok but with a twist.

  • Clearer instructions for Rsa

  • Add @RequiredArgsConstructor just like Kotlin

  • Use User instead of JwtUser

  • Use spring JwtAuthenticationToken instead of my homebrew version

  • Add links to missing source code in adoc files

  • Add posting of new user and more

  • Change claims

  • Move to spring boot 3.2.x and improve security

    • Improve security part I

    • Improve security part II

  • Add RSA generate demo code

  • Move from log4j to logback

  • Nicer layout regarding compose.yaml

  • application.properties to application.yaml in adoc also for integrationtest

  • application.properties to application.yaml in adoc

  • Set [source%nowrap,java,linenums]

  • Start container only and do not stop

  • Add InitProperties to set users and cars in the InitService using @ConfigurationProperties

  • Translate to English

  • Complete overhaul of the index.adoc to have correct courseware

  • Improve and Update the Security

  • Silence Makefile

  • Add more logging regarding creating user

  • Improve bcrypted

  • Add first / primitive version of Angular integration

v3.2.0 (17-03-2023)
  • Fix a prefix slash

  • Add missing jwtconfig in doc

  • Move auth$ to api/auth

  • Upgrade to the latest version of bcrypt

  • Log for creating a user.

v3.1.0 (16-03-2023)
  • Update instead of create-drop including fix of consequences

  • Use the latest version (2.7.9) of Spring and by consequence ⇒ Fix a dep

  • Add more explain regarding Salt and JWT

v3.0.0 (05-07-2022)
  • Set version of carapp

  • Some bugfixing

  • typing less passwords please

  • Prepare makefile for new deployment structure

  • Make dryer since we now have a parent project

  • Enable source with nowrap and linenums which needed a version upgrade

  • Add GeneralConfig

  • Add InitService and for the diehards the hard coded mysql file

  • Add lombok to the code to have less code for rendering in adoc

  • Move inline adoc java code to .java file from project carapp

  • Remove api and ui softlinks and thus enable api dir

  • Initial load after restarting one project for all carapp, api, ui and doc

  • Initial commit

v2.0.0 (06-12-2019)
  • Complete rewrite of the tutorial after implementing security for learninggoals.

  • Add some theory items regarding security

    • Blowfish

    • Bcrypt

    • JWT

    • CORS (Cross Origin)

  • Separation of Theory part and Implementing part

v1.0.0 (06-12-2017)
  • Initial release