Hello everyone,
Greetings today!
Today we will learn how to secure REST API with JWT Authentication in Spring Boot, so let's get started
- What is JWT
- JWT stands for JSON Web Token which is used to securely share information/claims between client and server for authorization purposes.
- Structure of JWT
- JWT consists of three parts separated by dots.
- Header
- Payload
- Signature
- JWT Header - Consists of information about the type of token & signing algorithm.
- JWT Payload- It consists of a set of claims such as issuer, expiration, subject, and role. The payload is Base64 encoded and forms the second part of the token
- JWT Signature - To create a signature, we need to take the encoded header + encoded payload + secret and sign it using the algorithm specified in the header. Signatures are used to verify all changes in the token.
- Example of JWT Token
- Below is an example JWT token that can be generated at the end of this example. You can access the JWT IO to decode the token below and understand the structure above.
- eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0QHlvcG1haWwuY29tIiwiaXNzIjoiZXhhbXBsZS5pbyIsIlJPTEUiOiJTVFVERU5UIiwiU1VCSkVDVF9JRCI6MywiaWF0IjoxNjU3Mzg0MTgxLCJleHAiOjE2NTc5ODg5ODF9.wMCQyZfa4cJCG6Qf5Fn7-yEJtQiI-NiC1DqrQ1NomSUovnOCGY4bNhYwDpV04a1pdSg21E5fICVA6dzQBsHxGw
Now you should know what a JWT is and how a JWT token is constructed. Create a Spring Boot project to learn Spring REST API authentication with JWT.
Go to the Spring Initializr, create a project with the following dependencies, and open the project in your favorite IDE.
Project Description
- We will be using PostgreSQL as a database.
- We will create 2 Rest Controller
- UserController has below endpoints that are unsecured i.e. anyone can access these endpoints to register/log in.
- Register User
- Login - This will provide JWT Token in response which will be used to access secured endpoints.
- ProductController
- Add Product - This API endpoint is a secure endpoint, so users with an EMPLOYEE role can access this API endpoint.
- We will be creating the below tables for storing user details
- auth_user - Storing user details & role id of a user.
- role - Storing Roles available in our system
Now let's start developing.
First, let's add database-related configuration to the application.properties file.
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.username=add_your_db_username
spring.datasource.password=add_your_db_password
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto = update
Next, add a model package and create User.java & Role.java classes to map your entities to the auth_user & role table.
User.java
package com.demo.jwt.model; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.persistence.*; @Entity @Table(name = "auth_user") @Builder @Data @AllArgsConstructor @NoArgsConstructor public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; @Column(unique = true) private String emailAddress; private String password; private String address; @ManyToOne(cascade = CascadeType.PERSIST) private Role role; }
Role.java
package com.demo.jwt.model; import lombok.Data; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "role") @Data public class Role { @Id @Column private Long id; private String name; }
Next, add a repository package and create a RoleRepository and UserRepository to access the database. Since we use Spring JPA, we extend JpaRepository. We will also add some methods that we will use later.
RoleRepository.java
package com.demo.jwt.repository; import com.demo.jwt.model.Role; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface RoleRepository extends JpaRepository<Role,Long> { Optional<Role> findByName(String name); }
UserRepository.java
package com.demo.jwt.repository; @Repository public interface UserRepository extends JpaRepository<User,Long>{ Optional<User> findByEmailAddress (String emailAddress); }
Next, let's create a dto package and add the following DTO classes for requests and responses from our REST API.
- UserDTO
- LoginDTO
- ResponseDTO
UserDTO.java
package com.demo.jwt.dto; import lombok.Data; @Data public class UserDTO { private String name; private String emailAddress; private String password; private String address; private String roleName; }
LoginDTO.java
package com.demo.jwt.dto; import lombok.Data; @Data public class LoginDTO { private String emailAddress; private String password; }
ResponseDTO.java
package com.demo.jwt.dto; import lombok.Builder; import lombok.Data; @Data @Builder public class ResponseDTO<T> { private String responseMsg; private T json; }
Then create an exception package and add BadRequestException. This extends the RunTimeException that throws in case of bad requests.
BadReqeustException.java
package com.demo.jwt.exception; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class BadRequestException extends RuntimeException{ private String msg; }
Next, we will add JSON web token dependency in pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Next, let's create a util package and add a JwtUtil.java class with methods for JWT validation, JWT token generation, and JWT token parsing.
Note - Currently we are only adding a method that generates a JWT token. Add the rest of the methods as needed.
Generating a token requires specifying an expiration date and time, an associated role, a signature algorithm, and a secret. You can also add a set of claims that are used to exchange information between the client and server.
JwtUtil.java
package com.demo.jwt.util; import com.demo.jwt.dto.JwtTokenDTO; import com.demo.jwt.model.User; import io.jsonwebtoken.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtUtil { private final Logger logger= LoggerFactory .getLogger(JwtUtil.class); private final String jwtSecret = "testsecret"; private final String jwtIssuer = "test_issuer"; public String generateAccessToken(User user) { return Jwts.builder() .setSubject(user.getEmailAddress()) .setIssuer(jwtIssuer) .claim("ROLE", user.getRole().getName()) .claim("SUBJECT_ID",user.getId()) .setIssuedAt(new Date()) .setExpiration( new Date(System.currentTimeMillis()+ 7 * 24 * 60 * 60 * 1000)) // 1 week .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact(); } }
Next, create a configuration package with the BeanConfiguration.java class where you can define all the beans your application needs. For now, let's add a PasswordEncoder bean that will be used to encrypt passwords that we store in the database.
BeanConfiguration.java
package com.demo.jwt.config; @Configuration public class BeanConfiguration { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
Next, let's create a UserController under the Controller package. UserController defines two endpoints for registering users and logins that can be accessed by anyone without authentication
package com.demo.jwt.controller; import com.demo.jwt.dto.LoginDTO; import com.demo.jwt.dto.ResponseDTO; import com.demo.jwt.dto.UserDTO; import com.demo.jwt.service.UserService; import org.springframework.beans.factory.annotation.Autowired; 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; @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/register") public ResponseEntity<ResponseDTO> addUser(@RequestBody UserDTO userDTO){ userService.registerUser(userDTO); return new ResponseEntity<> (ResponseDTO.builder() .responseMsg("User register successfully.") .build(), HttpStatus.CREATED); } @PostMapping("/login") public ResponseEntity<ResponseDTO> login(@RequestBody LoginDTO loginDTO){ String token = userService.login(loginDTO); return new ResponseEntity<> (ResponseDTO.builder() .json(token) .responseMsg("Login successful.") .build(), HttpStatus.OK); } }
In the above code, UserController is calling the UserService method for registration & login purposes so let's create a service package with the UserService.java interface.
UserService.java
package com.demo.jwt.service; import com.demo.jwt.dto.LoginDTO; import com.demo.jwt.dto.UserDTO; public interface UserService { void registerUser(UserDTO userDTO); String login(LoginDTO loginDTO); }
Then create an impl package under the service package and define the implementation of the above service methods in UserServiceImpl.java.
UserServiceImpl.java
package com.demo.jwt.service.impl; import com.demo.jwt.dto.LoginDTO; import com.demo.jwt.dto.UserDTO; import com.demo.jwt.exception.BadRequestException; import com.demo.jwt.model.Role; import com.demo.jwt.model.User; import com.demo.jwt.repository.RoleRepository; import com.demo.jwt.repository.UserRepository; import com.demo.jwt.service.UserService; import com.demo.jwt.util.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password .PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service @Transactional public class UserServiceImpl implements UserService { @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; @Autowired private PasswordEncoder passwordEncoder; @Autowired private JwtUtil jwtUtil; @Override public void registerUser(UserDTO userDTO) { //TODO Add Validation To Restrict Roles If Needed. //TODO Add Validations For Unique Email Optional<Role> optionalRole = roleRepository.findByName(userDTO.getRoleName()); if(optionalRole.isEmpty()){ throw new BadRequestException("Select a valid role."); } User user=User.builder() .name(userDTO.getName()) .address(userDTO.getAddress()) .emailAddress(userDTO.getEmailAddress()) .role(optionalRole.get()) .password(passwordEncoder.encode (userDTO.getPassword())) .build(); userRepository.save(user); } @Override public String login(LoginDTO loginDTO) { Optional<User> userOptional= userRepository.findByEmailAddress (loginDTO.getEmailAddress()); if(userOptional.isEmpty()){ throw new BadRequestException("User Not Found."); } if(passwordEncoder.matches (loginDTO.getPassword(), userOptional.get().getPassword()) ){ return jwtUtil.generateAccessToken (userOptional.get()); }else{ throw new BadRequestException("Invalid UserName Or Password"); } } }
Note - You should use the PasswordEncode matching method to ensure that the encrypted password you store in the database matches the plain password that the user enters at login. If the authentication succeeds, it calls the JwtUtil 's generateAccessToken() method that has already been defined.
Next, create an exception handler package and an underlying GlobalExceptionHandler.java class that handles all exceptions thrown by your code.
package com.demo.jwt.exceptionhandler; import com.demo.jwt.dto.ResponseDTO; import com.demo.jwt.exception.BadRequestException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @ControllerAdvice public class GlobalExceptionHandler { private final Logger logger= LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler({BadRequestException.class}) public ResponseEntity<ResponseDTO> handleBadRequestException(BadRequestException e) { logger.info("Bad Request Found {} ",e.getMsg(),e); return new ResponseEntity<>( ResponseDTO.builder(). responseMsg(e.getMsg()).build(), HttpStatus.BAD_REQUEST); } @ExceptionHandler({Exception.class}) public ResponseEntity<ResponseDTO> handleException(Exception e) { logger.info("Unknown error occur {} ", e.getMessage(),e); return new ResponseEntity<>( ResponseDTO.builder() .responseMsg(e.getMessage()).build(), HttpStatus.INTERNAL_SERVER_ERROR); } }
Next, add a JwtTokenDTO that will be used to store details after token parsing, and a Principal.java class that contains basic getters and setters.
Principal.java
package com.demo.jwt.dto; import lombok.Data; @Data public class Principal { private String emailAddress; private String token; private String role; }
JwtTokenDTO.java
package com.demo.jwt.dto; import lombok.Builder; import lombok.Data; import java.util.Date; @Builder @Data public class JwtTokenDTO { private String subject; private Date expirationDate; private String role; }
Next, under the JwtUtil.java class, add methods to validate and parse the JWT token using the secret key used to sign the token.
//TODO Throw And Handle Below Exceptions Under Global Exception Handler public boolean validate(String token) { try { Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token); return true; } catch (MalformedJwtException ex) { logger.error("Invalid JWT token - {}", ex.getMessage()); } catch (ExpiredJwtException ex) { logger.error("Expired JWT token - {}", ex.getMessage()); } catch (UnsupportedJwtException ex) { logger.error("Unsupported JWT token - {}", ex.getMessage()); } catch (IllegalArgumentException ex) { logger.error("JWT claims string is empty - {}", ex.getMessage()); } return false; } public JwtTokenDTO getJwtTokenDTO(String token){ Claims claims = Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token).getBody(); return JwtTokenDTO.builder().subject(claims.getSubject()) .expirationDate(claims.getExpiration()) .role(claims.get("ROLE", String.class)) .build(); }
Next, let's create a SpringSecurityConfiguration.java class under the configuration package. Here we define a SecurityFilterChain bean, define a lot of configurations such as the session creation policy as stateless, and also disable cors and csrf. Also, add configuration to expose the UserController API endpoint as public. And secured other API endpoints.
package com.demo.jwt.config; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringSecurityConfiguration { @Autowired private JwtTokenFilter jwtTokenFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity = httpSecurity.cors() .and().csrf().disable(); httpSecurity=httpSecurity.sessionManagement() .sessionCreationPolicy (SessionCreationPolicy.STATELESS) .and(); httpSecurity.authorizeRequests() .antMatchers(HttpMethod.POST,"/user/**") .permitAll() .anyRequest().authenticated(); httpSecurity.addFilterBefore (jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); } }
Now let's create JwtTokenFilter.java under the filter package. This filter uses the JwtUtil method to validate the token. Also create a UserNamePasswordAuthenticationToken and add a SecurityContextHolder.
package com.demo.jwt.filter; @Component public class JwtTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain ) throws ServletException, IOException { final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } final String token = header.split(" ")[1].trim(); if (!jwtUtil.validate(token)) { filterChain.doFilter(request, response); return; } JwtTokenDTO jwtTokenDTO=jwtUtil.getJwtTokenDTO(token); List<GrantedAuthority> grantedAuthorityList= new ArrayList<>(); grantedAuthorityList.add( new SimpleGrantedAuthority(jwtTokenDTO.getRole())); Principal principal=new Principal(); principal.setToken(token); principal.setEmailAddress(jwtTokenDTO.getSubject()); principal.setRole(jwtTokenDTO.getRole()); UsernamePasswordAuthenticationToken authenticationToken= new UsernamePasswordAuthenticationToken (principal,null,grantedAuthorityList); SecurityContextHolder.getContext() .setAuthentication(authenticationToken); filterChain.doFilter(request,response); } }
All necessary configuration for JWT authentication is complete. Now it's time to test. Now let's create a ProductController under the controller package with a simple endpoint (secure endpoint) for adding products.
ProductController.java
package com.demo.jwt.controller; @RestController @RequestMapping("/product") public class ProductController { private final Logger logger= LoggerFactory.getLogger(ProductController.class); @PostMapping @PreAuthorize("hasAuthority('EMPLOYEE')") public ResponseEntity<ResponseDTO> addProduct(){ //NOTE This method is just to check add product //is called or not. logger.info("Add product called successfully."); return new ResponseEntity<>( ResponseDTO.builder().responseMsg("Product Added.") .build(), HttpStatus.CREATED); } }
We specified that all users with the EMPLOYEE role should be granted access to the Add Product endpoint using Spring Security's @PreAuthorize annotation.
Next, we need to add a record in the role table using the below query.
insert into role values(1,'EMPLOYEE');
You can register a single user with the Registration API endpoint as shown below.
Note - We need to pass the token under Authorization in postman with Type as Bearer Token as shown below.
Also, trying to access the product endpoint without adding a token will return a 403 Forbidden as shown below.
Thanks!
Enjoy your learning!
Other reference articles
0 Comments
If you have any doubts let me know.