Careeroza
Custom AuthenticationProvider
When You Need One
Recall the architecture from the basic level: the AuthenticationManager (a ProviderManager) asks each AuthenticationProvider "do you support this kind of token?" and lets the first one that does try to authenticate it. To add a new login method — mobile number + OTP, an API key, a hardware token — you write a new token type and a provider for it.
POST /api/auth/otp/verify {phone, otp}
|
v
OtpAuthenticationToken(unauthenticated: "+919876543210", "482913")
|
ProviderManager --> DaoAuthenticationProvider.supports(OtpAuthenticationToken)? no
--> OtpAuthenticationProvider.supports(OtpAuthenticationToken)? yes
verify OTP -> load user -> OtpAuthenticationToken(authenticated, [ROLE_CUSTOMER])The Token
public class OtpAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal; // phone number before login, UserDetails after
private String otp;
private OtpAuthenticationToken(Object principal, String otp,
Collection<? extends GrantedAuthority> authorities, boolean authenticated) {
super(authorities);
this.principal = principal;
this.otp = otp;
super.setAuthenticated(authenticated);
}
public static OtpAuthenticationToken unauthenticated(String phone, String otp) {
return new OtpAuthenticationToken(phone, otp, null, false);
}
public static OtpAuthenticationToken authenticated(UserDetails user) {
return new OtpAuthenticationToken(user, null, user.getAuthorities(), true);
}
@Override public Object getCredentials() { return otp; }
@Override public Object getPrincipal() { return principal; }
@Override
public void eraseCredentials() {
super.eraseCredentials();
otp = null; // do not keep the OTP in memory after login
}
}The Provider
@Component
public class OtpAuthenticationProvider implements AuthenticationProvider {
private final OtpService otpService; // stores hashed OTPs with a 5-minute expiry
private final UserDetailsService users;
public OtpAuthenticationProvider(OtpService otpService, UserDetailsService users) {
this.otpService = otpService;
this.users = users;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String phone = authentication.getName();
String otp = (String) authentication.getCredentials();
if (!otpService.verifyAndConsume(phone, otp)) { // one-time: deleted after use
throw new BadCredentialsException("Invalid or expired OTP");
}
UserDetails user = users.loadUserByUsername(phone);
return OtpAuthenticationToken.authenticated(user);
}
@Override
public boolean supports(Class<?> authentication) {
return OtpAuthenticationToken.class.isAssignableFrom(authentication);
}
}Wiring and Using It
@Bean
AuthenticationManager authenticationManager(OtpAuthenticationProvider otpProvider,
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider passwordProvider = new DaoAuthenticationProvider(passwordEncoder);
passwordProvider.setUserDetailsService(userDetailsService);
return new ProviderManager(passwordProvider, otpProvider); // both login methods
}
@RestController
@RequestMapping("/api/auth/otp")
public class OtpLoginController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService; // from the medium level
// constructor omitted
@PostMapping("/verify")
public TokenResponse verify(@Valid @RequestBody OtpVerifyRequest req) {
Authentication auth = authenticationManager.authenticate(
OtpAuthenticationToken.unauthenticated(req.phone(), req.otp()));
return new TokenResponse(jwtService.createToken(auth), "Bearer", 900);
}
}First use works, replay fails
curl -s -X POST localhost:8080/api/auth/otp/verify -H "Content-Type: application/json" -d '{"phone":"+919876543210","otp":"482913"}'
curl -i -X POST localhost:8080/api/auth/otp/verify -H "Content-Type: application/json" -d '{"phone":"+919876543210","otp":"482913"}'Example output
{"accessToken":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIrOTE5ODc2NTQzMjEwIiwicm9sZXMiOlsiQ1VTVE9NRVIiXX0.k2...","tokenType":"Bearer","expiresIn":900}
HTTP/1.1 401
Content-Type: application/json
{"status":401,"error":"Unauthorized","message":"Invalid or expired OTP"}Tip: Store OTPs hashed (like passwords), allow at most 3–5 tries per OTP, expire them in 5 minutes, and rate-limit the "send OTP" endpoint — SMS costs money and is a favourite target for abuse ("SMS pumping").