×
☰ See All Chapters

Implementing Custom Authentication in Spring Security by Overriding AuthenticationProvider

In enterprise applications, you may encounter situations where the default username and password authentication method doesn't fit your needs. Sometimes, you might require various authentication methods, such as using a code from an SMS or a specific application, or even utilizing a user's fingerprint for authentication. To handle these scenarios, Spring Security allows you to implement custom authentication logic using the Authentication interface and an AuthenticationProvider.

public interface Authentication extends Principal, Serializable {
   Collection<?
extends GrantedAuthority> getAuthorities();
   
Object getCredentials();
   
Object getDetails();
   
Object getPrincipal();
   
boolean isAuthenticated();
   
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

public interface AuthenticationProvider {
   
Authentication authenticate(Authentication authentication)
           
throws AuthenticationException;
   
boolean supports(Class<?> authentication);
}

The Authentication interface represents the authentication request event and contains details about the entity requesting access to the application, known as the principal. Key methods in this interface include:

  1. isAuthenticated(): Indicates if the authentication process is complete. 

  2. getCredentials(): Provides the password or secret used in authentication. 

  3. getAuthorities(): Returns the granted authorities for the authenticated request. 

  4. getDetails(): Provides additional details about the request, if needed. 

The AuthenticationProvider interface defines how authentication is performed. It typically delegates user retrieval to a UserDetailsService and handles password management with a PasswordEncoder. You implement the authenticate() method to validate the authentication request, returning an authenticated object. The supports() method indicates if the AuthenticationProvider supports a particular authentication object.

To summarize, implementing authenticate() involves:

  1. Throwing AuthenticationException if authentication fails. 

  2. Returning null if the authentication object isn't supported. 

  3. Returning a fully authenticated Authentication instance. 

The supports() method helps Spring Security to determine which AuthenticationProvider to use based on the authentication object. This flexible approach allows for rejecting authentication requests based on various criteria.

Imagine a secure vault with multiple locks, each requiring a different type of key to open. Each key represents a different authentication method: one for fingerprints, another for access cards, and a third for numerical codes. The vault manager (authentication manager) determines which lock (authentication provider) to engage based on the type of key presented. If someone tries to use a fingerprint key at the access card lock, it won't work. However, if the access card lock is selected, it will validate the access card accordingly. This setup ensures robust security while accommodating diverse access methods.

Besides testing the authentication type, Spring Security adds one more layer for flexibility. The access card lock can recognize multiple kinds of cards. In this case, when you present a card, one of the authentication providers could say, “I understand this as being a card. But it isn’t the type of card I can validate!” This happens when supports() returns true but authenticate() returns null.

Example

In the below example we implement username and password authentication method which is same as default authentication approach.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
     
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <
modelVersion>4.0.0</modelVersion>
  <
parent>
     <
groupId>org.springframework.boot</groupId>
     <
artifactId>spring-boot-starter-parent</artifactId>
     <
version>2.6.0</version>
     <
relativePath/> <!-- lookup parent from repository, not local -->
 
</parent>
  <
groupId>com.java4coding</groupId>
  <
artifactId>ImplementingCustomAuthentication </artifactId>
  <
version>0.0.1-SNAPSHOT</version>
  <
name>ImplementingCustomAuthentication </name>
  <
description>Implementing Custom Authentication in Spring Security by Overriding AuthenticationProvider</description>
  <
properties>
     <
java.version>11</java.version>
     <
project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <
project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <
spring-boot.version>2.6.0</spring-boot.version>
     <
lombok.version>1.18.28</lombok.version>
  </
properties>
  <
dependencies>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter</artifactId>
     </
dependency>
     <
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.projectlombok</groupId>
        <
artifactId>lombok</artifactId>
        <
version>${lombok.version}</version>
        <
scope>provided</scope>
     </
dependency>
     <
dependency>
        <
groupId>com.h2database</groupId>
        <
artifactId>h2</artifactId>
        <
scope>runtime</scope>
     </
dependency>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter-data-jpa</artifactId>
     </
dependency>
  </
dependencies>
  <
dependencyManagement>
     <
dependencies>
        <
dependency>
           <
groupId>org.springframework.boot</groupId>
           <
artifactId>spring-boot-dependencies</artifactId>
           <
version>${spring-boot.version}</version>
           <
type>pom</type>
           <
scope>import</scope>
        </
dependency>
     </
dependencies>
  </
dependencyManagement>

  <
build>
     <
plugins>
        <
plugin>
           <
groupId>org.springframework.boot</groupId>
           <
artifactId>spring-boot-maven-plugin</artifactId>
           <
version>${spring-boot.version}</version>
           <
executions>
              <
execution>
                 <
id>build-info</id>
                 <
goals>
                    <
goal>build-info</goal>
                    <
goal>repackage</goal>
                 </
goals>
              </
execution>
           </
executions>
        </
plugin>
     </
plugins>
  </
build>

</
project>

 

 

SpringBootDemo.java

package com.java4coding;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan
@EnableJpaRepositories
public class SpringBootDemo {
   
public static void main(String[] args) {
       
SpringApplication.run(SpringBootDemo.class, args);
   }
}

 

 

ApplicationConfig.java

package com.java4coding.config;

import com.java4coding.repository.UserRepository;
import com.java4coding.security.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class ApplicationConfig {

   
@Bean
   
public UserDetailsService userDetailsService(UserRepository userRepository) {
       
return new UserDetailsServiceImpl(userRepository);
   }

   
@Bean
   
public PasswordEncoder passwordEncoder() {
       
return NoOpPasswordEncoder.getInstance();
   }
}

 

 

ApplicationWebSecurityConfigurerAdapter.java

package com.java4coding.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
@Autowired
   
private AuthenticationProvider authenticationProvider;

   
/*
   Since H2 has its own authentication provider, you can skip the Spring Security for the
   path of h2 console entirely in the same way that you do for your static content.
   In order to do that, in your Spring security config, you have to override the configuration
    method which takes an instance of org.springframework.security.config.annotation.web.builders.WebSecurity
    as a parameter instead of the one which takes an instance of org.springframework.security.config.annotation.web.builders.HttpSecurity
    */
   
@Override
   
public void configure(WebSecurity web) throws Exception {
       web.ignoring().antMatchers(
"/h2-console/**");
   }

   
@Override
   
protected void configure(AuthenticationManagerBuilder auth) {
       auth.authenticationProvider(
authenticationProvider);
   }


}

 

 

DemoController.java

package com.java4coding.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

   
@GetMapping(value = "/demo")
   
public String sayHello() {
       
return "Hurray! You are Authorized.";
   }
}

 

 

User.java

package com.java4coding.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Setter
@Getter
public class User {
   
@Id
   @GeneratedValue
(strategy = GenerationType.IDENTITY)
   
private Long id;
   
private String username;
   
private String password;
   
private String authority;
}

 

 

UserRepository.java

package com.java4coding.repository;

import com.java4coding.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {
   
List<User> findByUsername(String userName);
}

 

 

CustomAuthenticationProvider.java

package com.java4coding.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
   
@Autowired
   
private UserDetailsService userDetailsService;
   
@Autowired
   
private PasswordEncoder passwordEncoder;

   
@Override
   
public Authentication authenticate(Authentication authentication) {
       
String username = authentication.getName();
       
String password = authentication.getCredentials().toString();
       
UserDetails user = userDetailsService.loadUserByUsername(username);
       
if (passwordEncoder.matches(password, user.getPassword())) {
           
return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
       }
else {
           
throw new BadCredentialsException("Something went wrong!");
       }
   }

   
@Override
   
public boolean supports(Class<?> authenticationType) {
       
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authenticationType);
   }
}

 

 

SecurityUser.java

package com.java4coding.security;

import com.java4coding.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class SecurityUser implements UserDetails {
   
private final User user;

   
public SecurityUser(User user) {
       
this.user = user;
   }

   
@Override
   
public String getUsername() {
       
return user.getUsername();
   }

   
@Override
   
public String getPassword() {
       
return user.getPassword();
   }

   
@Override
   
public Collection<? extends GrantedAuthority> getAuthorities() {
       
return List.of(() -> user.getAuthority());
   }

   
@Override
   
public boolean isAccountNonExpired() {
       
return true;
   }

   
@Override
   
public boolean isAccountNonLocked() {
       
return true;
   }

   
@Override
   
public boolean isCredentialsNonExpired() {
       
return true;
   }

   
@Override
   
public boolean isEnabled() {
       
return true;
   }
}

 

 

UserDetailsServiceImpl.java

package com.java4coding.security;

import com.java4coding.entity.User;
import com.java4coding.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;

@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
   
private UserRepository userRepository;

   
@Override
   
public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException {
       
List<User> usersByUserName = userRepository.findByUsername(username);
       
return new SecurityUser(usersByUserName.get(0));
   }
}

 

 

application.properties

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console/

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=pass
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
#spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.hibernate.ddl-auto=none

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.format_sql=true

 

 

schema.sql

create table user (
  id bigint generated
by default as identity,
   authority
varchar(255),
   password
varchar(255),
   username
varchar(255),
   
primary key (id)
);

 

 

data.sql

INSERT INTO User (id, username, password, authority) VALUES (1, 'manu', 'pass', 'read');
INSERT INTO User (id, username, password, authority) VALUES (2, 'advith', 'xyz123', 'read');
INSERT INTO User (id, username, password, authority) VALUES (3, 'aashvith', 'xyz123', 'read');

 

Project Structure

overriding-authenticationprovider-0
 

Output

overriding-authenticationprovider-1
 

All Chapters
Author