# Spring Security

## Introduction

![](https://1374779285-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MFW3x2aqEO8GF2kr3VU%2F-MRQIXC1zqTsKnx4gp30%2F-MRQJlq6S_mgLjoaSWTH%2Fimage.png?alt=media\&token=68968556-8817-4153-9881-39dc39b09a6d)

* Spring Security just like a filter, it will pre-process between the user request and the controller
* The secured api end point will be cover by a spring security filter. In order to access the secured api end point, user is required to login with their correct role to pass the authorization which is a kind of filter

## Pre-Action

* Install the maven dependency

```markup
	<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>

		<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-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.hibernate</groupId>
			<artifactId>hibernate-validator</artifactId>
			<version>6.2.0.Final</version>
		</dependency>
	</dependencies>
```

## Configuration

```java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // Enable the PreAuthorize for specific api end point
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationEntryPoint unauthorizedHandler;
    
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Resource(name = "userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    // Declare the data source
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

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

    // Create authentication to authorize the username and password whether it is correct or not
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    // Allow CORS ,cancel session, and declare which api end point path should be authenticated
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests().antMatchers("/api/user/**").authenticated()
                .anyRequest().permitAll();

        // Apply custom filter to to do authentication
        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}
```

* To declare the data source of user
* To register the custom filter for authorization as a bean
* To register the authentication manager which act as an entry point of the login request as a bean
* To register the password encoder to do hashing for password as a bean
* To allow CORS and disable CSRF (Cross-site request forgery)
* To declare which API end point would be secured
* To declare handling method for failed authentication not access right issue

```java
@Configuration
@EnableWebMvc
public class CorConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**");
    }
}

```

* To allow all API end point can be cross-origin

## Authorization Filter

```java
public class AuthTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtils jwtUtils;

    @Resource(name = "userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    // If the secured api end point is called, the username will be obtained from jwt and check it is existed in db , successful authorization will be set manually
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        try{
            String jwt = parseJwt(httpServletRequest);
            logger.info("jwt: "+ jwt);
            if(jwt != null && jwtUtils.validateJwtToken(jwt)){
                String username = jwtUtils.getUserNameFromJwtToken(jwt);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        catch (Exception e){
            logger.error("Failed to do user authentication");
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

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

}
```

* To declare the logic of authentication which is a filter and the method of obtaining token from header of request
* Obtain the token from header, then validate the token
* If the token is null or not valid , the handler of failed authentication will be triggered to return the error response
* If the token is valid, the authorization will be set&#x20;

## Handler of failed authentication

```java
@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());
        ObjectMapper objectMapper = new ObjectMapper();
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        AuthMessageResponse authMessageResponse = new AuthMessageResponse(HttpServletResponse.SC_UNAUTHORIZED, "Failed to authorize", System.currentTimeMillis(),true);
        response.getOutputStream().println(objectMapper.writeValueAsString(authMessageResponse));
    }

}
```

* To declare the handling and the return response for failed authentication including user failed to login or access secured API end point without login

```java
@Component
public class AccessHandler implements AccessDeniedHandler {
    private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        logger.error("AccessDenied error: {}", e.getMessage());
        ObjectMapper objectMapper = new ObjectMapper();
        httpServletResponse.setContentType("application/json");
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        AuthMessageResponse authMessageResponse = new AuthMessageResponse(HttpServletResponse.SC_UNAUTHORIZED, "Access Denied!!!", System.currentTimeMillis(),true);
        httpServletResponse.getOutputStream().println(objectMapper.writeValueAsString(authMessageResponse));
    }
}
```

* To declare the handling and the return response for the issue that user have not enough access right to access secured API end point , even though he is logged in

## User Details

```java
public class UserDetailsImpl implements UserDetails {

    private String username;

    private String email;

    @JsonIgnore
    private String password;

    private Collection<? extends GrantedAuthority> authorities;

    public UserDetailsImpl(String username, String email, String password, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }

    public static UserDetailsImpl build (Account account){
        List<GrantedAuthority> authorities = account.getRoleList().stream().map(role -> new SimpleGrantedAuthority(role.getAuthority().name())).collect(Collectors.toList());
        return new UserDetailsImpl(
                account.getUsername(),
                account.getEmail(),
                account.getPassword(),
                authorities
        );
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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

    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 obj) {
        if (obj == null || getClass() != obj.getClass())
            return false;
        UserDetailsImpl user = (UserDetailsImpl) obj;
        return Objects.equals(username, user.username);
    }
}
```

* User details is the data modal that will be returned if authentication is passed
* Since the default user detail only include username , password and role
* In order to extend the modal ( in this case, also include email), we customize the modal

## User Details Service

```java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private AccountRepository accountRepository;

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

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("load User by Username: " + username);
        Account account = accountRepository.findAccountByUsername(username).orElseThrow(()-> new UsernameNotFoundException("User not found !!!"));
        return UserDetailsImpl.build(account);
    }
}
```

* User details service is to declare how to get the user details
* From here, we declare the method that get the user detail by searching table on database by using username

## Json Web Token Utils

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

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

    public String generateJwtToken(Authentication authentication){
        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date())
                .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;
    }
}
```

* To declare the method of generating token based on the user details from authentication
* To declare the method of obtaining username from token
* To do validate the token whether it is generated from our application or not

## Enum&#x20;

* Declare the type of role

```java
public enum ERole {
    ROLE_USER,
    ROLE_ADMIN
}
```

## Modal

### &#x20; ORM

* Declare the modal mapped with table of database

```java
@Entity(name = "account")
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long userId;

    @Column(name = "name")
    private String username;

    @Column(name = "email")
    private String email;

    @Column(name = "password")
    private String password;

    @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Role> roleList;

    ....
}
```

```java
@Entity(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long roleId;

    @ManyToOne
    @JoinColumn(name = "account_id")
    private Account account;

    @Enumerated(EnumType.STRING)
    private ERole authority;

    ...
}
```

### Request

```java
public class LoginRequest {
    @NotBlank(message = "Cannot be empty !!!")
    private String username;

    @NotBlank(message = "Cannot be empty !!!")
    private String password;

    public String getUsername() {
        return username;
    }

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

    public String getPassword() {
        return password;
    }

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

```java
public class SignupRequest {

    @NotBlank(message = "Cannot be empty !!!")
    private String username;

    @NotBlank(message = "Cannot be empty !!!")
    @Pattern(regexp = "^.+@([a-z]+\\.)+[a-z]{2,4}$", message = "Wrong Format")
    private String email;

    @NotBlank(message = "Cannot be empty !!!")
    private String password;

    public SignupRequest() {
    }

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

}
```

### Response

```java
public class AuthMessageResponse {
    private Integer status;
    private String message;
    private Long timeStamp;
    private boolean isError;

    public AuthMessageResponse() {
    }

    public AuthMessageResponse(Integer status, String message, Long timeStamp, boolean isError) {
        this.status = status;
        this.message = message;
        this.timeStamp = timeStamp;
        this.isError = isError;
    }
    ....
}
```

```java
public class JwtResponse {
    private String token;
    private String username;
    private String email;
    private List<String> roles;

    public JwtResponse(String token, String username, String email, List<String> roles) {
        this.token = token;
        this.username = username;
        this.email = email;
        this.roles = roles;
    }
    ...
}
```

## Exception and Exception handler

```java
public class ValidationErrorException extends RuntimeException{
    public ValidationErrorException(String message) {
        super(message);
    }
}
```

```java
@ControllerAdvice
public class CustomExceptionHandler {

    @ExceptionHandler({ValidationErrorException.class})
    public ResponseEntity<AuthMessageResponse> handleException(Exception exception){
        AuthMessageResponse authMessageResponse = new AuthMessageResponse();
        authMessageResponse.setStatus(HttpStatus.BAD_REQUEST.value());
        authMessageResponse.setMessage(exception.getMessage());
        authMessageResponse.setTimeStamp(System.currentTimeMillis());
        authMessageResponse.setError(true);
        return new ResponseEntity<>(authMessageResponse, HttpStatus.BAD_REQUEST);
    }

}
```

## Repository

```java
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {
    Boolean existsAccountByUsername(String username);
    Boolean existsAccountByEmail(String email);
    Optional<Account> findAccountByUsername(String username);
}

```

## Controller

```java
@RestController
@RequestMapping("/api/auth")
public class AuthController {

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

    @Autowired
    private AuthService authService;

    @InitBinder
    public void initBinder(WebDataBinder dataBinder) {

        StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);

        dataBinder.registerCustomEditor(String.class, stringTrimmerEditor);
    }

    @GetMapping("/test")
    public String test(){
        return "test";
    }

    @GetMapping("/preauthorize")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public String preauthorize(){
        return "preauthorize";
    }

    @PostMapping("/signup")
    public ResponseEntity<AuthMessageResponse> registerUser(@Valid @RequestBody SignupRequest signupRequest, BindingResult theBindingResult){
        logger.info("SignUp Request: "+ signupRequest.toString());
        // If the validation is failed, return error message object
        if(theBindingResult.hasErrors()){
            throw new ValidationErrorException(theBindingResult.getFieldError().getDefaultMessage());
        }
        authService.signUpUser(signupRequest);
        return ResponseEntity.ok(new AuthMessageResponse(HttpStatus.ACCEPTED.value(), "User registered successfully", System.currentTimeMillis(), false));
    }

    @PostMapping("/signupAdmin")
    public ResponseEntity<AuthMessageResponse> registerAdmin(@Valid @RequestBody SignupRequest signupRequest, BindingResult theBindingResult){
        logger.info("SignUp Request: "+ signupRequest.toString());
        if(theBindingResult.hasErrors()){
            throw new ValidationErrorException(theBindingResult.getFieldError().getDefaultMessage());
        }
        authService.signUpAdmin(signupRequest);
        return ResponseEntity.ok(new AuthMessageResponse(HttpStatus.ACCEPTED.value(), "Admin registered successfully", System.currentTimeMillis(), false));
    }


    @PostMapping("/login")
    public ResponseEntity<JwtResponse> userLogin(@Valid @RequestBody LoginRequest loginRequest, BindingResult theBindingResult){
        logger.info("Login Request: " + loginRequest.toString());
        if(theBindingResult.hasErrors()){
            throw new ValidationErrorException(theBindingResult.getFieldError().getDefaultMessage());
        }
        JwtResponse jwtResponse = authService.userLogin(loginRequest);
        return ResponseEntity.ok(jwtResponse);
    }
```

## Service

```java
@Service
public class AuthServiceImpl implements AuthService {
    private static Logger logger = LoggerFactory.getLogger(AuthServiceImpl.class);

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private PasswordEncoder encoder;

    @Autowired
    private JwtUtils jwtUtils;

    @Override
    public void signUpUser(SignupRequest signupRequest){
        // Check whether the user is existed or not
        if(accountRepository.existsAccountByUsername(signupRequest.getUsername())){
            throw new ValidationErrorException("Error: Username is already taken ");
        }
        if(accountRepository.existsAccountByEmail(signupRequest.getEmail())){
            throw new ValidationErrorException("Error: Email is already taken ");
        }
        Account account = new Account(signupRequest.getUsername(), signupRequest.getEmail(), encoder.encode(signupRequest.getPassword()));
        List<Role> roleList = new ArrayList<>();
        roleList.add(new Role(account, ERole.ROLE_USER));
        account.setRoleList(roleList);
        accountRepository.save(account);
    }

    @Override
    public void signUpAdmin(SignupRequest signupRequest){
        if(accountRepository.existsAccountByUsername(signupRequest.getUsername())){
            throw new ValidationErrorException("Error: Username is already taken ");
        }
        if(accountRepository.existsAccountByEmail(signupRequest.getEmail())){
            throw new ValidationErrorException("Error: Email is already taken ");
        }
        Account account = new Account(signupRequest.getUsername(), signupRequest.getEmail(), encoder.encode(signupRequest.getPassword()));
        List<Role> roleList = new ArrayList<>();
        roleList.add(new Role(account, ERole.ROLE_USER));
        roleList.add(new Role(account, ERole.ROLE_ADMIN));
        account.setRoleList(roleList);
        accountRepository.save(account);
    }


    @Override
    public JwtResponse userLogin(LoginRequest loginRequest){
        logger.info("user Login");
        // Do the authentication according to username and password
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
        
        // If passed, set the state to be authenticated
        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 the token to frontend
        return new JwtResponse(jwt,
                userDetails.getUsername(),
                userDetails.getEmail(),
                roles);
    }
```

## Flow

### Login

![](https://1374779285-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MFW3x2aqEO8GF2kr3VU%2F-MRQIXC1zqTsKnx4gp30%2F-MRQmKx0Q5wf_0gM8v9a%2Fimage.png?alt=media\&token=c7dfe540-d8d7-439d-a42c-ca6d2d09b5c5)

1. Post username and password&#x20;
2. Put it into UsernamePasswordAuthToken&#x20;
3. Do authentication by authentication manager which is help by user details service and password encoder&#x20;
4. If failed , go to error handler&#x20;
5. If success, return token

### Access Secured Api End Point

![](https://1374779285-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MFW3x2aqEO8GF2kr3VU%2F-MRQIXC1zqTsKnx4gp30%2F-MRQmq_DIEMbNtEo3RK-%2Fimage.png?alt=media\&token=e6878990-3862-4015-96fa-49bcac2783d2)

1. User make a request
2. Enter custom authentication filter which is a single execution for each request to api
3. Extract token from header of request
4. Validate the token
5. If failed, go to error handler
6. If success, access the API end point

## Reference

{% embed url="https://bezkoder.com/spring-boot-jwt-authentication/" %}

{% embed url="https://github.com/headshootcheng/springsecurity" %}

