Friday, May 27, 2022

Spring Boot + Spring Security JWT Authentication Example

In this tutorial we’ll see how to create a Spring Boot application that uses Spring Security and JWT token based authentication to bring authentication and authorization to the exposed REST APIs. DB used is MySQL.

What does JWT do

JWT (JSON Web Token) is used for securing REST APIs.

In the JWT authentication process a client application first need to authenticate using credentials. The server side verifies the sent credentials, if valid then it generates and returns a JWT.

Once the client has been authenticated it has to sent the token in the request’s Authorization header in the Bearer Token form with each request. The server will check the validity of the token to verify the validity of the client and authorize or reject requests. You can also store roles and method usage will be authorized based on the role.

You can also configure the URLs that should be authenticated and those that will be permitted without authentication.

Spring Boot + Spring Security with JWT authentication example

In the application we’ll have the user signup and user signin logic. Once the signup is done user should be authenticated when logging in, that configuration would be done using Spring security and JWT.

Maven Dependencies

These are the dependencies needed in the pom.xml file which include Spring security, Spring Data JPA, JWT and MySQL.

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.netjs</groupId>
  <artifactId>springbootsecurity</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>SpringBootSecurity</name>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
  </parent>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</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-data-jpa</artifactId>
    </dependency>
    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

Project Structure

Once ready the project structure for the Spring Boot authentication application looks like as given below-

Spring Boot application

DB Tables

Since we are doing both authentication and authorization so there are two master tables for storing User and Role records. There is also a table user_role to capture roles assigned to particular users.

CREATE TABLE `netjs`.`users` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(30) NOT NULL,
  `email` VARCHAR(45) NOT NULL,
  `password` VARCHAR(150) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `name_UNIQUE` (`name` ASC),
  UNIQUE INDEX `email_UNIQUE` (`email` ASC));


CREATE TABLE `netjs`.`role` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));


CREATE TABLE `netjs`.`user_role` (
  `user_id` INT NOT NULL,
  `role_id` INT NOT NULL);

Insert the required roles in the Role table.

insert into role (name) values ("ROLE_ADMIN");
insert into role (name) values ("ROLE_USER");

Model classes

Entity classes that map to the DB tables are as follows.

User.java

import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

@Entity
@Table(name="users")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id; 
  @Column(name="name")
  private String userName; 
  @Column(name="email")
  private String email;
  @Column(name="password")
  private String password;
  @ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(name = "user_role", 
      joinColumns = @JoinColumn(name="USER_ID", referencedColumnName="ID"),
      inverseJoinColumns = @JoinColumn(name="ROLE_ID", referencedColumnName="ID"))
  private Set<Role> roles = new HashSet<>();
  public Integer getId() {
    return id;
  }
  public void setId(Integer 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;
  }
}

User has Many-to-Many relationship with Role, that association is captured using the join table user_role.

Role.java

import javax.persistence.Column;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;

@Entity
@Table(name="role")
public class Role {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id; 
	@Enumerated(EnumType.STRING)
	@Column(name="name")
	private Roles roleName;
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public Roles getRoleName() {
		return roleName;
	}
	public void setRoleName(Roles roleName) {
		this.roleName = roleName;
	}
}

Roles.java (Enum)

public enum Roles {
	ROLE_USER,
	ROLE_ADMIN
}

Ensure that you follow the same nomenclature ROLE_XXX as Spring security uses ROLE_ prefix by default when using hasRole() method.

Apart from these Entity classes there are Model classes related to the request that is sent and the response received.

SignupRequest.java

This class captures the data for the SignUp request which is sent when a new user registers.

public class SignupRequest {
	private String userName; 
	private String email;
	private String password;
	private String[] roles;
	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 String[] getRoles() {
		return roles;
	}
	public void setRoles(String[] roles) {
		this.roles = roles;
	}
}

AuthResponse.java

This class represents the response you get back when logging in. It includes the authentication token and the roles user is authorized for.

public class AuthResponse {
  private String token;
  private List<String> roles;

  public String getToken() {
    return token;
  }

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

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

  public void setRoles(List<String> roles) {
    this.roles = roles;
  }  
}

CustomUserBean.java

Spring Security has an interface org.springframework.security.core.userdetails.UserDetails which provides core user information. You need to provide a concrete implemetation of this interface to add more fields. There is also a concrete implementation org.springframework.security.core.userdetails.User provided by Spring security that can be used directly. Here we are using our own implementation ConcreteUserBean.

Note that in the class there is also a getAuthorities() method that returns authorities of type GrantedAuthority. That list of GrantedAuthority objects is built using the instances of SimpleGrantedAuthority which is the basic concrete implementation of a GrantedAuthority. Stores a String representation of an authority granted to the Authentication object.

import java.util.Collection;
import java.util.List;
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.fasterxml.jackson.annotation.JsonIgnore;

public class CustomUserBean implements UserDetails {
  private static final long serialVersionUID = -4709084843450077569L;  
  private Integer id; 
  private String userName; 
  private String email;
  @JsonIgnore
  private String password;
  private Collection<? extends GrantedAuthority> authorities;
  CustomUserBean(Integer 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 CustomUserBean createInstance(User user) {
    List<GrantedAuthority> authorities = user.getRoles()
                     .stream()
                         .map(role -> new SimpleGrantedAuthority(role.getRoleName().name()))
                      .collect(Collectors.toList());
    return new CustomUserBean(user.getId(), user.getUserName(), 
        user.getEmail(), user.getPassword(), authorities);
  }
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return authorities;
  }

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

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

  public Integer getId() {
    return id;
  }

  public String getEmail() {
    return email;
  }

  @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 rhs) {
    if (rhs instanceof CustomUserBean) {
      return userName.equals(((CustomUserBean) rhs).userName);
    }
    return false;
  }

  @Override
  public int hashCode() {
    return userName.hashCode();
  }
}

Repositories

Since we are using Spring Data JPA so we just need to create interfaces extending the JpaRepository.

UserRepository

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.netjstech.model.User;

@Repository
public interface UserRepository extends JpaRepository<User, Integer>{
  public Optional<User> findByUserName(String userName);
  public boolean existsByEmail(String email);
  public boolean existsByUserName(String userName);
}

RoleRepository

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.netjstech.model.Role;
import com.netjstech.model.Roles;

@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {
  Optional<Role> findByRoleName(Roles role);
}

Service Class

Within Spring security there is an interface org.springframework.security.core.userdetails.UserDetailsService that has a method loadUserByUsername(java.lang.String username) to load user-specific data.

We’ll provide a concrete implementation of this interface where the loadUserByUsername() method implementation acts as a wrapper over the userRepository.findByUserName() method call. Returned user is passed to the CustomUserBean.createInstance() method to create instance of CustomUserBean as per our implementation.

import javax.transaction.Transactional;
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 com.netjstech.dao.UserRepository;
import com.netjstech.model.CustomUserBean;
import com.netjstech.model.User;

@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 with "
                        + "user name "+ username + " not found"));
    return CustomUserBean.createInstance(user);
  }
}

Configuring Spring Security and JWT

The following security configuration ensures that only authenticated users can access the APIs.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.netjstech.service.UserDetailsServiceImpl;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	@Autowired
	UserDetailsServiceImpl userDetailsService;
	@Autowired
	JwtAuthenticationEntryPoint authenticationEntryPoint;
	@Override
	public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception{
		authenticationManagerBuilder.userDetailsService(userDetailsService)
									.passwordEncoder(passwordEncoder());
	}
	
	@Bean 
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}
	@Bean
	public JwtTokenFilter jwtTokenFilter(){
		return new JwtTokenFilter();
	}
	@Override
	protected void configure(HttpSecurity httpSecurity) throws Exception {
		// We don't need CSRF for this example
		httpSecurity.cors().and().csrf().disable()
					.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
					.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
					.authorizeRequests().antMatchers("/auth/**").permitAll()
					.antMatchers(HttpMethod.GET, "/user/allusers").permitAll()
					.anyRequest().authenticated();
		
		httpSecurity.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);					
	}
}

Important points to note here-

The SecurityConfig class is annotated with @EnableWebSecurity to enable Spring Security’s web security support and provide the Spring MVC integration.

Spring Security must be configured in a bean that implements WebSecurityConfigurer or extends WebSecurityConfigurerAdapter. Since extending WebSecurityConfigurerAdapter is more convenient so that is done here and couple of its methods are overridden. Important one are the configure methods-

  • configure(AuthenticationManagerBuilder)- Override to configure user-details services.
  • configure(HttpSecurity)- Override to configure web based security for specific http requests.
  • configure(WebSecurity)- Override to configure the FilterChainProxy known as the Spring Security Filter Chain.

@EnableGlobalMethodSecurity- Used to enable method level security based on annotations.

Spring Security supports three different kinds of security annotations:

  • @Secured provided by Spring security itself
  • JSR-250’s @RolesAllowed annotation
  • Expression based annotations with @PreAuthorize, @PostAuthorize, @PreFilter and @PostFilter

We’ll be using @PreAuthorize annotation for securing methods as you will see in the Controller class.

If you see the configure(HttpSecurity httpSecurity) method any request whose path is /auth/*** and GET request with path /user/allusers should not be authenticated. Any other request should be authenticated. This method also configures which exception handler is used (AuthenticationEntryPoint), how to manage session (stateless in this case) and when to register filter (JwtTokenFilter)

configure(AuthenticationManagerBuilder authenticationManagerBuilder) method is used to add authentication based upon the custom UserDetailsService that is passed in. UserDetailsServiceImpl in our case. It then returns a DaoAuthenticationConfigurer to allow customization of the authentication.

JwtAuthenticationEntryPoint.java

Class that implements AuthenticationEntryPoint. It is used for exception handling and sends the 401 status code as response indicating that the request requires HTTPauthentication.

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
	private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		logger.error("Unauthorized access error : " + authException.getMessage());
		response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized Access");
	}
}

JwtTokenUtil.java

A utility class that does following three tasks-

  1. Generate the token by setting user name, issued time, expiration time.
  2. Validate token
  3. Get user name from taken.
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import com.netjstech.model.CustomUserBean;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;

@Component
public class JwtTokenUtil {
	@Value("${jwttoken.secret}")
	private String jwtTokenSecret;
	@Value("${jwttoken.expiration}")
	private long jwtTokenExpiration;
	
	public String generateJwtToken(Authentication authentication) {
		CustomUserBean userPrincipal = (CustomUserBean)authentication.getPrincipal();
		return Jwts.builder()
				   .setSubject(userPrincipal.getUsername())
				   .setIssuedAt(new Date(System.currentTimeMillis()))
				   .setExpiration(new Date(System.currentTimeMillis() + jwtTokenExpiration))
				   .signWith(SignatureAlgorithm.HS512, jwtTokenSecret)
				   .compact();
	}
	
	public boolean validateJwtToken(String token) {
		try {
			Jwts.parser()
				.setSigningKey(jwtTokenSecret)
				.parseClaimsJws(token);
			return true;
		}catch(UnsupportedJwtException exp) {
			System.out.println("claimsJws argument does not represent Claims JWS" + exp.getMessage());
		}catch(MalformedJwtException exp) {
			System.out.println("claimsJws string is not a valid JWS" + exp.getMessage());
		}catch(SignatureException exp) {
			System.out.println("claimsJws JWS signature validation failed" + exp.getMessage());
		}catch(ExpiredJwtException exp) {
			System.out.println("Claims has an expiration time before the method is invoked" + exp.getMessage());
		}catch(IllegalArgumentException exp) {
			System.out.println("claimsJws string is null or empty or only whitespace" + exp.getMessage());
		}
		return false;
	}
	
	public String getUserNameFromJwtToken(String token) {
		 Claims claims =Jwts.parser()
				   .setSigningKey(jwtTokenSecret)
				   .parseClaimsJws(token)
				   .getBody();
		 return claims.getSubject();
		
	}
}

JwtTokenFilter.java

A filter class that extends OncePerRequestFilter that guarantees a single execution per requestdispatch.

In the overridden doFilterInternal() method token is extracted from the request header and validated. If token is validated then get user name from it and use it to get UserDetails. From these user details and its authorities create an Authentication object.

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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.netjstech.service.UserDetailsServiceImpl;

public class JwtTokenFilter extends OncePerRequestFilter{
	@Autowired
	private JwtTokenUtil jwtTokenUtil;
	@Autowired
	private UserDetailsServiceImpl userDetailsService;
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		try {
			String token = getTokenFromRequest(request);
			//System.out.println("Token-- " + token);
			if (token != null && jwtTokenUtil.validateJwtToken(token)) {
				String username = jwtTokenUtil.getUserNameFromJwtToken(token);
				//System.out.println("User Name--JwtTokenFilter-- " + username);
				UserDetails userDetails = userDetailsService.loadUserByUsername(username);
				//System.out.println("Authorities--JwtTokenFilter-- " + userDetails.getAuthorities());
				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);
			throw new RuntimeException("Cannot set user authentication" + e.getMessage());
		}
		filterChain.doFilter(request, response);
	}
	
	private String getTokenFromRequest(HttpServletRequest request) {
		String token = request.getHeader("Authorization");
		
		if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
			// remove "Bearer "
			return token.substring(7, token.length());
		}
		return null;
	}
}

src/main/resources/application.properties

Properties to configure Spring Datasource, JPA and also JWT.

spring.datasource.url=jdbc:mysql://localhost:3306/netjs
spring.datasource.username=root
spring.datasource.password=admin

spring.jpa.properties.hibernate.sqldialect=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.properties.hibernate.showsql=true

jwttoken.secret=mysecretkey
# in milliseconds (5 mins)
jwttoken.expiration=300000

Controller classes

AuthController.java

This controller class has two methods-

  • userSignup() which is a handler method for any POST request to /auth/signup path.
  • userLogin() which is a handler method for any POST request to /auth/login path.
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.netjstech.dao.RoleRepository;
import com.netjstech.dao.UserRepository;
import com.netjstech.model.AuthResponse;
import com.netjstech.model.CustomUserBean;
import com.netjstech.model.Role;
import com.netjstech.model.Roles;
import com.netjstech.model.SignupRequest;
import com.netjstech.model.User;
import com.netjstech.security.JwtTokenUtil;

@RestController
@RequestMapping("/auth")
public class AuthController {
  @Autowired
  UserRepository userRepository;
  @Autowired
  RoleRepository roleRepository;
  @Autowired
  PasswordEncoder encoder;
  @Autowired
  AuthenticationManager authenticationManager;
  @Autowired
  JwtTokenUtil jwtTokenUtil;
  
  @PostMapping("/login")
  public ResponseEntity<?> userLogin(@Valid @RequestBody User user) {
    //System.out.println("AuthController -- userLogin");
    Authentication authentication = authenticationManager.authenticate(
          new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()));
    
    SecurityContextHolder.getContext().setAuthentication(authentication);
    String token = jwtTokenUtil.generateJwtToken(authentication);
    CustomUserBean userBean = (CustomUserBean) authentication.getPrincipal();    
    List<String> roles = userBean.getAuthorities().stream()
                   .map(auth -> auth.getAuthority())
                   .collect(Collectors.toList());
    AuthResponse authResponse = new AuthResponse();
    authResponse.setToken(token);
    authResponse.setRoles(roles);
    return ResponseEntity.ok(authResponse);
  }
  
  @PostMapping("/signup")
  public ResponseEntity<?> userSignup(@Valid @RequestBody SignupRequest signupRequest) {
    if(userRepository.existsByUserName(signupRequest.getUserName())){
      return ResponseEntity.badRequest().body("Username is already taken");
    }
    if(userRepository.existsByEmail(signupRequest.getEmail())){
      return ResponseEntity.badRequest().body("Email is already taken");
    }
    User user = new User();
    Set<Role> roles = new HashSet<>();
    user.setUserName(signupRequest.getUserName());
    user.setEmail(signupRequest.getEmail());
    user.setPassword(encoder.encode(signupRequest.getPassword()));
    //System.out.println("Encoded password--- " + user.getPassword());
    String[] roleArr = signupRequest.getRoles();
    
    if(roleArr == null) {
      roles.add(roleRepository.findByRoleName(Roles.ROLE_USER).get());
    }
    for(String role: roleArr) {
      switch(role) {
        case "admin":
          roles.add(roleRepository.findByRoleName(Roles.ROLE_ADMIN).get());
          break;
        case "user":
          roles.add(roleRepository.findByRoleName(Roles.ROLE_USER).get());
          break;  
        default:
          return ResponseEntity.badRequest().body("Specified role not found");
      }
    }
    user.setRoles(roles);
    userRepository.save(user);
    return ResponseEntity.ok("User signed up successfully");
  }
}

In the userSignup() method first thing is to verify that the passed user name or email are no already in use. Then create a User object using the values passed in SignupRequest object. Extract the roles and set those also in the User object and then save the User. If everything works fine then set the status code as ok in the response with the message.

In the userLogin() method authenticate the user and set the authentication object in SecurityContext. Then generate the token and get the list of user roles. Set both token and list of roles in the response that is sent back.

UserController.java

This controller just demonstrates the use of authorization by access controlling the methods using @PreAuthorize annotation.

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

@RestController
@RequestMapping("/user")
public class UserController {
	@GetMapping("/allusers")
	public String displayUsers() {
		return "Display All Users";
	}
	
	@GetMapping("/displayuser")
	@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
	public String displayToUser() {
		return "Display to both user and admin";
	}
	
	@GetMapping("/displayadmin")
	@PreAuthorize("hasRole('ROLE_ADMIN')")
	public String displayToAdmin() {
		return "Display only to admin";
	}
}

As evident from the annotations-

  • displayUsers() method can be accessed by all.
  • displayToUser() method can be accessed by user having role user or admin.
  • displayToAdmin() method can be accessed by user having role admin.

Application class

Class with main method to run the application.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootAuthApp {

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

Testing the application

You can run the above class with main method as a Java application that would set up the Spring Boot application and start the web server to listen on a given port.

Registering user by sending POST request to signup.

Spring Boot token authentication example

In the DB you can check the USER table to verify that user data is inserted.

Also the mapped roles.

With the registered user you can login. As a response you’ll get the generated token you need to send that token with other requests.

Spring Boot login example

Display all users, this method displayUsers() is permitted to all as configured in SecurityConfig class. With this URL- http://localhost:8080/user/allusers user should be able to access it even when not logged in.

displayToUser() method need authentication and also authorization that the logged user should have role USER or ADMIN. You need to send the token which is returned after login so go to Authorization tab select “Bearer Token” in the Type dropdown and add the token.

Spring Boot authorization

displayToAdmin() method need authentication and also authorization that the logged user should have role ADMIN. Trying to access this method with the user having only USER role authorization results in “Forbidden” error.

Spring Boot roles

Source code from GitHub- https://github.com/netjs/spring-boot-security-jwt

That's all for this topic Spring Boot + Spring Security JWT Authentication Example. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Spring Tutorial Page


Related Topics

  1. Angular + Spring Boot JWT Authentication Example
  2. Spring Boot REST API CRUD Example With Spring Data JPA
  3. Angular HttpClient + Spring Boot REST API CRUD Backend Service
  4. Spring Boot StandAlone (Console Based) Application Example
  5. Spring NamedParameterJdbcTemplate Insert, Update And Delete Example

You may also like-

  1. Spring MVC Pagination Example Using PagedListHolder
  2. Spring MVC Exception Handling - @ExceptionHandler And @ControllerAdvice Example
  3. Spring Transaction Management Example - @Transactional Annotation and JDBC
  4. Connection Pooling With Apache DBCP Spring Example
  5. Angular HttpClient to Communicate With Backend Service
  6. Exception Handling in Java Lambda Expressions
  7. Core Java Basics Interview Questions And Answers
  8. Variable Length Arguments (*args), Keyword Varargs (**kwargs) in Python

No comments:

Post a Comment