JWT Authentication with Spring Boot

JWT Authentication with Spring Boot

·

3 min read

Firstly I want to define some terminology used in the spring security context.

  • Authentication: verifying identity of a user based on provided credentials.
  • Authorization: ensuring if a user has proper permission to perform a particular action or read some data.

Folder structure:

image.png

Dependencies used:

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

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

To learn how spring security works refer to this URL Spring Security Architecture

I will be making /api/v1/demo which simply returns string.

controllers/AuthController.java

@RestController
@RequestMapping("/api/v1")
public class AuthController implements ErrorController {
    @RequestMapping({"/demo"})
    public String demoEndpoint(){
        return "Demo";
    }
}

security/SecurityConfigurer.java

@Configuration
@EnableWebSecurity
public class SecurityConfigurer  extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailService userDetailService;

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }

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

security/services/UserDetailService.java

@Service
public class UserDetailService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        return new User("demo", "demo", new ArrayList<>());
    }
}

1.png 2.png

Now I will introduce JWT(Json Web Token) into project.

JWT consists of three parts separated by (.), which are:

  • Header
  • Payload
  • Signature

refer:

image.png

The jjwt dependency helps to create and validate the JWT .

security/jwt/JWTUtil.java

@Component
public class JWTUtil {
    private String SECRET_KEY = "MySecretKey";

    public String getUsername(String token){
        return getClaim(token, Claims::getSubject);
    }

    public <T> T getClaim(String token, Function<Claims, T> claimResolver) {
        final Claims claims = getAllClaims(token);
        return claimResolver.apply(claims);
    }

    private Claims getAllClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    public Date getExpiration(String token){
        return getClaim(token, Claims::getExpiration);
    }

    private Boolean isTokenExpired(String token){
        return getExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails){
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

controllers/AuthController.java

@RestController
@RequestMapping("/api/v1")
public class AuthController implements ErrorController {
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JWTUtil jwtUtil;

    @RequestMapping({"/demo"})
    public String demoEndpoint(){
        return "Demo";
    }

    @RequestMapping(value = "/authenticate", method = RequestMethod.POST)
    public ResponseEntity<?> getAuthToken(@RequestBody AuthRequest authRequest) throws Exception{
        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            authRequest.getUsername(), authRequest.getPassword()
                    )
            );
        }catch (BadCredentialsException e){
            throw new Exception("Incorrect username or password", e);
        }

        final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
        String jwt = jwtUtil.generateToken(userDetails);
        return ResponseEntity.ok(new AuthResponse(jwt));

    }
}

As spring putting authentication around every request, we need to tell spring security not to expect authentication for /api/v1/authenticate/ endpoint.

security/SecurityConfigurer.java

@Configuration
@EnableWebSecurity
public class SecurityConfigurer  extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailService userDetailService;

    @Autowired
    private  JwtRequestFilter jwtRequestFilter;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/api/v1/authenticate").permitAll()
                .anyRequest().authenticated()
                .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

    }

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

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
    }

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

models/AuthRequest.java

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AuthRequest {
    private String username;
    private String password;
}

models/AuthResponse.java

@Getter
@AllArgsConstructor
public class AuthResponse {
    private final String jwt;
}

To make use of JWT, we need to intercept all incoming requests and get jwt from header. Further by extracting username and password from JWT, we can set username in security context by validating jwt.

security/filters/JwtRequestFilter.java

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
    @Autowired
    private JWTUtil jwtUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authorizationHeader = request.getHeader("Authorization");

        String jwt_token = null;
        String username = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
            jwt_token = authorizationHeader.substring(7);
            username = jwtUtil.getUsername(jwt_token);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
            if (jwtUtil.isTokenValid(jwt_token, userDetails)){
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                );

                usernamePasswordAuthenticationToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

Now if I try to hit /api/v1/demo , response will be 403 Forbidden

image.png

I will create a JWT by hitting /api/v1/authenticate and pass as Authorization header to /api/v1/demo/

image.png

Now, I can see the output response.

image.png

Did you find this article valuable?

Support Sumit by becoming a sponsor. Any amount is appreciated!