DEV Community

Jonathan
Jonathan

Posted on • Edited on

Getting started with Spring Security - Adding JWT

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>


Enter fullscreen mode Exit fullscreen mode


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);
    }
}


Enter fullscreen mode Exit fullscreen mode

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;
}


Enter fullscreen mode Exit fullscreen mode


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


Enter fullscreen mode Exit fullscreen mode

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);
    }
}


Enter fullscreen mode Exit fullscreen mode

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();
    }
}


Enter fullscreen mode Exit fullscreen mode

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);
    }
}


Enter fullscreen mode Exit fullscreen mode

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);
    }

//...


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)