Introduction
What 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
-
<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
-
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 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
Hashing where there is a kind of key which makes the reverting from cypher text to plain text possible. One way, trap door.
-
Weak
-
Password
-
-
Strong
-
Passport
-
-
Role
-
Authority (we will be used this)
-
Encrypting text to be sure that no one can read the text which is send
-
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
-
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
-
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
The original Bcrypt specification defined a prefix of $2$.
-
$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')
The original specification did not define how to handle non-ASCII character, nor how to handle a null terminator.
-
the string must be UTF-8 encoded
-
the null terminator must be included With this change, the version was changed to $2a$
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.
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
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.
-
Also follow the links which are mentioned in the above mentioned URLs for further reading
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
-
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.
How: JWT
Please read this ⇒ Everything you need to know regarding JWT
-
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 ⇒
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
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
-
What CORS is
-
How to protect it
-
How to use it in a JavaScript / Angular application
Why and How: CORS
Read this
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
Model related
-
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
-
To have an enum for expressing the role names
-
We will use this enum in the Authority class to express the role names
1
2
3
4
5
package nl.acme.carapp.model;
public enum AuthorityName {
USER, ADMIN
}
Add class Authority
-
To have a class for modelling the Authority and to be able to persist Authorities in the database
-
We will use this class in the User class to express the roles of a 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
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
-
To have a repository for persisting Authorities in the database
-
We will use this repository to persist Authorities in the database
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
-
To have a class for modelling the User and to be able to persist Users in the database
-
This class represents the PERSISTED user in the database
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
-
To have a repository for persisting Users in the database
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);
}
Database related
During this section we will add a user to the database. The password of the user will be hashed using Bcrypt.
Using the InitService
-
To create the security realm (users etc.) using the InitService
-
The InitService is a fast way to create the security realm (users etc.) without hassle
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);
}
}
}
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;
}
Maven related
Upgrade
Upgrade your Spring Boot project to the current version
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
-
Add the dependencies in the project
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>
Security related
Add a UserDetailsService implementation
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
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 |
JWT related
Introduction
Create and Use keys 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
rsa.private-key=classpath:certs/private.pem
rsa.public-key=classpath:certs/public.pem
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) {}
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;
}
}
1
@EnableConfigurationProperties(RsaKeyProperties.class);
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);
}
}
Authentication REST related
Add AuthController
-
To be able to login and validate the username and password, set a token and return it
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);
}
}
Add classes related to AuthController
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) {}
1
2
3
package nl.acme.carapp.dto;
public record SignInRequest(String username, String password) {}
Authentication service related
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);
}
}
Externalized config related
Update application.yaml
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 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
-
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'
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
-
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}
-
-
{
"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

Corresponding sourcecode
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;
}
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();
}
}
}
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);
}
}
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
-
Where I brew it all to a more conventient first step educational site: https://github.com/rloman/spring-security-demo-tour-of-heroes
Tips and Tricks
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();
}
}
}
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);
}
}
RSA related
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);
}
}
}
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() {}
}
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
-
Add UI documentation
-
Move this link to the UI part https://chariotsolutions.com/blog/post/angular-2-spring-boot-jwt-cors_part2/
-
-
Post a new user to the REST service and persist that to the Database
-
Use token expiration with this ⇒
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
-
Initial release