Spring Security: Custom Authencation with OTP based login
We will walk through you on how to setup Spring Security with Custom Authentication handler for OTP based login. We are going to use JWT for authentication which will be pass in request header as authorization bearer token.
Prerequisites: You must be familiar with Java & Spring Framework to understand it.
First you need to set up the project for which you can use https://start.spring.io/, for this demo I have used 3.5.6 version along with Java 21.
You need to add these basic dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
</dependency>
Configure your database, you can use the steps mentioned here to setup your database connection and create JPA Repository, Service to access it with basic CRUD operations.
Now first step is to create a service to handle JWT, this will be used to create, encrypt, decrypt, read and authenticate it.
@Service
public class JwtService {
public static final SecretKey key = Jwts.SIG.HS512.key().build();
private static final String APP_ID = "appId";
@Value("${in.stacknowledge.appId}") //defined in application.properties
private String appId;
@Value("${in.stacknowledge.app}") //defined in application.properties
private String app;
public String generateToken(User user) { // Use email as username
Map<String, Object> claims = new HashMap<>();
claims.put(APP_ID, appId);
claims.put("role", user.getRole().name());
claims.put("mobile", user.getMobile());
claims.put("name", user.getName());
claims.put("email", user.getEmail());
return createToken(claims, user.getEmail());
}
private String createToken(Map<String, Object> claims, String email) {
return Jwts.builder()
.issuer(app)
.subject(email)
.claims(claims)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
.signWith(key)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return (Claims) Jwts.parser()
.requireIssuer(app)
.require(APP_ID, appId)
.verifyWith(key)
.build()
.parse(token)
.getPayload();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Boolean validateToken(String token, User user) {
final String email = extractAllClaims(token).get("email", String.class);
final String role = extractAllClaims(token).get("role", String.class);
return (!user.isDeleted() && email.equals(user.getEmail()) && (role.equals(user.getRole().name())) && !isTokenExpired(token));
}
}
Implement one custom authentication provider by implementing – org.springframework.security.authentication.AuthenticationProvider
public class PasscodeAuthenticationProvider implements AuthenticationProvider {
@Setter
private UserService userService;
@Setter
private OtpService otpService; //Service for creating, validating and sending OTP
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userIdentifier = authentication.getPrincipal().toString();
String passcode = authentication.getCredentials().toString();
User user = userService.getUserByEmailOrMobile(userIdentifier);
if(user != null){
Otp otp = otpService.validateOtp(passcode, user.getUserId());
if(otp != null){
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(user.getRole().name()));
authentication = new UsernamePasswordAuthenticationToken(user, otp, authorities);
return authentication;
}
}
return authentication;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
From controller side you need 2 endpoint to handle the login flow.
- First one to get email or mobile number as input and trigger the OTP flow.
- Second to get email or mobile along with OTP as input. Call the custom authentication handler created above with both the values. If authenticated create new JWT with all the valid payload and send it to client.
final String email = ... // from request body
final String otp = ... // from request body
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(email, otp)
);
if (authentication != null && authentication.isAuthenticated()) {
final User user = (User) authentication.getPrincipal();
final String token = jwtService.generateToken(user);
return new ResponseEntity<>(createAuthResponse(user, token), HttpStatus.CREATED); //createAuthResponse is private method to create formatted response for client
} else {
return ResponseEntity.badRequest().body("Verification Failed");
}
This JWT than can be further use to authenticate the user’s request, like in filter
Final part is now to wired them together using Spring configuration. Here you need to exempt the main login paths from authentication check, configure application to use above created custom authentication provider and filter.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
private final UserService userService;
private final OtpService otpService;
private PasscodeAuthenticationProvider authProvider;
public SecurityConfig(JwtAuthFilter jwtAuthFilter,
UserService userService,
OtpService otpService) {
this.jwtAuthFilter = jwtAuthFilter;
this.userService = userService;
this.otpService = otpService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/init", "/auth/verify", "/auth/register").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
if(authProvider == null){
authProvider = new PasscodeAuthenticationProvider();
authProvider.setUserService(userService);
authProvider.setOtpService(otpService);
}
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.authenticationProvider(authenticationProvider());
return authenticationManagerBuilder.getOrBuild();
}
}
And if you want to get current logged in user at any point you can use this:
public User getCurrentUser(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getPrincipal() instanceof User ? (User) authentication.getPrincipal() : null;
}
That’s it, try to run this and let us know if you face any issues. You can also use Swagger to test your application’s endpoints.



Post Comment