Back to Spring Boot Course

Spring Boot Course

JWT stands for JSON Web Token. JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

This information can be verified and trusted because it is digitally signed. The client will need to authenticate with the server using the credentials only once.

During this time the server validates the credentials and returns the client a JSON Web Token(JWT).

For all future requests the client can authenticate itself to the server using this JSON Web Token(JWT) and so does not need to send the credentials like username and password.

screenshot of the app

During the first request the client sends a POST request with username and password.

Upon successful authentication the server generates the JWT sends this JWT to the client. This JWT can contain a payload of data.

On all subsequent requests the client sends this JWT token in the header. Using this token the server authenticates the user. So we don't need the client to send the user name and password to the server during each request for authentication, but only once after which the server issues a JWT to the client.

A JWT payload can contain things like user ID so that when the client again sends the JWT, you can be sure that it is issued by you, and you can see to whom it was issued.

JWT has the following format header.payload.signature

screenshot of the app

An important point to remember about JWT is that the information in the payload of the JWT is visible to everyone.

So we should not pass any sensitive information like passwords in the payload.

We can encrypt the payload data if we want to make it more secure.

However we can be sure that no one can tamper and change the payload information. If this is done the server will recognize it.

Lets develop a sample application for better understanding

We will be configuring Spring Security and JWT for performing 2 operations

  • Generating JWT - Expose a POST API with mapping /api/v1/auth/authenticate. On passing correct username and password it will generate a JSON Web Token(JWT)
  • Validating JWT - If user tries to access GET API with mapping /api/v1/employees. It will allow access only if request has a valid JSON Web Token(JWT)

Generating JWT

screenshot of the app

screenshot of the app

Validating JWT

screenshot of the app

Let's add required dependencies in pom.xml

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.4.1</version> <relativePath /> <!-- lookup parent from repository --> </parent> <packaging>jar</packaging> <groupId>com.praveen.employee</groupId> <artifactId>emp-mgmt-service</artifactId> <version>1.0.0</version> <name>Employee Management Service</name> <description>Employee Management Service</description> <properties> <java.version>17</java.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <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> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency> <dependency> <groupId>com.google.collections</groupId> <artifactId>google-collections</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.8</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

Let's create the JwtUtil Class

package com.praveen.employee.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; @Component public class JwtUtil { @Value("${jwt.secret}") private String secretKey; public String generateToken(String username, List<String> roles) { Map<String, Object> claims = new HashMap<>(); claims.put("roles", roles); return Jwts.builder() .claims() .add(claims) .subject(username) .issuedAt(new Date(System.currentTimeMillis())) .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 1 hour expiration .and() .signWith(getKey()) .compact(); } private SecretKey getKey() { byte[] keyBytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(keyBytes); } public String extractUserName(String token) { // extract the username from jwt token return extractClaim(token, Claims::getSubject); } private <T> T extractClaim(String token, Function<Claims, T> claimResolver) { final Claims claims = extractAllClaims(token); return claimResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser() .verifyWith(getKey()) .build() .parseSignedClaims(token) .getPayload(); } public boolean validateToken(String token, UserDetails userDetails) { final String userName = extractUserName(token); return (userName.equals(userDetails.getUsername()) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } private Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } }

Let's include jwt key in application.yml file which is a 256 bit key.

spring: application: name: emp-mgmt-service datasource: url: jdbc:postgresql://localhost:5432/praveenoruganti username: postgres password: praveen hikari: minimum-idle: 5 maximum-pool-size: 10 jpa: hibernate: ddl-auto: update show-sql: true format-sql: true jwt: secret: YXBpa2V5YXBpY2FsbXBhZCBzZWNyZXQyMjM0YXR1bmYwZGVzY2x1ZmY=

Let's create the UserPrincipal class

package com.praveen.employee.bo; import com.praveen.employee.entity.Role; import com.praveen.employee.entity.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; public class UserPrincipal implements UserDetails { private final String username; private final String password; private final Collection<? extends GrantedAuthority> authorities; public UserPrincipal(User user, Collection<? extends GrantedAuthority> authorities) { this.username = user.getUsername(); this.password = user.getPassword(); this.authorities = authorities; } public UserPrincipal(User user) { this.username = user.getUsername(); this.password = user.getPassword(); this.authorities = user.getRoles().stream() .map(Role::getName) .map(SimpleGrantedAuthority::new) .toList(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @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; } }

Let's create the MyUserDetailsService class

package com.praveen.employee.service; import com.praveen.employee.bo.UserPrincipal; import com.praveen.employee.entity.User; import com.praveen.employee.repository.UserRepository; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class MyUserDetailsService implements UserDetailsService { private final UserRepository userRepository; public MyUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username)); return new UserPrincipal(user); } }

Let's create the JWTFilter Class

package com.praveen.employee.config; import com.praveen.employee.service.MyUserDetailsService; import com.praveen.employee.utils.JwtUtil; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component public class JwtFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final ApplicationContext context; public JwtFilter(JwtUtil jwtUtil, ApplicationContext context) { this.jwtUtil = jwtUtil; this.context = context; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); String token = null; String username = null; if (authHeader != null && authHeader.startsWith("Bearer ")) { token = authHeader.substring(7); username = jwtUtil.extractUserName(token); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = context.getBean(MyUserDetailsService.class).loadUserByUsername(username); if (jwtUtil.validateToken(token, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }

Let's create the SecurityConfig Class

package com.praveen.employee.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { private final JwtFilter jwtFilter; private final UserDetailsService userDetailsService; public SecurityConfig(JwtFilter jwtFilter, UserDetailsService userDetailsService) { this.jwtFilter = jwtFilter; this.userDetailsService = userDetailsService; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(request -> request .requestMatchers("/api/v1/auth/authenticate").permitAll() .requestMatchers("/api/v1/auth/register").hasAnyAuthority("ROLE_ADMIN") .anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) .build(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(new BCryptPasswordEncoder(12)); provider.setUserDetailsService(userDetailsService); return provider; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }

Let's create the AuthService class

package com.praveen.employee.service; import com.praveen.employee.entity.Role; import com.praveen.employee.entity.User; import com.praveen.employee.exception.AuthenticationException; import com.praveen.employee.exception.UserAlreadyExistsException; import com.praveen.employee.repository.RoleRepository; import com.praveen.employee.repository.UserRepository; import com.praveen.employee.utils.JwtUtil; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.Set; @Service public class AuthService { private final JwtUtil jwtUtil; private final AuthenticationManager authenticationManager; private final UserRepository userRepository; private final RoleRepository roleRepository; public AuthService(JwtUtil jwtUtil, AuthenticationManager authenticationManager, UserRepository userRepository, RoleRepository roleRepository) { this.jwtUtil = jwtUtil; this.authenticationManager = authenticationManager; this.userRepository = userRepository; this.roleRepository = roleRepository; } private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); public User register(User user) { if (userRepository.existsByUsername(user.getUsername())) { throw new UserAlreadyExistsException("Username is already taken"); } if (user.getRoles() == null || user.getRoles().isEmpty()) { Set<Role> roles = new HashSet<>(); Role userRole = roleRepository.findByName("ROLE_USER") .orElseThrow(() -> new RuntimeException("Role not found: ROLE_USER")); roles.add(userRole); user.setRoles(roles); } user.setPassword(encoder.encode(user.getPassword())); return userRepository.save(user); } public String authenticate(User user) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()) ); if (user.getRoles() == null || user.getRoles().isEmpty()) { Set<Role> roles = new HashSet<>(); Role userRole = roleRepository.findByName("ROLE_USER") .orElseThrow(() -> new RuntimeException("Role not found: ROLE_USER")); roles.add(userRole); user.setRoles(roles); } if (authentication.isAuthenticated()) { return jwtUtil.generateToken(user.getUsername(), user.getRoles().stream().map(Role::getName).toList()); } else { throw new AuthenticationException("Authentication failed"); } } catch (BadCredentialsException e) { throw new AuthenticationException("Invalid username or password", e); } } }

Let's create the Auth Controller class

package com.praveen.employee.controller; import com.praveen.employee.entity.User; import com.praveen.employee.service.AuthService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/v1/auth") public class AuthController { private final AuthService service; public AuthController(AuthService service) { this.service = service; } @PostMapping("/register") public ResponseEntity<Map<String, String>> register(@RequestBody User user) { User registeredUser = service.register(user); Map<String, String> response = new HashMap<>(); response.put("message", "User registered successfully"); response.put("username", registeredUser.getUsername()); return ResponseEntity.status(HttpStatus.CREATED).body(response); } @PostMapping("/authenticate") public ResponseEntity<Map<String, String>> authenticate(@RequestBody User user) { String token = service.authenticate(user); Map<String, String> response = new HashMap<>(); response.put("token", token); return ResponseEntity.ok(response); } }

Let's create sample inserts for roles and the ADMIN user.

INSERT INTO roles (name) VALUES ('ROLE_ADMIN'),('ROLE_USER'); INSERT INTO users (username, password) VALUES ('praveen', '$2a$10$jG02uIw1VhfBV/7gs/7V/e9SMC/3wWvlr1Qg7E4/PtxLQHsvHR/YO'); INSERT INTO user_roles (user_id, role_id) VALUES (1, 1);

Let's fetch the ADMIN bearer token now for registering a new user.

screenshot of the app

Let's register a user using the above ADMIN bearer token.

screenshot of the app

screenshot of the app

Let's access the employee endpoint using the USER bearer token now.

screenshot of the app

screenshot of the app