loading...

Getting started with Spring Security - Adding JWT

jhonifaber profile image Jonathan Faber Updated on ・4 min read

This is the second part of the spring security post I started.

Json Web Token: standard that defines a self-contained way for transmitting information as a JSON object. Consist of three parts separated by dots.

  • Header: signing algorithm(SHA256,HS512...) + type of the token
  • Payload: contains the claims.
  • Signature: header(base64 encoded) + payload(base64 encoded) + a secret and all encoded with the algorithm specified in the header.

Claim: piece of information in the body of the token.

First of all, add the dependency that allows us to create jwt's and validate them.

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>


Authenticate user and return token

We are going to create a class which have all the related features about jwt. In the following example, when creating the token, Im just adding the subject, the authorities of that user(We just have one and is ROLE_SENSEI, check out MyUserDetails class), and the expiration time. You can add custom claims with claim(key, value) or pass a map of claims to setClaims(). I'm signing the token with the string "key" for this example. In a real project, it could be retrieved from the application configuration file.

To read the jwt, you need to pass the key to validate the signature of token and call parseClaimsJws with the token, then you will be able to get the body.

@Service
public class JwtService {
    private static final int EXPIRATION_TIME = 1000 * 60 * 60;
    private static final String AUTHORITIES = "authorities";
    private final String SECRET_KEY;

    public JwtService() {
        SECRET_KEY = Base64.getEncoder().encodeToString("key".getBytes());
    }

    public String createToken(UserDetails userDetails) {
        String username = userDetails.getUsername();
        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
        return Jwts.builder()
                .setSubject(username)
                .claim(AUTHORITIES, authorities)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }

    public Boolean hasTokenExpired(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getExpiration()
                .before(new Date());
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        String username = extractUsername(token);
        return (userDetails.getUsername().equals(username) && !hasTokenExpired(token));

    }

    public String extractUsername(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public Collection<? extends GrantedAuthority> getAuthorities(String token) {
        Claims claims = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
        return (Collection<? extends GrantedAuthority>) claims.get(AUTHORITIES);
    }
}

When the user tries to log in, we expect a username and a password (userAuthenticationRequest) and if the authentication goes well, we will respond with the token(AuthenticationResponse).

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticationRequest {
    private String username;
    private String password;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticationResponse {
    private String token;
}

In our controller, we autowired AuthenticationManager so that we can authenticate the passed object.
We need to build an Authentication object by using UsernamePasswordAuthenticationToken, it receives two params, the principal(username) and the credentials(password). This object is passed to AuthenticationProvider who is responsible for doing the validation.
If the validation is successful, the token is created, otherwise throws an exception.

@RestController
@RequiredArgsConstructor
public class UserController {

    private final AuthenticationManager authenticationManager;
    private final MyUserDetailService myUserDetailService;
    private final JwtService jwtService;

    @PostMapping("/login")
    public AuthenticationResponse createToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
        try {
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword());
            authenticationManager.authenticate(authentication);
        } catch (BadCredentialsException e) {
            throw new Exception("Invalid username or password", e);
        }
        UserDetails userDetails = myUserDetailService.loadUserByUsername(authenticationRequest.getUsername());
        String token = jwtService.createToken(userDetails);
        return new AuthenticationResponse(token);
    }
}

My security config class is as follows. You need to override authenticationManagerBean in order to autowired it. Here I'm allowing everyone to /login but for any other resource you must be authenticated.

@EnableWebSecurity
public class WebSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserDetailService myUserDetailService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailService);
    }
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests().antMatchers("/login").permitAll()
                .anyRequest().authenticated();
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

Remember I have the same UserDetails class from the previous post, so the password is 'pass' and the username could be whatever. So, if we try to log in, we can see the token returned.

postman log in


Intercept request

SecurityContext is used to store the details of the currently authenticated user.

We are now going to extract the token from the authorization header and validate it. To intercept a request we use filters.
First, We create JwtAuthorizationFilter that will be executed once per request and is responsible for user authorization.
We now get the token from the header, extract the username and check the token is valid. If everything is fine, we build the Authentication object with those user details, set the user in the SecurityContext and allow the request to move on with filterChain.doFilter.
I have defined my constants as class fields but it could be better to create a class 'JwtConstants' and have them all there.

@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private static final String HEADER_TOKEN_PREFIX = "Bearer ";
    private static final String HEADER_AUTHORIZATION = "Authorization";

    @Autowired
    private MyUserDetailService myUserDetailService;

    @Autowired
    private JwtService jwtService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
        if (authorizationHeader != null && authorizationHeader.startsWith(HEADER_TOKEN_PREFIX)) {
            String token = authorizationHeader.replace(HEADER_TOKEN_PREFIX, "");
            String username = jwtService.extractUsername(token);

            UserDetails userDetails = myUserDetailService.loadUserByUsername(username);
            if (jwtService.validateToken(token, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        filterChain.doFilter(request, response);
    }
}

In our webSecurity class, we will add sessionManagement to be stateless, because we don't want spring to create any session. Secondly, we'll add the created filter. This means, read JwtAuthorizationFilter before the UsernamePasswordFilter.

    @Autowired
    private JwtAuthorizationFilter jwtAuthorizationFilter;

//...

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests().antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and().addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
    }

//...

Let's try it on postman, I have created this GET to test what we've done.

    @GetMapping("/test")
    public String getTest(){
        return "test";
    }

"token postman"

To check the statelessness, try making the request again without the Authorization header, you will see how you get a 403 Forbidden because each request is self-contained.

Posted on by:

Discussion

pic
Editor guide