spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.55k stars 5.79k forks source link

Add Support for Passwordless Authentication with OTP #15114

Open marcusdacoregio opened 1 month ago

marcusdacoregio commented 1 month ago

We should add support for one time token authentication, one common example is magic links sent in email or a text code to log a user in.

Note that this is not the same as https://github.com/spring-projects/spring-security/issues/3046 since it is not in the scope of this ticket to support MFA.

CrazyParanoid commented 1 month ago

Hi @marcusdacoregio ! I can share my implementation which I use in my projects. The security configuration looks like this:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, OneTimePasswordVerificationFilter oneTimePasswordVerificationFilter) throws Exception {
        return http.httpBasic(AbstractHttpConfigurer::disable)
                        .csrf(AbstractHttpConfigurer::disable)
                        .authorizeHttpRequests(auth -> auth
                                .requestMatchers("/login/otp/**", "/otp/**")
                                .permitAll()
                                .anyRequest()
                                .authenticated())
                        .oauth2Login(login -> login.successHandler(new OneTimePasswordAuthenticationHandler()))
                        .addFilterAfter(new DefaultOneTimePasswordPageGeneratingFilter(), DefaultLoginPageGeneratingFilter.class)
                        .addFilterBefore(oneTimePasswordVerificationFilter, AuthorizationFilter.class)
                        .build();

    }

    @Bean
    AuthenticationProvider oneTimePasswordAuthenticationProvider() {
        return new OneTimePasswordAuthenticationProvider(new DefaultOneTimePasswordAuthenticationVerifier());
    }

    @Bean
    OneTimePasswordVerificationFilter oneTimePasswordVerificationFilter(AuthenticationProvider oneTimePasswordAuthenticationProvider) {
        OneTimePasswordVerificationFilter filter = new OneTimePasswordVerificationFilter(oneTimePasswordAuthenticationProvider::authenticate);
        filter.setAuthenticationFailureHandler(new OneTimePasswordAuthenticationHandler());
        return filter;
    }
}

I have several components, first of all this is a filter for verifying otp code OneTimePasswordVerificationFilter. It interacts with OneTimePasswordAuthenticationProvider, which verifies the otp code:

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    SecurityContext securityContext = SecurityContextHolder.getContext();
    Authentication authentication = securityContext.getAuthentication();
    String code = request.getParameter(otpCodeParameter);

    OneTimePasswordAuthenticationToken authenticationToken = new OneTimePasswordAuthenticationToken(authentication, code);

    return this.getAuthenticationManager().authenticate(authenticationToken);
}

OneTimePasswordAuthenticationProvider performs verification using the OneTimePasswordAuthenticationVerifier component:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    OneTimePasswordAuthenticationToken authenticationToken = (OneTimePasswordAuthenticationToken) authentication;
    try {
         oneTimePasswordAuthenticationVerifier.verify(authenticationToken);
    } catch (OAuth2AuthorizationException ex) {
         OAuth2Error oauth2Error = ex.getError();
         throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
     }

    return authenticationToken.getAuthentication();
}

Implementation OneTimePasswordAuthenticationVerifier of depends on the authenticator chosen, for example GoogleAuthenticator.

public interface OneTimePasswordAuthenticationVerifier {

    void verify(OneTimePasswordAuthenticationToken authenticationToken);

}

To generate the code entry page, use DefaultOneTimePasswordPageGeneratingFilter. OneTimePasswordAuthenticationHandler provides a redirect to the page for entering the otp code after successful oauth 2.0 authorization. I'm currently using such an implementation as part of the ouath 2.0 client along with spring cloud gateway.

evgeniycheban commented 3 weeks ago

Hi, @marcusdacoregio can I work on it?

marcusdacoregio commented 3 weeks ago

Hi @evgeniycheban, this feature will require several design interactions with the team members since we have other features in the horizon that might be affected by it (MFA for example). Because of that I think it will be better if one of the maintainers work on it.

However, since you have an extensive knowledge of the code base and have contributed to a few core features, I'd love to hear how you see this feature implemented, either a high level idea or some code.