Skip to content

Make a foundation for multi-factor(step) authentication including WebAuthn #5665

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.config.annotation.authentication.configurers.mfa;

import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.SecurityBuilder;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;

/**
*
* @param <B> the type of the {@link SecurityBuilder}
*/
public class MultiFactorAuthenticationProviderConfigurer<B extends ProviderManagerBuilder<B>>
extends SecurityConfigurerAdapter<AuthenticationManager, B> {

//~ Instance fields
// ================================================================================================
private AuthenticationProvider authenticationProvider;
private MFATokenEvaluator mfaTokenEvaluator = new MFATokenEvaluatorImpl();

/**
* Constructor
* @param authenticationProvider {@link AuthenticationProvider} to be delegated
*/
public MultiFactorAuthenticationProviderConfigurer(AuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}


public static MultiFactorAuthenticationProviderConfigurer multiFactorAuthenticationProvider(AuthenticationProvider authenticationProvider){
return new MultiFactorAuthenticationProviderConfigurer(authenticationProvider);
}

@Override
public void configure(B builder) {
MultiFactorAuthenticationProvider multiFactorAuthenticationProvider = new MultiFactorAuthenticationProvider(authenticationProvider, mfaTokenEvaluator);
multiFactorAuthenticationProvider = postProcess(multiFactorAuthenticationProvider);
builder.authenticationProvider(multiFactorAuthenticationProvider);
}

public MultiFactorAuthenticationProviderConfigurer<B> mfaTokenEvaluator(MFATokenEvaluator mfaTokenEvaluator) {
this.mfaTokenEvaluator = mfaTokenEvaluator;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.MFATokenEvaluator;
import org.springframework.security.authentication.MFATokenEvaluatorImpl;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
Expand Down Expand Up @@ -113,6 +115,7 @@ public <T> T postProcess(T object) {
private boolean authenticationManagerInitialized;
private AuthenticationManager authenticationManager;
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private MFATokenEvaluator mfaTokenEvaluator = new MFATokenEvaluatorImpl();
private HttpSecurity http;
private boolean disableDefaults;

Expand Down Expand Up @@ -391,6 +394,11 @@ public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
this.trustResolver = trustResolver;
}

@Autowired(required = false)
public void setMfaTokenEvaluator(MFATokenEvaluator mfaTokenEvaluator){
this.mfaTokenEvaluator = mfaTokenEvaluator;
}

@Autowired(required = false)
public void setContentNegotationStrategy(
ContentNegotiationStrategy contentNegotiationStrategy) {
Expand Down Expand Up @@ -420,6 +428,7 @@ private Map<Class<? extends Object>, Object> createSharedObjects() {
sharedObjects.put(ApplicationContext.class, context);
sharedObjects.put(ContentNegotiationStrategy.class, contentNegotiationStrategy);
sharedObjects.put(AuthenticationTrustResolver.class, trustResolver);
sharedObjects.put(MFATokenEvaluator.class, mfaTokenEvaluator);
return sharedObjects;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.util.LinkedHashMap;

import org.springframework.security.authentication.MFATokenEvaluator;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.AuthenticationEntryPoint;
Expand Down Expand Up @@ -67,6 +68,8 @@ public final class ExceptionHandlingConfigurer<H extends HttpSecurityBuilder<H>>

private AuthenticationEntryPoint authenticationEntryPoint;

private MFATokenEvaluator mfaTokenEvaluator;

private AccessDeniedHandler accessDeniedHandler;

private LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> defaultEntryPointMappings = new LinkedHashMap<>();
Expand Down Expand Up @@ -151,6 +154,18 @@ public ExceptionHandlingConfigurer<H> authenticationEntryPoint(
return this;
}

/**
* Specifies the {@link MFATokenEvaluator} to be used
*
* @param mfaTokenEvaluator the {@link MFATokenEvaluator} to be used
* @return the {@link ExceptionHandlingConfigurer} for further customization
*/
public ExceptionHandlingConfigurer<H> mfaTokenEvaluator(
MFATokenEvaluator mfaTokenEvaluator) {
this.mfaTokenEvaluator = mfaTokenEvaluator;
return this;
}

/**
* Sets a default {@link AuthenticationEntryPoint} to be used which prefers being
* invoked for the provided {@link RequestMatcher}. If only a single default
Expand Down Expand Up @@ -194,6 +209,9 @@ public void configure(H http) throws Exception {
entryPoint, getRequestCache(http));
AccessDeniedHandler deniedHandler = getAccessDeniedHandler(http);
exceptionTranslationFilter.setAccessDeniedHandler(deniedHandler);
if(mfaTokenEvaluator != null){
exceptionTranslationFilter.setMFATokenEvaluator(mfaTokenEvaluator);
}
exceptionTranslationFilter = postProcess(exceptionTranslationFilter);
http.addFilter(exceptionTranslationFilter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.MFATokenEvaluator;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
Expand Down Expand Up @@ -425,6 +426,11 @@ public void init(H http) throws Exception {
if (trustResolver != null) {
httpSecurityRepository.setTrustResolver(trustResolver);
}
MFATokenEvaluator mfaTokenEvaluator = http
.getSharedObject(MFATokenEvaluator.class);
if(mfaTokenEvaluator != null){
httpSecurityRepository.setMFATokenEvaluator(mfaTokenEvaluator);
}
http.setSharedObject(SecurityContextRepository.class,
httpSecurityRepository);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.config.annotation.authentication.configurers.mfa;

import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.MFATokenEvaluator;
import org.springframework.security.authentication.MultiFactorAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.springframework.security.config.annotation.authentication.configurers.mfa.MultiFactorAuthenticationProviderConfigurer.multiFactorAuthenticationProvider;

public class MultiFactorAuthenticationProviderConfigurerTests {

@Test
public void test(){
AuthenticationProvider delegatedAuthenticationProvider = mock(AuthenticationProvider.class);
MFATokenEvaluator mfaTokenEvaluator = mock(MFATokenEvaluator.class);
MultiFactorAuthenticationProviderConfigurer configurer
= multiFactorAuthenticationProvider(delegatedAuthenticationProvider);
configurer.mfaTokenEvaluator(mfaTokenEvaluator);
ProviderManagerBuilder providerManagerBuilder = mock(ProviderManagerBuilder.class);
configurer.configure(providerManagerBuilder);
ArgumentCaptor<AuthenticationProvider> argumentCaptor = ArgumentCaptor.forClass(AuthenticationProvider.class);
verify(providerManagerBuilder).authenticationProvider(argumentCaptor.capture());
MultiFactorAuthenticationProvider authenticationProvider = (MultiFactorAuthenticationProvider) argumentCaptor.getValue();

assertThat(authenticationProvider.getAuthenticationProvider()).isEqualTo(delegatedAuthenticationProvider);
assertThat(authenticationProvider.getMFATokenEvaluator()).isEqualTo(mfaTokenEvaluator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class AuthenticationTrustResolverImpl implements AuthenticationTrustResol
private Class<? extends Authentication> anonymousClass = AnonymousAuthenticationToken.class;
private Class<? extends Authentication> rememberMeClass = RememberMeAuthenticationToken.class;

private MFATokenEvaluator mfaTokenEvaluator = new MFATokenEvaluatorImpl();

// ~ Methods
// ========================================================================================================

Expand All @@ -52,6 +54,10 @@ public boolean isAnonymous(Authentication authentication) {
return false;
}

if(mfaTokenEvaluator != null && mfaTokenEvaluator.isMultiFactorAuthentication(authentication)){
return true;
}

return anonymousClass.isAssignableFrom(authentication.getClass());
}

Expand All @@ -70,4 +76,8 @@ public void setAnonymousClass(Class<? extends Authentication> anonymousClass) {
public void setRememberMeClass(Class<? extends Authentication> rememberMeClass) {
this.rememberMeClass = rememberMeClass;
}

public void setMFATokenEvaluator(MFATokenEvaluator mfaTokenEvaluator){
this.mfaTokenEvaluator = mfaTokenEvaluator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;

/**
* Evaluates <code>Authentication</code> tokens
*
* @author Yoshikazu Nojima
*/
public interface MFATokenEvaluator {

/**
* Indicates whether the passed <code>Authentication</code> token represents a
* user in the middle of multi factor authentication process.
*
* @param authentication to test (may be <code>null</code> in which case the method
* will always return <code>false</code>)
*
* @return <code>true</code> the passed authentication token represented a principal
* in the middle of multi factor authentication process, <code>false</code> otherwise
*/
boolean isMultiFactorAuthentication(Authentication authentication);

/**
* Indicates whether the principal associated with the <code>Authentication</code>
* token is allowed to login with only single factor.
*
* @param authentication to test (may be <code>null</code> in which case the method
* will always return <code>false</code>)
*
* @return <code>true</code> the principal associated with thepassed authentication
* token is allowed to login with only single factor, <code>false</code> otherwise
*/
boolean isSingleFactorAuthenticationAllowed(Authentication authentication);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.MFAUserDetails;

/**
* Basic implementation of {@link MFATokenEvaluator}.
* <p>
* Makes trust decisions based on whether the passed <code>Authentication</code> is an
* instance of a defined class.
* <p>
* If {@link #multiFactorClass} is <code>null</code>, the
* corresponding method will always return <code>false</code>.
*
* @author Yoshikazu Nojima
*/
public class MFATokenEvaluatorImpl implements MFATokenEvaluator {

private Class<? extends Authentication> multiFactorClass = MultiFactorAuthenticationToken.class;
private boolean singleFactorAuthenticationAllowed = true;

@Override
public boolean isMultiFactorAuthentication(Authentication authentication) {
if ((multiFactorClass == null) || (authentication == null)) {
return false;
}

return multiFactorClass.isAssignableFrom(authentication.getClass());
}

@Override
public boolean isSingleFactorAuthenticationAllowed(Authentication authentication) {
if(singleFactorAuthenticationAllowed && authentication.getPrincipal() instanceof MFAUserDetails){
MFAUserDetails webAuthnUserDetails = (MFAUserDetails) authentication.getPrincipal();
return webAuthnUserDetails.isSingleFactorAuthenticationAllowed();
}
return false;
}

Class<? extends Authentication> getMultiFactorClass() { return multiFactorClass; }

public void setMultiFactorClass(Class<? extends Authentication> multiFactorClass) {this.multiFactorClass = multiFactorClass; }

/**
* Check if single factor authentication is allowed
* @return true if single factor authentication is allowed
*/
public boolean isSingleFactorAuthenticationAllowed() {
return singleFactorAuthenticationAllowed;
}

/**
* Set single factor authentication is allowed
* @param singleFactorAuthenticationAllowed true if single factor authentication is allowed
*/
public void setSingleFactorAuthenticationAllowed(boolean singleFactorAuthenticationAllowed) {
this.singleFactorAuthenticationAllowed = singleFactorAuthenticationAllowed;
}

}
Loading