In the previous chapter, we learned mainly how to generate automated documentation for our created APIs in our Spring Boot project. We learned how to add and use the features and properties of springdoc-openapi, configure the plugin on the project, and access the generated JSON and YAML documentation. We also learned how to implement the Swagger UI to make our documentation interactive and allow us to test endpoints directly on the browser.
This chapter will now focus on the security side of our application. We will discuss the concept of Cross-Origin Resource Sharing (CORS) and how it can secure our application. We will also be discussing the features and implementation of Spring Security in Spring Boot, the concept of JSON Web Token (JWT), and Identity as a Service (IDaaS).
In this chapter, we will cover the following topics:
The link to the finished version of this chapter’s code is here: https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-07/superheroes.
We might have already encountered the term CORS several times when creating our applications as developers. Still, we may ask questions such as what does CORS do? Or what is the advantage of implementing CORS in our application? With these questions in mind, we will dive deeply, in this section, into the concepts and features of CORS and understand how it is used to secure our applications.
CORS is a header-based mechanism that allows a server to define a set of domains, schemes, or ports permitted to access the application’s resources. CORS is commonly used in REST APIs. Different frontend applications can access the APIs under our backend applications, especially in complex architectures. We don’t want our APIs to be accessed by unknown applications, and CORS is responsible for securing this part.
Let’s see a simple example of a cross-origin request. Say we have a frontend application with a domain of https://domain-one.com and a backend application served with a domain of https://domain-two.com. We can see that our application is served with different domains, and once the frontend application sends a request to the backend, this is considered a cross-origin request.
We should never forget that browsers restrict cross-origin requests by default, and same-origin requests are the only ones allowed for requesting resources unless the origin requesting the resources includes the proper CORS headers and is permitted on the backend application. This is just a simple example of how CORS works. Let’s look at a more detailed overview of the concept of CORS.
CORS is a header-based mechanism, which means that the first step to achieving cross-origin sharing is to add new HTTP headers that will describe the list of origins that are permitted to access resources. These headers can be described as our key to communication. The HTTP headers are divided into two categories, which are as follows:
Request headers are the headers required for the client to make use of the CORS mechanism. They are as follows:
Let’s see an example of what a request would look like using the request headers:
curl -i -X OPTIONS localhost:8080/api/v1 -H 'Access-Control-Request-Method: GET' -H 'Access-Control-Request-Headers: Content-Type, Accept' -H 'Origin: http://localhost:4200
Response headers are the headers that the servers send back with the response. They are as follows:
Let’s see an example of what response we would like with the given headers:
HTTP/1.1 204 No Content Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE Vary: Access-Control-Request-Headers Access-Control-Allow-Headers: Content-Type, Accept Content-Length: 0 Date: Sun, 16 Nov 2022 3:41:08 GMT+8 Connection: keep-alive
These are the standard headers that we will use to allow the CORS mechanism, but there are several different scenarios in which cross-origin sharing works.
These are requests that don’t trigger CORS preflight requests and, having no initial request, will be sent to the server for validation. To consider a request to be simple, it should satisfy the following conditions:
Let’s see an example of a simple request:
GET /content/test-data/ HTTP/1.1 Host: example.host User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: https://frontend.com
This request will perform a simple exchange between the client and the server. In response, the server returns the header with Access-Control-Allow-Origin: *, which means that the resource or endpoint can be accessed by any origin.
The browser sends a test or first HTTP request using the OPTIONS method to validate that the request is permitted or safe. Preflight requests will always occur on cross-origin requests as preflight requests check whether a different origin is allowed or permitted to access the resource.
Let’s see an example of a preflight request:
OPTIONS /content/test-data/ HTTP/1.1 Host: example.host User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Connection: keep-alive Origin: https://frontend.com Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type
The preceding example shows that the preflight request uses the OPTIONS request method to execute the preflight request. The OPTIONS method is used to identify more information from the servers to know whether the actual request is permitted.
We can also see that Access-Control-Request-Method and Access-Control-Request-Headers are identified. This indicates the request headers and request method to be used in the actual request.
Here is the header info:
HTTP/1.1 204 No Content Date: Sun, 16 Nov 2022 3:41:08 GMT+8 Server: Apache/2 Access-Control-Allow-Origin: https://frontend.com Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400 Vary: Accept-Encoding, Origin Keep-Alive: timeout=2, max=100 Connection: Keep-Alive
Now, in the preceding example, this is an example response returned after the preflight request. Access-Control-Allow-Origin indicates that access to resources is only allowed on the specified domain (https://frontend.com in the example). Access-Control-Allow-Methods confirms that POST and GET are valid methods. Access-Control-Allow-Headers ensures that X-PINGOTHER and Content-Type are proper headers for the actual request.
We have learned the basic concepts of CORS; now, we will implement CORS in our Spring Boot application in the next section.
We have learned how CORS works and the advantage it brings to the security of our applications. Now, we will configure and implement a CORS policy in our Spring Boot project.
There are several ways to configure CORS on our project. We will discuss them one by one.
We can enable CORS on a single endpoint; this means that we can specify different permitted origins for other endpoints. Let’s have a look at the following example:
@CrossOrigin @GetMapping public List<AntiHeroDto> getAntiHeroes(Pageable pageable) { ..code implementation }
In our Spring Boot project, we have the getAntiHeroes() method. To enable CORS on a specific method, we will use the @CrossOrigin annotation. We can see that we have not configured any other settings, and this applies the following:
We can also specify the configuration of the CORS policy by adding the values of the origin, methods, allowedHeaders, exposedHeaders, allowedCredentials, and maxAge:
@CrossOrigin(origin = "origin.example") @GetMapping public List<AntiHeroDto> getAntiHeroes(Pageable pageable) { ..code implementation }
In the previous configuration, we were adding CORS to each method. Now, we will add the CORS policy at the controller level. Let’s have a look at the following example:
@CrossOrigin @AllArgsConstructor @RestController @RequestMapping("api/v1/anti-heroes") public class AntiHeroController { .. methods }
We can see that @CrossOrigin is added at the class level. This means that the CORS policy will be added to all the methods under AntiHeroController.
We can combine the application of CORS at both the controller and method levels in our application. Let’s have a look at the following example:
@CrossOrigin(allowedHeaders = "Content-type") @AllArgsConstructor @RestController @RequestMapping("api/v1/anti-heroes") public class AntiHeroController { private final AntiHeroService service; private final ModelMapper mapper; @CrossOrigin(origins = "http://localhost:4200") @GetMapping public List<AntiHeroDto> getAntiHeroes(Pageable pageable) { … code implementation }
We can see in our example that we have applied the @CrossOrigin annotation at both the controller and method levels, @CrossOrigin(allowedHeaders = "Content-type") will be used on all the methods under AntiHeroController, and @CrossOrigin(origins = http://localhost:4200) will be applied only on the getAntiHeroes() method, thus other methods will allow all origins.
The last way we can implement a CORS policy is by using global configuration, which means that our CORS policy applies to all the existing methods in our project. There are several ways to implement global CORS configuration, and we will see how to implement CORS by using CorsFilter:
@Configuration
public class CorsConfig {
}
@Bean
CorsFilter corsFilter() {
CorsConfiguration corsConfiguration =
new CorsConfiguration();
}
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
corsConfiguration.setAllowedHeaders(
Arrays.asList(
"Origin",
"Access-Control-Allow-Origin",
"Content-Type",
"Accept",
"Authorization",
"Origin, Accept",
"X-Requested-With",
"Access-Control-Request-Method",
"Access-Control-Request-Headers"
)
);
corsConfiguration.setExposedHeaders(
Arrays.asList(
"Origin",
"Content-Type",
"Accept",
"Authorization",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials"
)
);
corsConfiguration.setAllowedMethods(
Arrays.asList("GET", "POST", "PUT", "DELETE",
"OPTIONS")
);
var urlBasedCorsConfigurationSource =
new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration(
"/**",
corsConfiguration
);
return new CorsFilter(urlBasedCorsConfigurationSource);
@Bean
CorsFilter corsFilter() {
CorsConfiguration corsConfiguration =
new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setAllowedOrigins(
Arrays.asList("http://localhost:4200"));
corsConfiguration.setAllowedHeaders(
Arrays.asList("Origin",
"Access-Control-Allow-Origin",
"Content-Type","Accept","Authorization",
"Origin, Accept","X-Requested-With",
"Access-Control-Request-Method",
"Access-Control-Request-Headers"));
corsConfiguration.setExposedHeaders(
Arrays.asList( "Origin","Content-Type",
"Accept","Authorization",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials"));
corsConfiguration.setAllowedMethods(
Arrays.asList("GET", "POST", "PUT", "DELETE",
"OPTIONS")
);
var urlBasedCorsConfigurationSource =
new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource
.registerCorsConfiguration(
"/**",
corsConfiguration
);
return new CorsFilter(
urlBasedCorsConfigurationSource);
}
Having started our application, we will now apply the CORS configuration to all the methods in our project. We have successfully implemented a CORS policy in our application, but this is just part of how we secure our application.
In the next section, we will discuss the concept of Spring Security and how to implement it in a Spring Boot project.
Spring Security is an application-level security framework widely used in Spring Boot applications. It is a flexible authentication framework that provides most of the standard security requirements for Java applications. Spring Security is popular owing to the fact that it allows developers to integrate different authorization and authentication providers on the fly with the other modules available.
As we’re using Spring Security in our application, we do not need to code security-related tasks from scratch as Spring Security has these features under the hood.
Let’s discuss the concepts of Spring Security further.
Spring Security mainly focuses on integrating authentication and authorization into applications. To compare the two, authentication refers to validating that a user can access your application and identifying who the user is. This mainly refers to the login page itself. On the other hand, authorization is used for more complex applications; this relates to the operations or actions that a specific user can do inside your applications.
Authorization can be accomplished by integrating roles to implement user access controls. Spring Security also provides different password encoders – one-way transformation passwords – which are as follows:
The preceding list is of the most commonly used password encoders and can be accessed directly when using Spring Security. It also provides different features that will help you to meet security requirements, which are as follows:
Spring Security offers a wide range of features for the application. In this case, the design of Spring Security is divided into separate Java Archive (JAR) files based on its functionality, only requiring the installation of the needed part for our development.
The following is a list of JAR files that are included in the Spring Security module:
We have now learned about the different features and modules that Spring Security offers. In the next section, we will learn how to implement authentication and authorization using Spring Security in our Spring Boot application.
We have already discussed the concepts of Spring Security in the previous section; now, we will learn how to integrate Spring Security into our Spring Boot application. As we move on to the examples, we will be using all the modules and features of Spring Boot Security.
Authentication and authorization are the most common concepts that we come across when we implement security in our applications. These are the two validations we apply for our application to be secure.
We will first implement authentication in our application. We first need to add the Spring Boot Security dependency to our project. To add the dependency, we will add the following to pom.xml:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
Reload the project to install the new dependency and run the server. Let’s try to visit localhost:8080 to open the Spring Boot application project in the browser. As we can see, a login page is now applied to our project as we’ve installed Spring Boot Security:
Figure 7.1 – Login page integrated from Spring Boot Security
To create credentials for the login, we can configure the username and password under the application.properties file by placing the following setting:
spring.security.user.name=admin spring.security.user.password=test
In the preceding example, we have used admin as the username and test as the password for our Spring Boot Security login, which will allow us to log in successfully to our application.
We have now successfully set up Spring Boot Security for our project, and this automatically applies authentication to our endpoints. The next step we need to do is add a configuration for our security; we would want to override the default configuration and implement a customized login endpoint for our application to give access to our other endpoints provided.
To start with the configuration, let’s first create a new class named SecurityConfig under the config file. We will extend our new SecurityConfig class with WebSecurityConfigurerAdapter. This adapter allows us to override and customize the configuration of WebSecurity and HttpSecurity, and after extending the class, we will override the first two methods:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //We will place the customized userdetailsservice here in the following steps } @Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
The first method that we will override on WebSecurityConifigurerAdapter is the configure(AuthenticationManagerBuilder auth) method, which accepts AuthenticationManagerBuilder, which is used to build LDAP authentication, JDBC-based authentication, adding a custom UserDetailsService, and adding AuthenticationProviders. In this case, we will use this to access userDetailsServiceMethod() to customize our authentication. We will do that in the following steps as we have not yet created our modified UseDetailsService.
The second method is authenticationManagerBean(); we override this method to expose AuthenticationManager as a bean in our application, which we will later use in AuthenticateController. The next step is to implement the configuration we want for our HTTP requests. To achieve this, we will override the configure(HttpSecurity http) method.
HttpSecurity allows us to call methods that will implement configuration for web-based security requests for the HTTP requests. By default, the security configuration will be applied to all HTTP requests, but we can also set only specific requests by using the requestMatcher() methods. HttpSecurity is the same as the Spring Security XML configuration.
Let’s discuss the standard methods under HttpSecurity:
In our code, let’s configure a basic configuration for our security. Let’s place the following code inside the configure(HttpSecurity http) method:
@Override protected void configure(HttpSecurity http) throws Exception { http // first chain .csrf() .disable() // second chain .antMatcher("/**") .authorizeRequests() // third chain .antMatchers("/**") .permitAll() // fourth chain .and() .sessionManagement() .sessionCreationPolicy( SessionCreationPolicy.STATELESS); }
In the preceding example configuration, we have implemented several configurations for our application. You can notice that we have divided the methods into chains. This is to show the methods are related to each other.
The first chain, with .csrf().disable(), disables the use of CSRF protection. This is just an example, and disabling CSRF is not recommended when building your application. The second chain, with .antMatcher("/**").authorizedRequests(), states that any requests are authorized to be accessed by any users regardless of the role.
This can be modified by specifying the role in the hasRole() method by restricting the users based on the assigned roles. The third chain is .antMatchers("/**").permitAll(), which indicates that any users can access all the URLs, and lastly, sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) indicates that no session should be created by Spring Security.
We have successfully created SecurityConfig, which contains all of our configurations for Spring Security; our code should look like the following:
@AllArgsConstructor @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { // removed some code for brevety @Override protected void configure(HttpSecurity http) throws Exception { http // first chain .csrf() .disable() // second chain .antMatcher("/**") .authorizeRequests() // third chain .antMatchers("/**") .permitAll() // fourth chain .and() .sessionManagement() .sessionCreationPolicy( SessionCreationPolicy.STATELESS); } }
Now, we will move on to the next step, where we will create our endpoints for our user entity.
When implementing CRUD, we need to create our user endpoints. We need to develop these endpoints such that they will be used for the registration of a new user in our database. In this example, we will repeat the steps on how to develop endpoints discussed in w, Documenting APIs with OpenAPI Specification, but we will also create a whole CRUD capability for the user entity.
Let’s create a new user package and make the controller, data, entity, repository, and service packages under the user package.
Let’s create the user entity first by creating a new class named UserEntity under the entity package, and we will place the following code:
@Entity @Data @AllArgsConstructor @NoArgsConstructor public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "UUID") @Column(nullable = false, updatable = false) private UUID id; @Column(unique = true) private String email; private String mobileNumber; private byte[] storedHash; private byte[] storedSalt; public UserEntity(String email, String mobileNumber) { this.email = email; this.mobileNumber = mobileNumber; } }
In the preceding example, we have assigned several properties for UserEntity. We have annotated it with @Entity to indicate that this is a Java Persistence API (JPA) entity. We have configured it with an email address, mobile number, storedHash, and a storedSalt property. storedHash and storedSalt will be used for hashing and verifying the user’s password.
After creating the user entity, we will make the Data Transfer Object (DTO). The DTO is an object that we commonly use for responses or to hide unnecessary properties. We will create a new class named UserDto under the data package, and we will place the following code:
@Data @AllArgsConstructor @NoArgsConstructor public class UserDto { private UUID id; private String email; private String mobileNumber; private String password; }
The next thing we need to do is create the repository for our user. Under the repository package, create a new class named UserRepository, and we will extend the class with JPARepository by adding the following code:
@Repository public interface UserRepository extends JpaRepository<UserEntity, UUID> { @Query( "" + "SELECT CASE WHEN COUNT(u) > 0 THEN " + "TRUE ELSE FALSE END " + "FROM UserEntity u " + "WHERE u.email = ?1" ) Boolean selectExistsEmail(String email); UserEntity findByEmail(String email); }
In the preceding example, we extended UserRepository with JPARepository, which grants all the CRUD capabilities to our repository. We have also created two methods with an @Query annotation, which checks whether the email address already exists.
The next step is now to create our user service where we will implement the business logic of the application. Under the service package, we will create a new class named UserService, after the creation of the service.
We will place @AllArgsConstructor for the constructor injecting the dependencies and the @Service annotation to let Spring know that this is a service layer, and we will also inject ModelMapper and UserRepository into our service after the annotations and dependency injection.
We can create two methods that allow us to convert an entity into a DTO and vice versa by placing the following code:
private UserDto convertToDto(UserEntity entity) { return mapper.map(entity, UserDto.class); } private UserEntity convertToEntity(UserDto dto) { return mapper.map(dto, UserEntity.class); }
Now, we will create the code for the basic CRUD functionalities:
public List<UserDto> findAllUsers() {
var userEntityList =
new ArrayList<>(repo.findAll());
return userEntityList
.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
The example code returns all the list of users converted into a DTO.
public UserDto findUserById(final UUID id) {
var user = repo
.findById(id)
.orElseThrow(
() -> new NotFoundException("User by id " + id +
" was not found")
);
return convertToDto(user);
}
This example method retrieves a specific user using the findByID() method of the user repository.
Let’s place the code for the createSalt() method:
private byte[] createSalt() { var random = new SecureRandom(); var salt = new byte[128]; random.nextBytes(salt); return salt; }
The next method is createPasswordHash(), which will allow us to hash the user’s password. We use the SHA-512 hashing algorithm and the provided salt to create the method. The following code is for the createPasswordHash() implementation:
private byte[] createPasswordHash(String password, byte[] salt) throws NoSuchAlgorithmException { var md = MessageDigest.getInstance("SHA-512"); md.update(salt); return md.digest( password.getBytes(StandardCharsets.UTF_8)); }
The last method is the createUser() method itself. We will first check whether a password is provided and then whether the email address already exists using the selectExistsEmail() method we have created. Next, after all the validations have passed, make a salt using the createSalt() method and hash the password using createPasswordHash(). Lastly, save the new user in the database. The following code is for the createUser() implementation:
public UserDto createUser(UserDto userDto, String password) throws NoSuchAlgorithmException { var user = convertToEntity(userDto); if (password.isBlank()) throw new IllegalArgumentException( "Password is required." ); var existsEmail = repo.selectExistsEmail(user.getEmail()); if (existsEmail) throw new BadRequestException( "Email " + user.getEmail() + " taken" ); byte[] salt = createSalt(); byte[] hashedPassword = createPasswordHash(password, salt); user.setStoredSalt(salt); user.setStoredHash(hashedPassword); repo.save(user); return convertToDto(user); }
Let’s see the following code implementation:
public void updateUser(UUID id, UserDto userDto, String password) throws NoSuchAlgorithmException { var user = findOrThrow(id); var userParam = convertToEntity(userDto); user.setEmail(userParam.getEmail()); user.setMobileNumber(userParam.getMobileNumber()); if (!password.isBlank()) { byte[] salt = createSalt(); byte[] hashedPassword = createPasswordHash(password, salt); user.setStoredSalt(salt); user.setStoredHash(hashedPassword); } repo.save(user); } public void removeUserById(UUID id) { findOrThrow(id); repo.deleteById(id); } private UserEntity findOrThrow(final UUID id) { return repo .findById(id) .orElseThrow( () -> new NotFoundException("User by id " + id + " was not found") ); }
We have already created the services needed for our user entity. Now, the last step is to make our controller.
The last requirement for the user is to create the controller. We will create a method for findAllUsers(), findUserById(), deleteUserById(), createUser(), and putUser() under the annotated services with specific HTTP requests.
Let’s see the following code implementation:
@AllArgsConstructor @RestController public class UserController { private final UserService userService; @GetMapping("/api/v1/users") public Iterable<UserDto> getUsers() { return userService.findAllUsers(); } @GetMapping("/api/v1/users/{id}") public UserDto getUserById(@PathVariable("id") UUID id) { return userService.findUserById(id); } @DeleteMapping("/api/v1/users/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUserById(@PathVariable("id") UUID id) { userService.removeUserById(id); } @PostMapping("/register") @ResponseStatus(HttpStatus.CREATED) public UserDto postUser(@Valid @RequestBody UserDto userDto) throws NoSuchAlgorithmException { return userService.createUser(userDto, userDto.getPassword()); } @PutMapping("/api/v1/users/{id}") public void putUser( @PathVariable("id") UUID id, @Valid @RequestBody UserDto userDto ) throws NoSuchAlgorithmException { userService.updateUser(id, userDto, userDto.getPassword()); }
We have successfully created our endpoints for our user entity; we can now use the /register endpoint to create a new user for a valid authentication. Now, we will make the login endpoint using JWT.
JWT is a URL-safe method for communicating data. A JWT can be seen as an encrypted string containing a JSON object with a lot of information. It includes an additional structure consisting of a header payload that uses JSON format. JWTs can be encrypted or signed with a Message Authentication Code (MAC). A JWT is created by combining the header and payload JSON, and the whole token is Base64-URL-encoded.
JWT is used chiefly on RESTful web services that cannot maintain a client state since JWT holds some information connected to the user. It can provide state information to the server for each request. JWT is utilized in applications that require client authentication and authorization.
Let’s have a look at the following JWT example:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIi wibmFtZSI6IlNlaWppIFZpbGxhZnJhbmNhIiwiaWF0IjoxNTE2MjM5MDIyfQ.uhmdFM4ROwnerVam-zdYojURqrgL7WQRBRj-P8kVv6s
The JWT in the given example is composed of three parts – we can notice that it is divided with a dot (.) character. The first string is the encoded header, the second string is the encoded payload, and the last string is the signature of the JWT.
The following block is an example of the decoded structure:
// Decoded header { "alg": "HS256", "typ": "JWT" } // Decoded Payload { "sub": "1234567890", "name": "Seiji Villafranca", "iat": 1516239022 } // Signature HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret-key )
We can see in the preceding example that the three parts are JSON objects, the headers, which contain the algorithm used for signing the JWT, the payload, which holds information that can be used to define the state, and the signature, which encodes both the headers and the payload appended by the secret key.
We already know the concept and use of JWT; now, we will implement JWT generation in our Spring Boot project. We want to create an authentication endpoint that will return a valid JWT when a valid credential is submitted.
The first step is to add the JWT dependencies to our Spring Boot project.
Let’s add the following XML code to pom.xml:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.2</version> </dependency>
Next, we need to create a package named jwt under our project package, and after its creation, create packages called controllers, filters, models, services, and util. We will start making the necessary models for our authentication endpoint.
We need to create three models for our authentication. The first model is for the request, the next is for the response, and lastly, we have a model containing the user information and one to implement UserDetails from Spring Security.
For the request model, create a new class named AuthenticationRequest under the models’ package. The implementation of the model is shown in the following code:
@Data @NoArgsConstructor @AllArgsConstructor public class AuthenticationRequest implements Serializable { private String email; private String password; }
The request only needs the email address and the password, since these are the credentials we need to validate.
Then, for the response model, create a new class named AuthenticationResponse; the implementation of the model is shown in the following code:
@Data @NoArgsConstructor @AllArgsConstructor public class AuthenticationResponse implements Serializable { private String token; }
The response model only contains the token; the JWT is returned once the credentials are validated.
Lastly, for the user principal model, create a new class named UserPrincipal; the implementation of the model is shown in the following code:
@AllArgsConstructor public class UserPrincipal implements UserDetails { private final UserEntity userEntity; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return null; } @Override public String getUsername() { return this.userEntity.getEmail(); } // Code removed for brevity. Please refer using the // GitHub repo. @Override public boolean isEnabled() { return false; } }
The use principal model implements UserDetails as this will be our custom user for Spring Security. We have overridden several methods, such as getAuthorities(), which retrieves the list of authorizations of the user, isAccountNonLocked(), which checks whether the user is locked, isAccountNonExpired(), which validates that the user is valid and not yet expired, and isEnabled(), which checks whether the user is active.
We need to create the utilities for our authentication; the utilities will be responsible for the JWT creation, validation and expiration checks, and extraction of the information. These are the methods we will use to validate our token.
We will create a class named JwtUtil under the util package, and we will annotate this with an @Service annotation. Let’s start with the methods needed for util.
Let’s create the first two methods that we need to create a valid token:
private String createToken(Map<String, Object> claims, String subject) { Keys. return Jwts .builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, userDetails.getUsername()); }
The preceding implementation calls several methods from the JWT extension:
The next method we need to implement is the claims extraction. We will use this method mainly to get useful information, such as the subject and the expiration of the token.
Let’s have a look at the following code implementation:
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts .parserBuilder() .setSigningKey(SECRET_KEY) .build() .parseClaimsJws(token) .getBody(); }
The extractAllClaims() method receives the token and uses the secret key provided by the application. We have called the parseClaimsJWS() method to extract the claims from the JWT.
Now, we will create the methods to extract and check whether the token is expired and extract the username using the extractClaims() method we have created.
Let’s have a look at the following code implementation:
public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); }
We have used the getExpiration and getSubject built-in functions to get the expiration date and subject from the claims.
Lastly, we will create a method to validate that the token is not yet expired or a valid user is using the token.
Let’s have a look at the following code implementation:
public Boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return ( username.equals(userDetails.getUsername()) && !isTokenExpired(token) ); }
Now, we will create the service for our authentication, as we know that services are responsible for the logic of our application. We will make the following methods, which will verify whether the password is correct using the hash, check whether the user has valid credentials, and provide a method that will override the default authentication.
The first step is to create a new class named ApplicationUserDetailsService under the service package, and we will implement the class using UserDetailsService from Spring Security. We will override the loadUserByUsername() method and execute the following code:
@Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return new UserPrincipal( userService.searchByEmail(email)); }
The preceding code calls the searchByEmail() method, which is our custom implementation for checking whether a user exists, and we will return the user as a UserPrincipal object.
The next step is to create the verifyPasswordHash() method, which will validate the user’s password.
Let’s have a look at the following code implementation:
private Boolean verifyPasswordHash( String password, byte[] storedHash, byte[] storedSalt ) throws NoSuchAlgorithmException { // Code removed for brevety. Please refer to the GitHub // repo for (int i = 0; i < computedHash.length; i++) { if (computedHash[i] != storedHash[i]) return false; } // The above for loop is the same as below return MessageDigest.isEqual(computedHash, storedHash); }
The method we have created accepts the password, the stored salt, and the user’s hash. We will first check whether storedHash has a length of 64 and storedSalt has a size of 128 to validate whether it is 64 bytes. We will get the computed hash by using the stored salt and message digest for the password, and lastly, we will check whether the passwords match by seeing whether the calculated hash and stored hash are equal.
The last method we need to implement is the authenticate() method. This is the primary method that our authenticate endpoint will call.
Let’s have a look the following code implementation:
public UserEntity authenticate(String email, String password) throws NoSuchAlgorithmException { if ( email.isEmpty() || password.isEmpty() ) throw new BadCredentialsException("Unauthorized"); var userEntity = userService.searchByEmail(email); if (userEntity == null) throw new BadCredentialsException("Unauthorized"); var verified = verifyPasswordHash( password, userEntity.getStoredHash(), userEntity.getStoredSalt() ); if (!verified) throw new BadCredentialsException("Unauthorized"); return userEntity; }
The method first checks whether the user exists using the searchByEmail() method and checks whether the password is valid using the verifyPasswordHash() method that we have created.
Now, we will create the controllers of our authentication. This would create the primary endpoint for our login. The first step is to create a class named AuthenticateController under the controllers’ package, and next, we will make authenticate() with the following implementation:
@RestController @AllArgsConstructor class AuthenticateController { private final AuthenticationManager authenticationManager; private final JwtUtil jwtTokenUtil; private final ApplicationUserDetailsService userDetailsService; @RequestMapping(value = "/authenticate") @ResponseStatus(HttpStatus.CREATED) public AuthenticationResponse authenticate( @RequestBody AuthenticationRequest req ) throws Exception { UserEntity user; try { user = userDetailsService.authenticate( req.getEmail(), req.getPassword()); } catch (BadCredentialsException e) { throw new Exception("Incorrect username or password", e); }
Then, we get the details of the user by calling loadUserByUsername from userDetailsService but don’t forget to pass the email address of the user like so:
var userDetails = userDetailsService.loadUserByUsername(user.getEmail()); System.out.println(userDetails); var jwt = jwtTokenUtil.generateToken(userDetails); return new AuthenticationResponse(jwt); } }
The authenticate() method accepts an AuthenticationRequest body, which requires an email address and password. We will use service.authenticate() we previously created to check whether the credentials are valid. Once this is confirmed, we can generate the token using generateToken() from our utilities and return an AuthenticationResponse object.
The last step we need to accomplish is to create the filter for our authentication. We will use filters to validate each HTTP request with a valid JWT in the request headers. We need to make sure that a filter is invoked only once for each request. We can achieve this by using OncePerRequestFilter. We will extend our filter class with the filter to ensure that the filter is only executed once for a specific request.
Now, let’s create our authentication filter; first, let’s create a class named JwtRequestFilter under the filters package, and we will extend this class with OncePerRequestFilter, then we will override the doFilterInternal() method, which has parameters of HttpServletRequest, HttpServletResponse, and FilterChain. We will also inject ApplicationUserDetailsService and JwtUtil for the credentials and token validation.
Our code will look like the following:
@AllArgsConstructor @Component public class JwtRequestFilter extends OncePerRequestFilter { private final ApplicationUserDetailsService userDetailsService; private final JwtUtil jwtUtil; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain ) throws ServletException, IOException { } }
Now, for the implementation of the method, the first thing we need to do is extract the JWT from the request header. Let’s implement the following code:
//JWT Extraction final String authorizationHeader = request.getHeader("Authorization"); String username = null; String token = null; if ( authorizationHeader != null && authorizationHeader.startsWith("Bearer ") ) { token = authorizationHeader.substring(7); username = jwtUtil.extractUsername(token); }
The preceding code retrieves the JWT on the header with an authorization key, and when a token is retrieved, we will extract the username to check whether the user exists.
Then, the next step is to load the user’s details using the retrieved username and check that the token is valid and not yet expired. If the token is good, we will create a UsernamePasswordAuthenticationToken from the user details and the list of the authorized users.
We will set the new authenticated principal in our security context; let’s have a look the following code implementation:
//JWT Extraction section // JWT Validation and Creating the new // UsernamePasswordAuthenticationToken if ( username != null && SecurityContextHolder.getContext() .getAuthentication() == null ) { UserDetails userDetails = this.userDetailsService .loadUserByUsername(username); if (jwtUtil.validateToken(token, userDetails)) { var usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); usernamePasswordAuthenticationToken.setDetails( new WebAuthenticationDetailsSource() .buildDetails(request) ); SecurityContextHolder .getContext() .setAuthentication( usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); }
We have successfully created a filter for our requests, and our authentication endpoints are all configured. The only thing we need to do is finalize our configuration. We would want to modify UserDetailsService with our custom authentication.
To achieve this, we will go back to our SecurityConfig file and place the following code implementation on our configure(AuthenticationManagerBuilder auth) method:
private final ApplicationUserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); }
The next step is we need to add the filter we have created; under the configure(HttpSecurity http) method, we will place the following code:
private final JwtRequestFilter jwtFilter; @Override protected void configure(HttpSecurity http) throws Exception { …. Http security configurations http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); }
Now that our security configuration is complete, our final step is to add authentication to our anti-hero endpoints. A valid JWT is required upon making a request to the anti-hero endpoints.
To achieve this, we will annotate AntiHeroController with @PreAuthorize("isAuthenticated()") to configure the endpoints with the authentication process:
@PreAuthorize("isAuthenticated()") public class AntiHeroController { … methods }
We have successfully implemented Spring Security and JWT on our application; let’s simulate the endpoints created.
We will send an HTTP GET request for the anti-hero controller to get the list of all anti-heroes:
Figure 7.2 – 403 Forbidden on getting the anti-heroes list
When we send a sample request to one of the anti-heroes, this will now return a 403 error since it requires a valid token from our request headers. In this case, we need to create a new user using the /register endpoint:
Figure 7.3 – User registration
After successfully creating our user, this is now a valid credential, and we can log in using the /authenticate endpoint:
Figure 7.4 – New credential login
We can see in the preceding example that our login is successful and the /authenticate endpoint returned a valid token. We can now use the token in the request header to send a request to anti-hero endpoints:
Figure 7.5 – Anti-hero endpoint returns the list successfully
We can see in the preceding example that we have used the generated token in our authorization header, and we have received a 200 response and returned the list of anti-heroes.
We have now successfully created the custom authentication and authorization for our Spring Boot application using Spring Security. In the next section, we will discuss an additional topic relating to security, called IDaaS.
In the previous section, we created our custom login authentication using Spring Security. We utilized some of the features of Spring Security and also used JWT to store user states and validate credentials. However, this example is not enough of a reliable and secure way of implementing authentication for our application.
Large and enterprise applications nowadays demand several security features to be able to prevent possible vulnerabilities that can occur. These features can include the architecture and the implementation of other services, such as SSO and Multi-Factor Authentication (MFA). These features can be cumbersome to work with and can require several sprints to modify, leading to a longer time to develop. This is where IDaaS comes to the rescue.
IDaaS is a delivery model that allows users to connect, authenticate, and use identity management services from the cloud. IDaaS helps speed up the development process as all authentication and authorization processes are provided under the hood.
It is commonly used by large and enterprise applications because of the advantages and features it offers. IDaaS systems utilize the power of cloud computing to handle Identity Access Management (IAM), which ensures that the right users access the resources. It is very helpful as companies do not need to worry about security and IAM responsibilities, which are very demanding due to the adaptation of cybersecurity threats.
There are several types of IDaaS available on the market; some providers only provide clients with a directory, others offer several sets of features, which include SSO and MFA, but we will split IDaaS into two categories:
Small- and medium-sized businesses commonly use basic IDaaS. It usually provides SSO and MFA and a cloud directory for storing credentials.
Basic IDaaS providers are also packaged with a more straightforward interface that gives users the capability to handle configuration and administrative tasks.
Enterprise IDaaS, compared to basic IDaaS, is more complex and used by large and enterprise businesses. This is commonly used to extend the IAM infrastructure of the organization and provide access management to web, mobile, and API environments.
There are five requirements that an IDaaS should possess:
Those are the five characteristics required of an IDaaS.
If you are wondering about any examples of an IDaaS that you can use, here are some service providers:
To learn more about Google Cloud Identity, you can visit https://cloud.google.com/identity.
Okta and Auth0 joined forces around 2021, providing identity platforms and solutions such as universal login, password-less authentication, and machine-to-machine communication.
To learn more about Auth0 and Okta, you can visit the following links: https://auth0.com/ and https://www.okta.com/workforce-identity/.
To learn more about Azure Active Directory, you can visit https://azure.microsoft.com/en-us/services/active-directory/.
With this, we have reached the end of this chapter. Let’s have a recap of the valuable things you have learned. You have learned about the concept and importance of CORS and how it can provide security for accessing resources. We have discussed the different ways that we can implement CORS in our Spring Boot applications, which are at the method level, at the controller level, and a combination of both approaches.
We have also learned about the concept and features of Spring Security and discussed the implementation of custom authentication and authorization in our application. Lastly, we have also learned about IDaaS, a delivery model that allows users to connect, authenticate, and use identity management services from the cloud.
In the next chapter, we will be learning about the integration of event loggers into our Spring Boot application.
18.222.193.207