User sign in with JWT in Spring

Hi. This is the fourteenth part of the diary about developing the “Programmers’ diary” blog. The open source code of this project is on https://github.com/TheProgrammersDiary. The thirteenth part: https://hashnode.programmersdiary.com/building-a-blogging-website-with-microservices-sign-up.

The first diary entry contains explanation on why this project was done: https://medium.com/@vievaldas/developing-a-website-with-microservices-part-1-the-idea-fe6e0a7a96b5.

Next entry:

—————

2023-11-03:

OK, we have sign up, let’s use it: we will implement login.

The login will be facilitated with JWT tokens.

By using jwt token, API client is able to identify itself against the server.

Simply speaking:

  1. You login

  2. Server creates a jwt token and sends it to you

  3. You send that token to server for every following request that requires authorization

  4. When token expires, you need to generate a new one (this can be done either by logging in again, or e.g. refreshing refreshing the token when a request is made).

Let’s start the implementation with imports:

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

The updated SecurityConfig:

@Configuration
@EnableMethodSecurity
public class SecurityConfig {
    private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
    @Autowired
    UserDetailsServiceImpl userDetailsService;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .exceptionHandling(
                        exception -> exception.authenticationEntryPoint(
                                (request, response, authException) -> {
logger.error("Unauthorized error: {}", authException.getMessage());
                                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
                                }
                        )
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(requests -> requests.anyRequest().permitAll());
        http.authenticationProvider(authenticationProvider());
        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public static PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
}

In SecurityFilterChain we add an exception handler if authentication fails. We use stateless session so that Spring does not create the session itself and let’s JWT handle it. We also need to implement user details service:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return UserDetailsImpl.build(
                userRepository
                        .findByUsername(username)
                        .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username))
        );
    }
}

We use our userRepository we created previously. If the user does not exist we throw error, if it does exist, we instantiate this class:

public class UserDetailsImpl implements UserDetails {
    private static final longserialVersionUID= 1L;
    private final String id;
    private final String username;
    private final String email;
    @JsonIgnore
    private final String password;

    public UserDetailsImpl(String id, String username, String email, String password) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.password = password;
    }

    public static UserDetailsImpl build(UserRepository.UserEntry user) {
        return new UserDetailsImpl(
                user.getId(),
                user.getUsername(),
                user.getEmail(),
                user.getPassword());
    }

    public String getId() {
        return id;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return new ArrayList<>();
    }

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

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

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

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

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

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

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        UserDetailsImpl user = (UserDetailsImpl) o;
        return Objects.equals(id, user.id);
    }
}

Via this class, we can handle user credentials.

In UserController we add login method:

@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody User user) {
    Authentication authentication = authManager.authenticate(
            new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())
    );
    SecurityContextHolder.getContext().setAuthentication(authentication);
    return ResponseEntity.ok(jwtUtils.generateJwtToken(authentication));
}

The code will try to authenticate the user indirectly using the authenticationProvider we noted in SecurityConfig. The raw password will be encoded before matching it with password stored in the database.

Here is the JwtUtils class, which is used to generate, validate and extract user details from the token:

@Component
public class JwtUtils {
    private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);

    private final SecretKey key = Jwts.SIG.HS256.key().build();
    private final int jwtExpirationMs = 1000 * 60;

    public String generateJwtToken(Authentication authentication) {
        return Jwts.builder()
                .subject(((UserDetailsImpl) authentication.getPrincipal()).getUsername())
                .issuedAt(new Date())
                .expiration(new Date((new Date()).getTime() + jwtExpirationMs))
                .signWith(key)
                .compact();
    }

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

    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parser().verifyWith(key).build().parse(authToken);
            return true;
        } catch(ExpiredJwtException | MalformedJwtException | SecurityException | IllegalArgumentException e) {
logger.error("Exception while trying to validate JWT token: {}", e.getMessage());
            return false;
        }
    }
}

We have short-lived tokens by providing expiration Date near the current date, making the tokens I would say very safe. Even if a hacker stole the token from the user it would expire very quickly making the token useless.

OK, let’s try to create an account:

and login:

Whoops, wrong password. Let’s try again:

We have got JWT token as a response. Now we can use it to authenticate against the server for future requests.

But first, lets make sure we authorize our endpoints:

.authorizeHttpRequests(
        requests -> requests
                .requestMatchers("/users/signup", "/users/login", "/actuator/prometheus")
                .permitAll()
                .anyRequest()
                .authenticated()
);

We do this in SecurityConfig. Note that signup and login do not require authentication since users use these endpoint to establish it. Prometheus endpoint is also excluded since in other case Prometheus would not be able to scrape metrics. All other requests require authentication.

Since we have retrieved our JWT token while logging in, we need to use it. We still need to add some configuration in Security config:

@Bean
public JwtFilter jwtFilter() {
    return new JwtFilter();
}

and

http.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);

in security filter chain method. We need to create a filter for JWT authentication and execute before security filter chain (security chain executes first, if we don’t add JWT authentication before it, the request will be rejected with status: UNAUTHORIZED since the logic which establishes authorization is not in the chain).

We add jwtFilter() as a bean since we need to let Spring handle all injections (if done by hand like http.addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class); error will be given since by creating instance by hand we don’t have access to Spring injections and JwtFilter has @Autowired meaning it expects Spring to inject these dependencies). Talking about JwtFilter:

public class JwtFilter extends OncePerRequestFilter {
    private static final Loggerlogger = LoggerFactory.getLogger(JwtFilter.class);
    @Autowired
    private JwtUtils jwtUtils;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        try {
            Optional<String> jwt = parseJwt(request);
            if (jwt.isPresent() && jwtUtils.validateJwtToken(jwt.get())) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(
                        jwtUtils.getUserNameFromJwtToken(jwt.get())
                );
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities()
                        );
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception e) {
logger.error("Cannot set user authentication: ", e);
        }
        filterChain.doFilter(request, response);
    }

    private Optional<String> parseJwt(HttpServletRequest request) {
        String headerAuth = request.getHeader("Authorization");
        if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
            return Optional.of(headerAuth.substring(7));
        }
        return Optional.empty();
    }
}

This filter uses the data from JWT token to check if username and password are valid. If they are, the user is authenticated and the request will be authorized.

I login and get the token: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyaXMiLCJpYXQiOjE2OTkwMTk4MTEsImV4cCI6MTY5OTAyMDQxMX0.UYpB3uKBDVOQ3f27bJNUFsLizHOxZ2WbvsvuNnOvOXY.

Now I can use it by placing it in request headers:

The request is OK.

If I modify the token, I get UNAUTHORIZED as response.

Cool, next thing to do is to implement JWT in FE, since without JWT some functionality is broken.

—————

Thanks for reading.

The project logged in this diary is open source, so if you would like to code or suggest changes, please visit https://github.com/TheProgrammersDiary.

You can check out the website: https://www.programmersdiary.com/.

Next part coming soon.