Creating Authentication Using Phone Number and OTP with Spring Boot, Spring Security
Implementing Secure Phone Number and OTP-Based Authentication in Spring Boot Applications Using Spring Security and Third-Party Providers
Introduction:
In this article, we will walk you through the process of creating authentication using phone numbers and One-Time Passwords (OTPs) in a Spring Boot application. We will be using Spring Security to secure our application and a placeholder third-party provider for sending OTPs. This tutorial assumes that you have a basic understanding of Spring Boot and Spring Security. We'll also include code examples and Mermaid diagrams for better understanding.
Step 1: Setting up the Spring Boot project
Go to the Spring Initializr website (https://start.spring.io/) to generate a new Spring Boot project with the required dependencies.
Select the following dependencies: "Web", "Security", and "JPA".
Click "Generate" to download the generated project and unzip it.
Step 2: Configure Spring Security
Add the following dependencies to your
pom.xml
file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Create a new class called
SecurityConfig
in thecom.example.demo.config
package, and make it extendWebSecurityConfigurerAdapter
:
package com.example.demo.config;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
Step 3: Implementing phone number authentication
Create a new package called
com.example.demo.security
.In the
com.example.demo.security
package, create a new class calledPhoneNumberAuthenticationProvider
and make it extendAuthenticationProvider
:
package com.example.demo.security;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public class PhoneNumberAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// Implement phone number authentication logic here
}
@Override
public boolean supports(Class<?> authentication) {
return PhoneNumberAuthenticationToken.class.isAssignableFrom(authentication);
}
}
In the
PhoneNumberAuthenticationProvider
class, implement theauthenticate
method. This method should verify the user's phone number and return anAuthentication
object if the user is authenticated.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phoneNumber = authentication.getName();
// Verify the phone number and return an Authentication object if the user is authenticated.
// Use your placeholder third-party provider for phone number verification.
}
Step 4: Create a custom authentication filter
In the
com.example.demo.security
package, create a new class calledPhoneNumberAuthenticationFilter
and make it extendAbstractAuthenticationProcessingFilter
:
package com.example.demo.security;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
public class PhoneNumberAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public PhoneNumberAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/phone", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// Implement phone number authentication logic here
}
}
Implement the
attemptAuthentication
method in thePhoneNumberAuthenticationFilter
class. This method should extract the user's phone number from the request, create anAuthentication
object, and pass it to theAuthenticationManager
.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws Authentication throws AuthenticationException, IOException, ServletException {
String phoneNumber = request.getParameter("phoneNumber");
PhoneNumberAuthenticationToken authRequest = new PhoneNumberAuthenticationToken(phoneNumber);
return getAuthenticationManager().authenticate(authRequest);
}
Step 5: Configuring the authentication filter and provider in the SecurityConfig
class
In the
SecurityConfig
class, add the following methods to register thePhoneNumberAuthenticationFilter
andPhoneNumberAuthenticationProvider
:
package com.example.demo.config;
import com.example.demo.security.PhoneNumberAuthenticationFilter;
import com.example.demo.security.PhoneNumberAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PhoneNumberAuthenticationProvider phoneNumberAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(phoneNumberAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(phoneNumberAuthenticationProvider)
.authorizeRequests()
.antMatchers("/login/phone").permitAll()
.anyRequest().authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(phoneNumberAuthenticationProvider);
}
@Bean
public PhoneNumberAuthenticationFilter phoneNumberAuthenticationFilter() throws Exception {
PhoneNumberAuthenticationFilter filter = new PhoneNumberAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Step 6: Implementing the OTP generation and validation
In the
com.example.demo.service
package, create a new interface calledOTPService
with the following methods:
public interface OTPService {
String generateOTP(String phoneNumber);
boolean validateOTP(String phoneNumber, String otp);
}
Create a new class called
OTPServiceImpl
in thecom.example.demo.service.impl
package and implement theOTPService
interface:
@Service
public class OTPServiceImpl implements OTPService {
// Implement your third-party OTP provider here
@Override
public String generateOTP(String phoneNumber) {
// Call the third-party OTP provider to generate an OTP and return it
}
@Override
public boolean validateOTP(String phoneNumber, String otp) {
// Call the third-party OTP provider to validate the OTP and return true if valid, false otherwise
}
}
Step 7: Implementing the OTP generation and validation in the PhoneNumberAuthenticationProvider
class
Inject the
OTPService
into thePhoneNumberAuthenticationProvider
class:
@Autowired
private OTPService otpService;
Update the
authenticate
method in thePhoneNumberAuthenticationProvider
class to generate an OTP and send it to the user via the third-party provider:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phoneNumber = authentication.getName();
String otp = otpService.generateOTP(phoneNumber);
// Send the generated OTP to the user's phone number using the third-party provider
// Store the generated OTP in the user's session or another storage mechanism for later validation
return null; // Return null since the user is not yet authenticated
}
Add a new method called
validateOTP
in thePhoneNumberAuthenticationProvider
class to validate the OTP entered by the user:
public Authentication validateOTP(String phoneNumber, String otp) throws AuthenticationException {
// Validate the OTP entered by the user using the OTPService
boolean isValid = otpService.validateOTP(phoneNumber, otp);
if (isValid) {
// If the OTP is valid, create an authenticated Authentication object with the user's phone number
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
PhoneNumberAuthenticationToken authentication = new PhoneNumberAuthenticationToken(phoneNumber, authorities);
return authentication;
} else {
// If the OTP is not valid, throw an AuthenticationException
throw new BadCredentialsException("Invalid OTP");
}
}
Step 8: Implement the OTP validation in the PhoneNumberAuthenticationFilter
class
Update the
attemptAuthentication
method in thePhoneNumberAuthenticationFilter
class to validate the OTP entered by the user:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String phoneNumber = request.getParameter("phoneNumber");
String otp = request.getParameter("otp");
PhoneNumberAuthenticationToken authRequest = new PhoneNumberAuthenticationToken(phoneNumber);
Authentication authentication = getAuthenticationManager().authenticate(authRequest);
if (authentication == null) {
// If the user is not yet authenticated, call the validateOTP method in the PhoneNumberAuthenticationProvider class
PhoneNumberAuthenticationProvider provider = (PhoneNumberAuthenticationProvider) getAuthenticationManager()
.getProviders().stream().filter(p -> p instanceof PhoneNumberAuthenticationProvider).findFirst()
.orElseThrow(() -> new AuthenticationServiceException("PhoneNumberAuthenticationProvider not found"));
authentication = provider.validateOTP(phoneNumber, otp);
}
return authentication;
}
Step 9: Testing the authentication flow
Create a simple controller for testing purposes:
@RestController
public class TestController {
@GetMapping("/")
public ResponseEntity<String> home() {
return ResponseEntity.ok("Welcome to the home page!");
}
@GetMapping("/login/phone")
public ResponseEntity<String> login() {
return ResponseEntity.ok("Please provide your phone number and OTP to authenticate.");
}
}
Run the application and test the authentication flow using a tool like Postman or Curl. Send a POST request to
/login/phone
with the phone number and OTP as parameters. If the authentication is successful, you should receive a response with the authenticated user's phone number and authorities.
Conclusion:
In this article, we have walked you through the process of creating authentication using phone numbers and OTPs in a Spring Boot application using Spring Security and a placeholder third-party provider for sending OTPs. By following these steps, you can implement a secure and reliable authentication mechanism for your application.
Feel free to modify and extend the code as needed to fit your specific requirements. Happy coding!
Can you please share the github repo for this implementation
could you please show how the PhoneNumberAuthenticationToken looks like?