Project Archive

Why we need to improve security

In the version of the auction application that I have implemented so far I implemented a mild security feature. By using UUIDs instead of simple integers to identify users I have made it harder for malicious users to cause damage in our system.

For example, to post a profile for a user in our system we have to supply a URL of the form

/users/<id>/profile

where <id> is replaced by the UUID of the user in question. This is a little more secure, because UUIDs are much harder to guess than simple integers.

This is a relatively weak security strategy that the computer security community refers to as a security through obscurity strategy. This strategy has one very big flaw: if a malicious user obtains the UUID for another user in the system, they can cause a lot of trouble with that information. In fact, if we are not careful in the design of our system we can accidentally have our system reveal those UUIDs to attackers.

In fact, the current system already actively leaks out those UUIDs. All an attacker has to do is to request the list of Bids for an auction. Since the Bid objects returned will include the UUIDs of the users who placed the bids, the attacker will have ready access to a bunch of valid UUIDs.

Better security

An improved system of security should make use of a more careful system of control to regulate access to key parts of a system. Here are the key aspects of such a system:

Since computer security is an important consideration in most real-world systems, it is no surprise that Spring Boot offers a way to implement such a security system through the Spring Security module.

Basics of Spring Security

To add the Spring Security module to our application we start by adding a dependency to our pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

The Spring Security module is quite complex. Most of this complexity comes from the fact that over time multiple mechanisms have been developed to handle authentication for server applications. Spring Security needs to be flexible enough to accommodate a wide range of authentication methods. In these notes I am going to limit myself to discussing one very specific application of Spring Security: protecting access to methods on a Spring Boot REST server.

Regardless of which security mechanism we use, Spring Security will require us to structure our code in a very specific way. Spring Security will require us to create Authentication objects as evidence that a particular user has been authenticated. Authentication objects contain UserDetail objects that carry additional information about the user in question.

When a REST request arrives at the server, it will first go through a special mechanism called a filter chain. We will be inserting code into the filter chain that checks whether or not the request requires authentication, and if it does our code will check for the presence of a token to authenticate the request. If we are able to authenticate the request we will create the necessary Authentication object, and that object will travel down to the controller where the controller can use that object to get useful information about the user making the request.

Working with tokens

Our authentication mechanism will be based on the use of security tokens. To start the process, a user will log in to our server by posting a user object to the URL /users/login. That particular method does not require authentication, so the request will be delivered directly to the controller method. That method will check the user name and password of the user against the database. If the login is valid, the method will respond by returning a security token in the body of its response.

If the user then wants to access a resource on the server that requires authentication, they will be expected to send that token along with the request. To do this, they will insert an HTTP header to the request that takes the form

Authorization: Bearer <token>

where <token> is the token they received at login time.

We still need a mechanism to implement a secure token. The mechanism we are going to use is a widely used type of token called the JSON web token, or JWT for short.

A JWT consists of a body and a signature. The body contains a subject, an expiration time, and an optional set of additional claims. We will be using the subject section to store the UUID for a logged in user. We will not be making use of any additional claims. JWTs have a signature designed to give us confidence that the token is legitimate, and that it has not been altered from its original form. Specifically, to create a signature for our JWT we will use a library that combines the body of the JWT with a secret key value known only to the server in a signing algorithm. That signing algorithm is designed so that if we don't know the secret key, or if we alter the body in any way we will not be able to compute the correct signature. When a JWT arrives at the server the library we will use can check that the signature is valid and that the body has not been altered.

The library we will use to create and check our tokens is the jjwt library. To use it we will need to add some additional dependencies to our project:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>

Next, we create a Service class for the project to handle generating and checking JWTs:

@Service
public class JwtService {

  SecretKey key = Jwts.SIG.HS256.key().build();

  public boolean isValid(String token) {
    try {
      return Jwts.parser()
          .verifyWith(key)
          .build()
          .parseSignedClaims(token)
          .getPayload()
          .getExpiration()
          .after(new Date(System.currentTimeMillis()));
    } catch (Exception ex) {

    }
    return false;
  }

  public String getSubject(String token) {
    return Jwts.parser()
        .verifyWith(key)
        .build()
        .parseSignedClaims(token)
        .getPayload()
        .getSubject();
  }

  public String makeJwt(String userid) {
    return Jwts.builder()
        .subject(userid)
        .issuedAt(new Date(System.currentTimeMillis()))
        .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 30))
        .signWith(key)
        .compact();
  }
}

This service generates its own secret key each time the application launches. To create a JWT in the makeJwt() method it will sign the JWT with its key. The isValid() method starts by verifying the signature with the secret key. If the signature is OK, we go on and also check the expiration date. If the token has not expired we declare the JWT to be valid.

Besides helping us authenticate users, JWTs will also carry useful information about the user. When we create a JWT for a user who has logged in successfully we will embed the UUID for that user in the JWT as the subject of the JWT. When the JWT comes back to use in a request we can extract that subject to obtain the UUID.

Here also is the updated code for the controller method that handles the login:

@PostMapping("/login")
public ResponseEntity<String> checkLogin(@RequestBody UserDTO user) {
    User result = us.findByNameAndPassword(user.getName(), user.getPassword());
    if (result == null) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid user name or password");
    }
    String token = jwtService.makeJwt(result.getUserid().toString());
    return ResponseEntity.ok().body(token);
}

Here we use the JwtService to make a token from the user's id.

Managing the Filter Chain

When a request arrives at the server it will have to pass through a filter chain on its way to the appropriate controller method. We can insert our own class into the filter chain to test for the presences of a JWT in an Authorization header.

Here is the code for that class:

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");
        String token = null;
        String userid = null;
        if(authHeader != null && authHeader.startsWith("Bearer ")){
            token = authHeader.substring(7);
            if(jwtService.isValid(token))
              userid = jwtService.getSubject(token);
        }

        if(userid != null && SecurityContextHolder.getContext().getAuthentication() == null){
            AuctionUserDetails userDetails = new AuctionUserDetails(userid);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        filterChain.doFilter(request, response);
    }
}

This component is designed to be inserted into the filter chain. It features one method that the request will pass through. That method will check to see if the request contains an Authorization header, look for the token in that header, and then ask the JwtService to check the token.

If the request passes the test, we will respond by constructing an Authentication object and inserting that object into the Spring Security security context. Spring Security requires that Authentication object to contain a UserDetails object. The UserDetails object is an object that implements the Spring Security UserDetails interface. For this project, I created my own custom class for this purpose:

public class AuctionUserDetails implements UserDetails {
  private static final long serialVersionUID = 1L;
  private String userid;
  private List<GrantedAuthority> authorities;
  
  public AuctionUserDetails(String id) {
    userid = id;
    authorities = new ArrayList<GrantedAuthority>();
    authorities.add(new SimpleGrantedAuthority("USER"));
  }
  
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorities;
  }

  @Override
  public String getPassword() {
    return null;
  }

  @Override
  public String getUsername() {
    return userid;
  }

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

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

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

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

}

UserDetails objects are expected to indentify users in some way and they also should be able to return a list of authorities granted to that user. The system of authorities makes it possible for us to grant different levels of privileges to different users in our system. For this example we will keep things simple by granting all users of our system the ordinary USER authority. If we want to, we can come back later and construct a separate class for administrators that grants those users, say, both the USER authority and the ADMIN authority. This will make it possible for us to set up controller methods are only accessible to users who have that elevated ADMIN authority.

Setting up the filter chain

Now that we have filter class to insert into the filter chain, we have to insert it. This is done by setting up a Spring Security security configuration by creating a configuration class:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Autowired
    JwtAuthFilter jwtAuthFilter;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(management -> management
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(authorize -> authorize.requestMatchers(HttpMethod.POST, "/users", "/users/login").permitAll()
            .requestMatchers(HttpMethod.GET, "/auctions", "/auctions/{id}/bids").permitAll()
                .anyRequest().authenticated()
                )
        .addFilterBefore( jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); 
        return http.build();
    }

}

There are two key method calls here: authorizeHttpRequests() and .addFilterBefore(). The second of these inserts our custom filter into the filter chain to check for JWTs. The first method allows us to distinguish between requests that require authentication and those that do not.

We will require no authentication for these four specific request types:

The order of the two method calls is significant here. We want to first give some requests a chance to bypass the security checks. Those requests that do require authentication will go on to our custom filter which will only authenticate those requests that have a valid JWT in an Authorization header. If a request requires authentication and the security context does not contain a valid Authorization at the end of the filter chain the server will reject that request with a 403, forbidden error.

Using Authentications in a controller

One nice side effect of this security mechanism is that it can deliver an Authentication object to a controller method on demand. Here is an example. In the previous version of our server we allowed users to post Shipping objects to the URL

/users/<id>/shipping

where <id> is the UUID of the user. In the new version we will simplify the URL to

/users/shipping

and change our controller method to this:

@PostMapping("/shipping")
public ResponseEntity<String> postShipping(Authentication authentication,@RequestBody ShippingDTO shipping) {
  AuctionUserDetails details = (AuctionUserDetails) authentication.getPrincipal();
  UUID id = UUID.fromString(details.getUsername());
  try {
    us.saveShipping(id,shipping);
  } catch(WrongUserException ex) {
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid user id");
  }
  return ResponseEntity.status(HttpStatus.CREATED).body("Shipping saved.");
}

The new version of the controller method adds a parameter that we can use to ask the security mechanism to give us the Authentication object generated by our custom filter. We know that that Authentication object contains an AuctionUserDetails object that can tell us the UUID of the user posting the request, so that information no longer needs to appear in the URL.

Using JWTs with Postman

If we want to test our new system in Postman, we can start by sending a login request.

To send a request that requires authentication we have to click on the Auth tab in the Postman window, select "Bearer Token" as the type of authorization to use, and then paste the JWT into the Token field.

With the authorization in place we can successfully send our request: