Spring Boot, MongoDB: JWT Authentication with Spring Security

This tutorial helps you build a Spring Boot Authentication (Login & Registration) & role-based Authorization example with JWT, Spring Security and Spring Data MongoDB. You’ll know:

  • Appropriate Flow for User Signup & User Login with JWT Authentication
  • Spring Boot Application Architecture with Spring Security
  • How to configure Spring Security to work with JWT
  • How to define Data Models and association for Authentication and Authorization
  • Way to use Spring Data MongoDB to interact with MongoDB Database

More Practice:
Spring Boot JWT Authentication with Spring Security, Spring Data JPA & MySQL/PostgreSQL
Spring Boot + GraphQL + MongoDB example with Spring Data & graphql-java
Spring Boot with MongoDB CRUD example using Spring Data


Spring Boot JWT Authentication with MongoDB example

Let’s me describe our Spring Boot application.

  • User can signup new account, or login with username & password.
  • By User’s role (admin, moderator, user), we authorize the User to access resources (role-based Authorization)

So we’re gonna provide APIs as following table:

MethodsUrlsActions
POST/api/auth/signupsignup new account
POST/api/auth/signinlogin an account
GET/api/test/allretrieve public content
GET/api/test/useraccess User’s content
GET/api/test/modaccess Moderator’s content
GET/api/test/adminaccess Admin’s content

– Spring Security will manage cors, csrf, session, rules for protected resources, authentication & authorization along with exception handler.
– The database we will use is MongoDB which can be accessed by the help of Spring Data MongoDB.

Spring Boot Signup & Login with JWT Authentication Flow

The diagram shows flow of how we implement User Registration, User Login and Authorization process.

spring-boot-mongodb-jwt-authentication-spring-security-flow

A legal JWT must be added to HTTP Authorization Header if Client accesses protected resources.

This app can be used as a back-end that works well with these front-end applications (I’ve tested all of them):

Spring Boot Server Architecture with Spring Security

You can have an overview of our Spring Boot Server with the diagram below:

spring-boot-mongodb-jwt-authentication-spring-security-architecture

Let me explain it briefly.

Spring Security

WebSecurityConfigurerAdapter is the crux of our security implementation. It provides HttpSecurity configurations to configure cors, csrf, session management, rules for protected resources. We can also extend and customize the default configuration that contains the elements below.

UserDetailsService interface has a method to load User by username and returns a UserDetails object that Spring Security can use for authentication and validation.

UserDetails contains necessary information (such as: username, password, authorities) to build an Authentication object.

UsernamePasswordAuthenticationToken gets {username, password} from login Request, AuthenticationManager will use it to authenticate a login account.

AuthenticationManager has a DaoAuthenticationProvider (with help of UserDetailsService & PasswordEncoder) to validate UsernamePasswordAuthenticationToken object. If successful, AuthenticationManager returns a fully populated Authentication object (including granted authorities).

OncePerRequestFilter makes a single execution for each request to our API. It provides a doFilterInternal() method that we will implement parsing & validating JWT, loading User details (using UserDetailsService), checking Authorizaion (using UsernamePasswordAuthenticationToken).

AuthenticationEntryPoint will catch authentication error.

Repository contains UserRepository & RoleRepository to work with Database, will be imported into Controller.

Controller receives and handles request after it was filtered by OncePerRequestFilter.

AuthController handles signup/login requests

TestController has accessing protected resource methods with role based validations.

Technology

  • Java 8
  • Spring Boot 2.2.4 (with Spring Security, Spring Web, Spring Data MongoDB)
  • jjwt 0.9.1
  • MongoDB
  • Maven 3.6.1

Project Structure

This is folders & files structure for our Spring Boot application:

spring-boot-mongodb-jwt-auth-spring-security-project-structure

security: we configure Spring Security & implement Security Objects here.

  • WebSecurityConfig extends WebSecurityConfigurerAdapter
  • UserDetailsServiceImpl implements UserDetailsService
  • UserDetailsImpl implements UserDetails
  • AuthEntryPointJwt implements AuthenticationEntryPoint
  • AuthTokenFilter extends OncePerRequestFilter
  • JwtUtils provides methods for generating, parsing, validating JWT

controllers handle signup/login requests & authorized requests.

  • AuthController: @PostMapping(‘/signin’), @PostMapping(‘/signup’)
  • TestController: @GetMapping(‘/api/test/all’), @GetMapping(‘/api/test/[role]’)

repository has intefaces that extend Spring Data MongoDB MongoRepository to interact with Database.

  • UserRepository extends MongoRepository<User, String>
  • RoleRepository extends MongoRepository<Role, String>

models defines two main models for Authentication (User) & Authorization (Role). They have many-to-many relationship.

  • User: id, username, email, password, roles
  • Role: id, name

payload defines classes for Request and Response objects

We also have application.properties for configuring Spring Data MongoDB and App properties (such as JWT Secret string or Token expiration time).

Setup new Spring Boot project

Use Spring web tool or your development tool (Spring Tool Suite, Eclipse, Intellij) to create a Spring Boot project.

Then open pom.xml and add these 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>

Configure Spring Data MongoDB & App properties

Under src/main/resources folder, open application.properties, add these lines.

spring.data.mongodb.database=bezkoder_db
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017

# App Properties
bezkoder.app.jwtSecret= bezKoderSecretKey
bezkoder.app.jwtExpirationMs= 86400000

Create the models

We’re gonna have 2 collections in database: users & roles.

Let’s define these models.
In models package, create 3 files:

ERole enum in ERole.java. There are 3 roles corresponding to 3 enum.

package com.bezkoder.spring.jwt.mongodb.models;

public enum ERole {
  ROLE_USER,
  ROLE_MODERATOR,
  ROLE_ADMIN
}

Role model in Role.java

package com.bezkoder.spring.jwt.mongodb.models;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "roles")
public class Role {
  @Id
  private String id;

  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;
  }
}

User model in User.java with 5 fields: id, username, email, password, roles.

package com.bezkoder.spring.jwt.mongodb.models;

import java.util.HashSet;
import java.util.Set;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "users")
public class User {
  @Id
  private String id;

  @NotBlank
  @Size(max = 20)
  private String username;

  @NotBlank
  @Size(max = 50)
  @Email
  private String email;

  @NotBlank
  @Size(max = 120)
  private String password;

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

  public User() {
  }

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

  public String getId() {
    return id;
  }

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

  public String getUsername() {
    return username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  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;
  }
}

You can see that we annotate each model with @Document. The annotation specifies domain object to be persisted to MongoDB.

Implement Repositories

Now, each model above needs a repository for persisting and accessing data. In repository package, we’re gonna create 2 repositories.

UserRepository

There are 3 necessary methods that MongoRepository supports.

package com.bezkoder.spring.jwt.mongodb.repository;

import java.util.Optional;

import org.springframework.data.mongodb.repository.MongoRepository;

import com.bezkoder.spring.jwt.mongodb.models.User;

public interface UserRepository extends MongoRepository<User, String> {
  Optional<User> findByUsername(String username);

  Boolean existsByUsername(String username);

  Boolean existsByEmail(String email);
}

RoleRepository

This repository also extends MongoRepository and provides a finder method.

package com.bezkoder.spring.jwt.mongodb.repository;

import java.util.Optional;

import org.springframework.data.mongodb.repository.MongoRepository;

import com.bezkoder.spring.jwt.mongodb.models.ERole;
import com.bezkoder.spring.jwt.mongodb.models.Role;

public interface RoleRepository extends MongoRepository<Role, String> {
  Optional<Role> findByName(ERole name);
}

Configure Spring Security

In security package, create WebSecurityConfig class that extends WebSecurityConfigurerAdapter.

WebSecurityConfig.java

package com.bezkoder.spring.jwt.mongodb.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.bezkoder.spring.jwt.mongodb.security.jwt.AuthEntryPointJwt;
import com.bezkoder.spring.jwt.mongodb.security.jwt.AuthTokenFilter;
import com.bezkoder.spring.jwt.mongodb.security.services.UserDetailsServiceImpl;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
		// securedEnabled = true,
		// jsr250Enabled = true,
		prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	UserDetailsServiceImpl userDetailsService;

	@Autowired
	private AuthEntryPointJwt unauthorizedHandler;

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

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

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

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

	@Override
	protected void configure(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);
	}
}

@EnableWebSecurity allows Spring to find and automatically apply the class to the global Web Security.

@EnableGlobalMethodSecurity provides AOP security on methods. It enables @PreAuthorize, @PostAuthorize, it also supports JSR-250. You can find more parameters in configuration in Method Security Expressions.

– We override the configure(HttpSecurity http) method from WebSecurityConfigurerAdapter interface. It tells Spring Security how we configure CORS and CSRF, when we want to require all users to be authenticated or not, which filter (AuthTokenFilter) and when we want it to work (filter before UsernamePasswordAuthenticationFilter), which Exception Handler is chosen (AuthEntryPointJwt).

– Spring Security will load User details to perform authentication & authorization. So it has UserDetailsService interface that we need to implement.

– The implementation of UserDetailsService will be used for configuring DaoAuthenticationProvider by AuthenticationManagerBuilder.userDetailsService() method.

– We also need a PasswordEncoder for the DaoAuthenticationProvider. If we don’t specify, it will use plain text.

Implement UserDetails & UserDetailsService

If the authentication process is successful, we can get User’s information such as username, password, authorities from an Authentication object.

Authentication authentication = 
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(username, password)
        );

UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()

If we want to get more data (id, email…), we can create an implementation of this UserDetails interface.

security/services/UserDetailsImpl.java

package com.bezkoder.spring.jwt.mongodb.security.services;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.bezkoder.spring.jwt.mongodb.models.User;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class UserDetailsImpl 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 UserDetailsImpl(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 UserDetailsImpl build(User user) {
		List<GrantedAuthority> authorities = user.getRoles().stream()
				.map(role -> new SimpleGrantedAuthority(role.getName().name()))
				.collect(Collectors.toList());

		return new UserDetailsImpl(
				user.getId(), 
				user.getUsername(), 
				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;
		UserDetailsImpl user = (UserDetailsImpl) o;
		return Objects.equals(id, user.id);
	}
}

Look at the code above, you can notice that we convert Set<Role> into List<GrantedAuthority>. It is important to work with Spring Security and Authentication object later.

UserDetailsService will be used for getting UserDetails object. You can look at UserDetailsService interface that has following method:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Now we implement it to override loadUserByUsername() method.

security/services/UserDetailsServiceImpl.java

package com.bezkoder.spring.jwt.mongodb.security.services;

import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.transaction.annotation.Transactional;

import com.bezkoder.spring.jwt.mongodb.models.User;
import com.bezkoder.spring.jwt.mongodb.repository.UserRepository;

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

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

		return UserDetailsImpl.build(user);
	}

}

In the code above, we get full custom User object using UserRepository, then we build a UserDetails object using static build() method.

Filter the Requests

Let’s define a filter that executes once per request. So we create AuthTokenFilter class that extends OncePerRequestFilter and override doFilterInternal() method.

security/jwt/AuthTokenFilter.java

package com.bezkoder.spring.jwt.mongodb.security.jwt;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import com.bezkoder.spring.jwt.mongodb.security.services.UserDetailsServiceImpl;

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

  @Autowired
  private UserDetailsServiceImpl userDetailsService;

  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 username = jwtUtils.getUserNameFromJwtToken(jwt);

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        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 String parseJwt(HttpServletRequest request) {
    String headerAuth = request.getHeader("Authorization");

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

    return null;
  }
}

What we do inside doFilterInternal():
– get JWT from the Authorization header (by removing Bearer prefix)
– if the request has JWT, validate it, parse username from it
– from username, get UserDetails to create an Authentication object
– set the current UserDetails in SecurityContext using setAuthentication(authentication) method.

After this, everytime you want to get UserDetails, just use SecurityContext like this:

UserDetails userDetails =
	(UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()

Create JWT Utility class

This class has 3 funtions:

  • generate a JWT from username, date, expiration, secret
  • get username from JWT
  • validate a JWT

security/jwt/JwtUtils.java

package com.bezkoder.spring.jwt.mongodb.security.jwt;

import java.util.Date;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import com.bezkoder.spring.jwt.mongodb.security.services.UserDetailsImpl;
import io.jsonwebtoken.*;

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

	@Value("${bezkoder.app.jwtSecret}")
	private String jwtSecret;

	@Value("${bezkoder.app.jwtExpirationMs}")
	private int jwtExpirationMs;

	public String generateJwtToken(Authentication authentication) {

		UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();

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

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

	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;
	}
}

Remember that we’ve added bezkoder.app.jwtSecret and bezkoder.app.jwtExpirationMs properties in application.properties file.

Handle Authentication Exception

Now we create AuthEntryPointJwt class that implements AuthenticationEntryPoint interface. Then we override the commence() method. This method will be triggerd anytime unauthenticated User requests a secured HTTP resource and an AuthenticationException is thrown.

security/jwt/AuthEntryPointJwt.java

package com.bezkoder.spring.jwt.mongodb.security.jwt;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@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");
	}

}

HttpServletResponse.SC_UNAUTHORIZED is the 401 Status code. It indicates that the request requires HTTP authentication.

We’ve already built all things for Spring Security. The next sections of this tutorial will show you how to implement Controllers for our RestAPIs.

Define payloads for Spring RestController

Let me summarize the payloads for our RestAPIs:
– Requests:

  • LoginRequest: { username, password }
  • SignupRequest: { username, email, password }

– Responses:

  • JwtResponse: { token, type, id, username, email, roles }
  • MessageResponse: { message }

I don’t show these POJOs here for keeping the tutorial not so long,
You can find details for payload classes in source code of the project on Github.

Create Spring RestAPIs Controllers

Controller for Authentication

This controller provides APIs for register and login actions.

/api/auth/signup

  • check existing username/email
  • create new User (with ROLE_USER if not specifying role)
  • save User to database using UserRepository

/api/auth/signin

  • authenticate { username, pasword }
  • update SecurityContext using Authentication object
  • generate JWT
  • get UserDetails from Authentication object
  • response contains JWT and UserDetails data

controllers/AuthController.java

package com.bezkoder.spring.jwt.mongodb.controllers;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
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 com.bezkoder.spring.jwt.mongodb.models.ERole;
import com.bezkoder.spring.jwt.mongodb.models.Role;
import com.bezkoder.spring.jwt.mongodb.models.User;
import com.bezkoder.spring.jwt.mongodb.payload.request.LoginRequest;
import com.bezkoder.spring.jwt.mongodb.payload.request.SignupRequest;
import com.bezkoder.spring.jwt.mongodb.payload.response.JwtResponse;
import com.bezkoder.spring.jwt.mongodb.payload.response.MessageResponse;
import com.bezkoder.spring.jwt.mongodb.repository.RoleRepository;
import com.bezkoder.spring.jwt.mongodb.repository.UserRepository;
import com.bezkoder.spring.jwt.mongodb.security.jwt.JwtUtils;
import com.bezkoder.spring.jwt.mongodb.security.services.UserDetailsImpl;

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

	@Autowired
	UserRepository userRepository;

	@Autowired
	RoleRepository roleRepository;

	@Autowired
	PasswordEncoder encoder;

	@Autowired
	JwtUtils jwtUtils;

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

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

		SecurityContextHolder.getContext().setAuthentication(authentication);
		String jwt = jwtUtils.generateJwtToken(authentication);
		
		UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();		
		List<String> roles = userDetails.getAuthorities().stream()
				.map(item -> item.getAuthority())
				.collect(Collectors.toList());

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

	@PostMapping("/signup")
	public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
		if (userRepository.existsByUsername(signUpRequest.getUsername())) {
			return ResponseEntity
					.badRequest()
					.body(new MessageResponse("Error: Username is already taken!"));
		}

		if (userRepository.existsByEmail(signUpRequest.getEmail())) {
			return ResponseEntity
					.badRequest()
					.body(new MessageResponse("Error: Email is already in use!"));
		}

		// Create new user's account
		User user = new User(signUpRequest.getUsername(), 
							 signUpRequest.getEmail(),
							 encoder.encode(signUpRequest.getPassword()));

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

		if (strRoles == null) {
			Role userRole = roleRepository.findByName(ERole.ROLE_USER)
					.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
			roles.add(userRole);
		} 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;
				case "mod":
					Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR)
							.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
					roles.add(modRole);

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

		user.setRoles(roles);
		userRepository.save(user);

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

Controller for testing Authorization

There are 4 APIs:
/api/test/all for public access
/api/test/user for users has ROLE_USER or ROLE_MODERATOR or ROLE_ADMIN
/api/test/mod for users has ROLE_MODERATOR
/api/test/admin for users has ROLE_ADMIN

Remember that we used @EnableGlobalMethodSecurity(prePostEnabled = true) for WebSecurityConfig class?

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }

Now we can secure methods in our Apis with @PreAuthorize annotation easily.

controllers/TestController.java

package com.bezkoder.spring.jwt.mongodb.controllers;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/test")
public class TestController {
	@GetMapping("/all")
	public String allAccess() {
		return "Public Content.";
	}
	
	@GetMapping("/user")
	@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
	public String userAccess() {
		return "User Content.";
	}

	@GetMapping("/mod")
	@PreAuthorize("hasRole('MODERATOR')")
	public String moderatorAccess() {
		return "Moderator Board.";
	}

	@GetMapping("/admin")
	@PreAuthorize("hasRole('ADMIN')")
	public String adminAccess() {
		return "Admin Board.";
	}
}

Run & Test

Run Spring Boot application with command: mvn spring-boot:run

We also need to add some rows into roles collection before assigning any role to User.
Run following MongoDB insert statements:

db.roles.insertMany([
   { name: "ROLE_USER" },
   { name: "ROLE_MODERATOR" },
   { name: "ROLE_ADMIN" },
])

Then check the MongoDB collections:

spring-boot-mongodb-jwt-auth-spring-security-add-roles

Register some users with /signup API:

  • admin with ROLE_ADMIN
  • moderator with ROLE_MODERATOR and ROLE_USER
  • bezkoder with ROLE_USER

spring-boot-mongodb-jwt-auth-spring-security-user-registration

After make some user registration, users collection could look like this-

spring-boot-mongodb-jwt-auth-spring-security-users-collection

Access public resource: GET /api/test/all

spring-boot-mongodb-jwt-auth-spring-security-access-public

Access protected resource: GET /api/test/user

spring-boot-mongodb-jwt-auth-security-access-protected-resources

Login an account: POST /api/auth/signin

spring-boot-mongodb-jwt-auth-spring-security-user-login

Access ROLE_USER resource: GET /api/test/user

spring-boot-mongodb-jwt-auth-spring-security-user-data

Access ROLE_MODERATOR resource: GET /api/test/mod

spring-boot-mongodb-jwt-auth-security-access-protected-data

Access ROLE_ADMIN resource: GET /api/test/admin

spring-boot-mongodb-jwt-auth-security-access-protected-data-failed

Problem with newer JDK

If you run this Spring Boot App with JDK 9 or newer versions and get following error when trying to authenticate:

FilterChain java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

Just add following dependency to pom.xml:

<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>2.3.2</version>
</dependency>

Everything’s gonna work fine.

Conclusion

Oh Yeah! Today we’ve learned many things about Spring Security and JWT Token based Authentication in a Spring Boot MongoDB login & registration example (with Authorization).

This app can be used as a back-end that works well with these front-end applications (I’ve tested all of them):

Happy learning! See you again.

Further Reading

Source Code

You can find the complete source code for this tutorial on Github.

40 thoughts to “Spring Boot, MongoDB: JWT Authentication with Spring Security”

  1. hi,
    i have a doubt in these part…

    if (strRoles == null) {
      Role userRole = roleRepository.findByName(ERole.ROLE_USER)
          .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
      roles.add(userRole);
    } 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;
        case "mod":
          Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR)
              .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
          roles.add(modRole);
    
          break;
        default:
          Role userRole = roleRepository.findByName(ERole.ROLE_USER)
              .orElseThrow(() -> new RuntimeException("Error: Role is not found."));
          roles.add(userRole);
        }
      });
    }
    

    when if part executes.i checked in postman if am giving null the role user is not saved .can u pls explain these briefly

    1. Hi, I think you forgot to run following MongoDB insert statements:

      db.roles.insertMany([
         { name: "ROLE_USER" },
         { name: "ROLE_MODERATOR" },
         { name: "ROLE_ADMIN" },
      ])
      
    2. 2020-06-11 16:42:32.272 ERROR 13972 — [nio-8089-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Error: Role is not found.] with root cause

      java.lang.RuntimeException: Error: Role is not found.

      I am getting above error message, I have already inserted role.

  2. I have a doubt in reactjs that how we can register with roles.I tried by using dropdown select option but im not getting the exact output.can you give some idea for login ,logout, registration with roles using JWT

  3. Hi!
    I faced this problem even I insert ROLE into roles document so please can you help me
    Error: Role is not found.

          1. Sorry I just saw your comment you should first insert roles into your database then problem fixed

  4. Hi i am getting error {“timestamp”:”2020-04-16T06:20:27.849+0000″,”status”:500,”error”:”Internal Server Error”,”message”:”Error: Role is not found.”,”path”:”/api/auth/signup”}

      1. Hi I’m also getting this error
        {“timestamp”: “2020-10-19T07:22:47.901+00:00”,
        “status”: 500,
        “error”: “Internal Server Error”,
        “message”: “”,
        “path”: “/api/auth/signup”}
        so what is the solution for this

  5. Hello! Please may you implement “Password Reset” functionality, however I’m looking for a guide to follow. I’ve searched but I didn’t found it I’m always found jwt using another tool.
    So can you plan a serie on that topic.
    Best regards.

  6. Thanks a lot for your tutorial. May I know How to implement log-out functionality? Also, How to retrieve email Id from Authentication principal UserDetails?

    1. Hi,
      – for logout in Spring back-end:

      SecurityContextHolder.getContext().setAuthentication(null);
      /code>

      - for Email, you can use UserDetailsImpl:

      UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
      userDetails.getEmail();
      
      1. You mean something like this?

        @PostMapping("/logout")
        	@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
        	public ResponseEntity logoutUser() {
        		SecurityContext securityContext = SecurityContextHolder.getContext();
        		securityContext.setAuthentication(null);
        		return ResponseEntity.ok(new MessageResponse("logout successful"));
        	}
        
  7. I suppose that the collections roles and users would be auto created after running the project.
    But that’s not the case, can you please explain why?

  8. Thank you for the tutorial. Very detailed and appreciate the visuals.

    I’m getting an error when attempting to POST a request for api/auth/signin using Postman.
    {
    “message”: “Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter”,
    “details”: “uri=/api/v1/auth/signin”
    }

    My research points to a parse error linked to the xml conversion of the data using DatatypeConverter class. I’m using java 8 and believe the dependency module is included.

    Tried importing the DatatypeConvert class in JwtUtils class to parse the secret key but still getting the error. Hopefully you have some ideas? Thank you in advance.

    // get username from JWT
    public String getUserNameFromJwtToken(String token) {
    return Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(jwtSecret)).parseClaimsJws(token).getBody().getSubject();
    }

    // validate a JWT
    public boolean validateJwtToken(String authToken) {

    try {
    Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(jwtSecret)).parseClaimsJws(authToken);
    return true;
    }

    }

    1. Managed to solve this issue. Added the following dependencies. I’m running JDK 8.

      javax.xml.bind
      jaxb-api

      com.sun.xml.bind
      jaxb-impl
      2.3.0

      com.sun.xml.bind
      jaxb-core
      2.3.0

      javax.activation
      activation
      1.1.1

  9. Hello Guys ! When you are using the authentication URL: / api / auth / signin and a conversion error occurs you will need to include these libraries in the project, this problem occurred to me, I am forwarding it in case anyone needs help with this problem.

    jakarta.xml.bind
    jakarta.xml.bind-api

    org.glassfish.jaxb
    jaxb-runtime

  10. Can we use the email in place of username, because like you know apps are modified the way the client want, what if he don’t want a username

  11. When running under JDK 9/10/11, add these dependencies to your pom file to prevent class not found errors:

    jakarta.xml.bind
    jakarta.xml.bind-api
    2.3.2

    org.glassfish.jaxb
    jaxb-runtime
    2.3.2

  12. for a class WebSecurityConfig extends WebSecurityConfigurerAdapter
    a method with configure shows the following error as
    The method userDetailsService(T) in the type AuthenticationManagerBuilder is not applicable for the arguments (UserDetailsImpl) at line authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());

  13. I’m getting “Unauthorized User” 401 error on signup and signin requests. Can you please tell me what can be the possible issue? Thanks in advance.

Leave a Reply

Your email address will not be published. Required fields are marked *