Skip to content

WebAuthn support #6842

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
wants to merge 9 commits into from
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,62 @@
/*
* 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
*
* https://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.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;

/**
* Allows configuring a {@link MultiFactorAuthenticationProvider}
*
* @param <B> the type of the {@link ProviderManagerBuilder}
*
* @author Yoshikazu Nojima
*/
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 @@ -390,6 +393,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 @@ -419,6 +427,7 @@ private Map<Class<?>, 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 @@ -15,8 +15,7 @@
*/
package org.springframework.security.config.annotation.web.configurers;

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 All @@ -30,6 +29,8 @@
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.util.LinkedHashMap;

/**
* Adds exception handling for Spring Security related exceptions to an application. All
* properties have reasonable defaults, so no additional configuration is required other
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) {
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 @@ -15,18 +15,13 @@
*/
package org.springframework.security.config.annotation.web.configurers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.config.Customizer;
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 All @@ -48,15 +43,16 @@
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.session.ConcurrentSessionFilter;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
import org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy;
import org.springframework.security.web.session.*;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* Allows configuring session management.
*
Expand Down Expand Up @@ -471,6 +467,11 @@ public void init(H http) {
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
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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user is not anonymous if the token is a multi factor authentication. This implementation should not consult isMultiFactorAuthentication to determine if the user is authenticated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MultiFactorAuthenticationToken represents a principal in the middle of multi factor (stage) authentication.
In the case of two-step authentication using a password and a security key, it indicates that the password has already been provided and authentication using the security key has not been completed.
Since authentication has not yet finished fully, I think it is appropriate to treat it as anonymous.
Do you think the name MultiFactorAuthenticationToken is inappropriate? I want a suggestion for a good name.

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
*
* https://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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The APIs need to consider that multi factor authentication is really an authorization requirement. It isn't sufficient to be able to answer if single factor authentication allowed. There needs to be the ability to answer:

Does any specific part of the application require another authentication factor? If so, we need to be able to request that specific factor from the user. There might be different types of factors that are triggered at different places within the application. A page that uses a credit card might require that the user has submitted their password again recently, a user with settings enabled for MFA may require the user to enter a code sent over SMS to access any part of the application, an administrator may require FIDO2 to access administrative parts of the applications, etc.

A possibility is that we should set these rules using granted authorities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The objective of this API is to check the user is allowed to login with single authentication factor or not.
It is intended to be used in MultiFactorAuthenticationProvider.
https://github.com/spring-projects/spring-security/pull/6842/files#diff-d87a3d8131f824e9c9a173b2a907dc92R68

To realize access-controll based on authentication level, yes, we should use granted authority.
I suppose users can implement it by handling the AuthenticationSuccessEvent, but it is not the scope of isSingleFactorAuthenticationAllowed method.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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
*
* https://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