Spring Boot + Spring Security + JWT Authentication and Authorization Example

Hello everyone, Hope you are doing well, Today we will learn how to handle authentication and authorization on RESTful APIs written with Spring Boot. The GitHub repository link is provided at the end of this tutorial. You can download the source code.

A little bit of Background

Spring Boot

Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can "just run". 


Spring Security

Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first-class support for securing both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.


JSON Web Token(JWT)

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. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.


MongoDB

MongoDB is a document database designed for ease of application development and scaling.


Token-based Authentication system flow diagram




Final Project Directory



Maven[pom.xml] 

Puts spring-boot-starter-data-mongodb, spring-boot-starter-webspring-boot-starter-security, jjwt dependencies.
<?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>2.7.1</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev</groupId>
<artifactId>spring-boot-security-jwt-mongodb</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-security-jwt-mongodb</name>
<description>Demo project for Spring Boot </description>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<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>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>


application.yaml

knf:
app:
jwtExpirationMs: 76300000
jwtSecret: knowledgeFactory
spring:
data:
mongodb:
database: knfDemoDb
host: localhost
port: 27017


Creat Employee Document

@Document(collection = "employees")
public class Employee {
@Id
private String id;

private String employeename;

private String email;

private String password;

@DBRef
private Set<Role> roles = new HashSet<>();

public Employee() {
}

public Employee(String employeename,
String email, String password) {
super();
this.employeename = employeename;
this.email = email;
this.password = password;
}

public String getEmployeename() {
return employeename;
}

public void setEmployeename(String employeename) {
this.employeename = employeename;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Set<Role> getRoles() {
return roles;
}

public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
  • @Id annotation is currently used by Spring to support mapping for other non-relational persistence databases or frameworks that do not have a defined common persistence API like JPA.
  • @Document is an annotation provided by the Spring Data project. It is used to identify a domain object, which is persisted to MongoDB. So you can use it to map a Java class into a collection inside MongoDB.


Create Role Document

@Document(collection = "roles")
public class Role {
@Id
private String id;
@Indexed(unique = true)
private ERole name;

public Role() {

}

public Role(ERole name) {
this.name = name;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public ERole getName() {
return name;
}

public void setName(ERole name) {
this.name = name;
}
}


Create Enum ERole

public enum ERole {
ROLE_EMPLOYEE, ROLE_ADMIN
}


Create Employee Repository

public interface EmployeeRepository 
extends MongoRepository<Employee, String> {

Optional<Employee> findByEmployeename(String employeename);

Boolean existsByEmployeename(String employeename);

Boolean existsByEmail(String email);
}
MongoRepository is just a specialized PagingAndSortingRepository suited to Mongo, which in turn is a specialized CrudRepository.


Create Role Repository

public interface RoleRepository 
extends MongoRepository<Role, String> {

Optional<Role> findByName(ERole name);
}


Create Service EmployeeDetailsImpl

public class EmployeeDetailsImpl implements UserDetails {

private static final long serialVersionUID = 1L;

private String id;

private String username;

private String email;

@JsonIgnore
private String password;

private Collection<? extends GrantedAuthority> authorities;

public EmployeeDetailsImpl(String id,
String username, String email, String password,
Collection<? extends GrantedAuthority> authorities) {

this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}

public static EmployeeDetailsImpl build(Employee user) {

List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority
(role.getName().name()))
.collect(Collectors.toList());

return new EmployeeDetailsImpl(user.getId(),
user.getEmployeename(), user.getEmail(),
user.getPassword(),authorities);
}

@Override
public Collection<? extends
GrantedAuthority> getAuthorities() {

return authorities;
}

public String getId() {
return id;
}

public String getEmail() {
return email;
}

@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;
EmployeeDetailsImpl user = (EmployeeDetailsImpl) o;
return Objects.equals(id, user.id);
}
}


Create Service EmployeeDetailsServiceImpl

@Service
public class EmployeeDetailsServiceImpl
implements UserDetailsService {

@Autowired
EmployeeRepository employeeRepository;

@Override
@Transactional
public UserDetails loadUserByUsername(String employeename)
throws UsernameNotFoundException {

Employee employee = employeeRepository
.findByEmployeename(employeename)
.orElseThrow(() -> new UsernameNotFoundException
("Employee Not Found with username: "
+ employeename));

return EmployeeDetailsImpl.build(employee);
}
}


Create Jwt Utility

@Component
public class JwtUtils {

private static final Logger logger = LoggerFactory
.getLogger(JwtUtils.class);

@Value("${knf.app.jwtExpirationMs}")
private int jwtExpirationMs;
@Value("${knf.app.jwtSecret}")
private String jwtSecret;

public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret)
.parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}

return false;
}

public String generateJwtToken(Authentication authentication) {

EmployeeDetailsImpl employeePrincipal =
(EmployeeDetailsImpl) authentication.getPrincipal();

return Jwts.builder().setSubject((employeePrincipal
.getUsername())).setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime()
+ jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
}

public String getEmployeeNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret)
.parseClaimsJws(token).getBody().getSubject();
}

}


Create AuthTokenFilter

public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;

@Autowired
private EmployeeDetailsServiceImpl employeeDetailsService;

private static final Logger logger = LoggerFactory
.getLogger(AuthTokenFilter.class);

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {

String employeename = jwtUtils
.getEmployeeNameFromJwtToken(jwt);

UserDetails employeeDetails = employeeDetailsService
.loadUserByUsername(employeename);

UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
employeeDetails, null,
employeeDetails.getAuthorities());

authentication.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));

SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set employee authentication: {}", e);
}

filterChain.doFilter(request, response);
}

private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");

if (StringUtils.hasText(headerAuth)
&& headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7, headerAuth.length());
}

return null;
}
}


Create AuthEntryPointJwt

@Component
public class AuthEntryPointJwt
implements AuthenticationEntryPoint {

private static final Logger logger = LoggerFactory
.getLogger(AuthEntryPointJwt.class);

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {

logger.error("Unauthorized error: {}",
authException.getMessage());

response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Error: Unauthorized");
}

}


Create Web Security Configuration

WebSecurityConfigurerAdapter is deprecated, So going forward, the recommended way of doing this is registering a SecurityFilterChain bean like below.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {

@Autowired
private AuthEntryPointJwt unauthorizedHandler;

@Autowired
EmployeeDetailsServiceImpl employeeDetailsService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {

http.cors().and().csrf().disable().exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy
(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.anyRequest()
.authenticated();

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

return http.build();
}

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

@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}

@Bean
public AuthenticationManager authenticationManager
(AuthenticationConfiguration authenticationConfiguration)
throws Exception {

return authenticationConfiguration
.getAuthenticationManager();
}
}


Create Login Request

public class LoginRequest {

private String employeename;

private String password;

public String getEmployeename() {
return employeename;
}

public void setEmployeename(String employeename) {
this.employeename = employeename;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

Create Signup Request

public class SignupRequest {

private String employeename;

private String email;

private Set<String> roles;

private String password;

public String getEmployeename() {
return employeename;
}

public void setEmployeename(String employeename) {
this.employeename = employeename;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Set<String> getRoles() {
return this.roles;
}

public void setRole(Set<String> roles) {
this.roles = roles;
}
}


Create Jwt Response

public class JwtResponse {

private String token;
private String type = "Bearer";
private String id;
private String employeename;
private String email;
private List<String> roles;

public JwtResponse(String accessToken,
String id, String employeename,
String email, List<String> roles) {

this.token = accessToken;
this.id = id;
this.employeename = employeename;
this.email = email;
this.roles = roles;
}

public String getAccessToken() {
return token;
}

public void setAccessToken(String accessToken) {
this.token = accessToken;
}

public String getTokenType() {
return type;
}

public void setTokenType(String tokenType) {
this.type = tokenType;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getEmployeename() {
return employeename;
}

public void setEmployeename(String employeename) {
this.employeename = employeename;
}

public List<String> getRoles() {
return roles;
}
}


Create Message Response

public class MessageResponse {
private String message;

public MessageResponse(String message) {
this.message = message;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}


Create Auth Controller

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;

@Autowired
EmployeeRepository employeeRepository;

@Autowired
RoleRepository roleRepository;

@Autowired
PasswordEncoder encoder;

@Autowired
JwtUtils jwtUtils;

@PostMapping("/signin")
public ResponseEntity<?> authenticateEmployee
(@RequestBody LoginRequest loginRequest) {

Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken
(loginRequest.getEmployeename(),
loginRequest.getPassword()));

SecurityContextHolder.getContext()
.setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);

EmployeeDetailsImpl employeeDetails = (EmployeeDetailsImpl)
authentication.getPrincipal();
List<String> roles = employeeDetails.getAuthorities()
.stream().map(item -> item.getAuthority())
.collect(Collectors.toList());

return ResponseEntity.ok(new JwtResponse(jwt,
employeeDetails.getId(),
employeeDetails.getUsername(),
employeeDetails.getEmail(), roles));
}

@PostMapping("/signup")
public ResponseEntity<?> registerUser
(@RequestBody SignupRequest signUpRequest) {

if (employeeRepository.existsByEmployeename
(signUpRequest.getEmployeename())) {

return ResponseEntity.badRequest()
.body(new MessageResponse
("Error: Employeename is already taken!"));
}

if (employeeRepository
.existsByEmail(signUpRequest.getEmail())) {

return ResponseEntity.badRequest()
.body(new MessageResponse
("Error: Email is already in use!"));
}

// Create new employee account
Employee employee = new Employee(signUpRequest
.getEmployeename(), signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));

Set<String> strRoles = signUpRequest.getRoles();
Set<Role> roles = new HashSet<>();

if (strRoles == null) {
Role employeeRole = roleRepository
.findByName(ERole.ROLE_EMPLOYEE)
.orElseThrow(() -> new RuntimeException
("Error: Role is not found."));
roles.add(employeeRole);
} else {
strRoles.forEach(role -> {
switch (role) {
case "admin":
Role adminRole = roleRepository
.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException
("Error: Role is not found."));
roles.add(adminRole);

break;
default:
Role defaultRole = roleRepository
.findByName(ERole.ROLE_EMPLOYEE)
.orElseThrow(() -> new RuntimeException
("Error: Role is not found."));
roles.add(defaultRole);
}
});
}

employee.setRoles(roles);
employeeRepository.save(employee);

return ResponseEntity.ok(new MessageResponse
("Employee registered successfully!"));
}
}


Create Employee Controller

@CrossOrigin(origins = "*", maxAge = 4800)
@RestController
@RequestMapping("/api/test")
public class EmployeeController {
@GetMapping("/all")
public MessageResponse allAccess() {
return new MessageResponse("Public ");
}

@GetMapping("/employee")
@PreAuthorize("hasRole('EMPLOYEE') ")
public MessageResponse employeeAccess() {

return new MessageResponse("Employee zone");
}

@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public MessageResponse adminAccess() {
return new MessageResponse("Admin zone");
}
}


Spring Boot Main Driver

@SpringBootApplication
public class Application implements CommandLineRunner {
@Autowired
RoleRepository roleRepository;

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

/* Add some rows into roles collection
* before assigning any role to Employee. */
@Override
public void run(String... args) throws Exception {
try {
if (roleRepository.findAll().isEmpty()) {
Role role = new Role();
role.setName(ERole.ROLE_EMPLOYEE);
roleRepository.save(role);
Role role2 = new Role();
role2.setName(ERole.ROLE_ADMIN);
roleRepository.save(role2);
} else {

}
} catch (Exception e) {

}
}

}


Local Setup and Run the application

Step 1: Download or clone the source code from GitHub to the local machine - Click here


Step 2: mvn clean install


Step 3: Run the Spring Boot application - 

mvn spring-boot:run or Run as Spring Boot application.


Register employee

http://localhost:8080/api/auth/signup



Employee Sign in to an account

http://localhost:8080/api/auth/signin



Using accessToken access ROLE_EMPLOYEE resource

http://localhost:8080/api/test/employee



Register Admin

http://localhost:8080/api/auth/signup



Admin Sign in to an account

http://localhost:8080/api/auth/signin



Using accessToken access ROLE_ADMIN resource

http://localhost:8080/api/test/admin



More related topics,

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Spring Boot + Mockito simple application with 100% code coverage

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete