Skip to content

Commit c1fc9ae

Browse files
committed
Add MultiFactorAuthenticationProvider
1 parent f1ccf2f commit c1fc9ae

File tree

7 files changed

+305
-2
lines changed

7 files changed

+305
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.authentication.configurers.mfa;
18+
19+
import org.springframework.security.authentication.*;
20+
import org.springframework.security.config.annotation.SecurityBuilder;
21+
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
22+
import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;
23+
24+
/**
25+
*
26+
* @param <B> the type of the {@link SecurityBuilder}
27+
*/
28+
public class MultiFactorAuthenticationProviderConfigurer<B extends ProviderManagerBuilder<B>>
29+
extends SecurityConfigurerAdapter<AuthenticationManager, B> {
30+
31+
//~ Instance fields
32+
// ================================================================================================
33+
private AuthenticationProvider authenticationProvider;
34+
private MFATokenEvaluator mfaTokenEvaluator = new MFATokenEvaluatorImpl();
35+
36+
/**
37+
* Constructor
38+
* @param authenticationProvider {@link AuthenticationProvider} to be delegated
39+
*/
40+
public MultiFactorAuthenticationProviderConfigurer(AuthenticationProvider authenticationProvider) {
41+
this.authenticationProvider = authenticationProvider;
42+
}
43+
44+
45+
public static MultiFactorAuthenticationProviderConfigurer multiFactorAuthenticationProvider(AuthenticationProvider authenticationProvider){
46+
return new MultiFactorAuthenticationProviderConfigurer(authenticationProvider);
47+
}
48+
49+
@Override
50+
public void configure(B builder) {
51+
MultiFactorAuthenticationProvider multiFactorAuthenticationProvider = new MultiFactorAuthenticationProvider(authenticationProvider, mfaTokenEvaluator);
52+
multiFactorAuthenticationProvider = postProcess(multiFactorAuthenticationProvider);
53+
builder.authenticationProvider(multiFactorAuthenticationProvider);
54+
}
55+
56+
public MultiFactorAuthenticationProviderConfigurer<B> mfaTokenEvaluator(MFATokenEvaluator mfaTokenEvaluator) {
57+
this.mfaTokenEvaluator = mfaTokenEvaluator;
58+
return this;
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.springframework.security.config.annotation.authentication.configurers.mfa;
2+
3+
import org.junit.Test;
4+
import org.mockito.ArgumentCaptor;
5+
import org.springframework.security.authentication.AuthenticationProvider;
6+
import org.springframework.security.authentication.MFATokenEvaluator;
7+
import org.springframework.security.authentication.MultiFactorAuthenticationProvider;
8+
import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;
9+
10+
import static org.assertj.core.api.Assertions.assertThat;
11+
import static org.mockito.Mockito.mock;
12+
import static org.mockito.Mockito.verify;
13+
import static org.springframework.security.config.annotation.authentication.configurers.mfa.MultiFactorAuthenticationProviderConfigurer.multiFactorAuthenticationProvider;
14+
15+
public class MultiFactorAuthenticationProviderConfigurerTests {
16+
17+
@Test
18+
public void test(){
19+
AuthenticationProvider delegatedAuthenticationProvider = mock(AuthenticationProvider.class);
20+
MFATokenEvaluator mfaTokenEvaluator = mock(MFATokenEvaluator.class);
21+
MultiFactorAuthenticationProviderConfigurer configurer
22+
= multiFactorAuthenticationProvider(delegatedAuthenticationProvider);
23+
configurer.mfaTokenEvaluator(mfaTokenEvaluator);
24+
ProviderManagerBuilder providerManagerBuilder = mock(ProviderManagerBuilder.class);
25+
configurer.configure(providerManagerBuilder);
26+
ArgumentCaptor<AuthenticationProvider> argumentCaptor = ArgumentCaptor.forClass(AuthenticationProvider.class);
27+
verify(providerManagerBuilder).authenticationProvider(argumentCaptor.capture());
28+
MultiFactorAuthenticationProvider authenticationProvider = (MultiFactorAuthenticationProvider)argumentCaptor.getValue();
29+
30+
assertThat(authenticationProvider.getAuthenticationProvider()).isEqualTo(delegatedAuthenticationProvider);
31+
assertThat(authenticationProvider.getMFATokenEvaluator()).isEqualTo(mfaTokenEvaluator);
32+
}
33+
}

core/src/main/java/org/springframework/security/authentication/MFATokenEvaluator.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,16 @@ public interface MFATokenEvaluator {
3636
* in the middle of multi factor authentication process, <code>false</code> otherwise
3737
*/
3838
boolean isMultiFactorAuthentication(Authentication authentication);
39+
40+
/**
41+
* Indicates whether the principal associated with the <code>Authentication</code>
42+
* token is allowed to login with only single factor.
43+
*
44+
* @param authentication to test (may be <code>null</code> in which case the method
45+
* will always return <code>false</code>)
46+
*
47+
* @return <code>true</code> the principal associated with thepassed authentication
48+
* token is allowed to login with only single factor, <code>false</code> otherwise
49+
*/
50+
boolean isSingleFactorAuthenticationAllowed(Authentication authentication);
3951
}

core/src/main/java/org/springframework/security/authentication/MFATokenEvaluatorImpl.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.security.authentication;
1818

1919
import org.springframework.security.core.Authentication;
20+
import org.springframework.security.core.userdetails.MFAUserDetails;
2021

2122
/**
2223
* Basic implementation of {@link MFATokenEvaluator}.
@@ -32,8 +33,7 @@
3233
public class MFATokenEvaluatorImpl implements MFATokenEvaluator {
3334

3435
private Class<? extends Authentication> multiFactorClass = MultiFactorAuthenticationToken.class;
35-
36-
Class<? extends Authentication> getMultiFactorClass() { return multiFactorClass; }
36+
private boolean singleFactorAuthenticationAllowed = true;
3737

3838
@Override
3939
public boolean isMultiFactorAuthentication(Authentication authentication) {
@@ -44,6 +44,33 @@ public boolean isMultiFactorAuthentication(Authentication authentication) {
4444
return multiFactorClass.isAssignableFrom(authentication.getClass());
4545
}
4646

47+
@Override
48+
public boolean isSingleFactorAuthenticationAllowed(Authentication authentication) {
49+
if(singleFactorAuthenticationAllowed && authentication.getPrincipal() instanceof MFAUserDetails){
50+
MFAUserDetails webAuthnUserDetails = (MFAUserDetails) authentication.getPrincipal();
51+
return webAuthnUserDetails.isSingleFactorAuthenticationAllowed();
52+
}
53+
return false;
54+
}
55+
56+
Class<? extends Authentication> getMultiFactorClass() { return multiFactorClass; }
57+
4758
public void setMultiFactorClass(Class<? extends Authentication> multiFactorClass) {this.multiFactorClass = multiFactorClass; }
4859

60+
/**
61+
* Check if single factor authentication is allowed
62+
* @return true if single factor authentication is allowed
63+
*/
64+
public boolean isSingleFactorAuthenticationAllowed() {
65+
return singleFactorAuthenticationAllowed;
66+
}
67+
68+
/**
69+
* Set single factor authentication is allowed
70+
* @param singleFactorAuthenticationAllowed true if single factor authentication is allowed
71+
*/
72+
public void setSingleFactorAuthenticationAllowed(boolean singleFactorAuthenticationAllowed) {
73+
this.singleFactorAuthenticationAllowed = singleFactorAuthenticationAllowed;
74+
}
75+
4976
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.authentication;
18+
19+
import org.springframework.context.support.MessageSourceAccessor;
20+
import org.springframework.security.core.Authentication;
21+
import org.springframework.security.core.SpringSecurityMessageSource;
22+
import org.springframework.util.Assert;
23+
24+
import java.util.Collections;
25+
26+
/**
27+
* An {@link AuthenticationProvider} implementation for the first factor(step) of multi factor authentication.
28+
* Authentication itself is delegated to another {@link AuthenticationProvider}.
29+
*/
30+
public class MultiFactorAuthenticationProvider implements AuthenticationProvider {
31+
32+
33+
// ~ Instance fields
34+
// ================================================================================================
35+
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
36+
37+
/**
38+
* {@link AuthenticationProvider} to be delegated
39+
*/
40+
private AuthenticationProvider authenticationProvider;
41+
private MFATokenEvaluator mfaTokenEvaluator;
42+
43+
/**
44+
* Constructor
45+
* @param authenticationProvider {@link AuthenticationProvider} to be delegated
46+
*/
47+
public MultiFactorAuthenticationProvider(AuthenticationProvider authenticationProvider, MFATokenEvaluator mfaTokenEvaluator) {
48+
Assert.notNull(authenticationProvider, "authenticationProvider must be set");
49+
Assert.notNull(mfaTokenEvaluator, "mfaTokenEvaluator must be set");
50+
this.authenticationProvider = authenticationProvider;
51+
this.mfaTokenEvaluator = mfaTokenEvaluator;
52+
}
53+
54+
/**
55+
* {@inheritDoc}
56+
*/
57+
@Override
58+
public Authentication authenticate(Authentication authentication) {
59+
if (!supports(authentication.getClass())) {
60+
throw new IllegalArgumentException("Not supported AuthenticationToken " + authentication.getClass() + " was attempted");
61+
}
62+
63+
Authentication result = authenticationProvider.authenticate(authentication);
64+
65+
if(mfaTokenEvaluator.isSingleFactorAuthenticationAllowed(result)){
66+
return result;
67+
}
68+
69+
return new MultiFactorAuthenticationToken(
70+
result.getPrincipal(),
71+
result.getCredentials(),
72+
Collections.emptyList() // result.getAuthorities() is not used as not to inherit authorities from result
73+
);
74+
}
75+
76+
/**
77+
* {@inheritDoc}
78+
*/
79+
@Override
80+
public boolean supports(Class<?> authentication) {
81+
return authenticationProvider.supports(authentication);
82+
}
83+
84+
/**
85+
* {@link AuthenticationProvider} to be delegated
86+
*/
87+
public AuthenticationProvider getAuthenticationProvider() {
88+
return authenticationProvider;
89+
}
90+
91+
public MFATokenEvaluator getMFATokenEvaluator() {
92+
return mfaTokenEvaluator;
93+
}
94+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.springframework.security.core.userdetails;
2+
3+
public interface MFAUserDetails extends UserDetails {
4+
5+
boolean isSingleFactorAuthenticationAllowed();
6+
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.springframework.security.authentication;
2+
3+
4+
import org.junit.Test;
5+
import org.springframework.security.core.Authentication;
6+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
7+
import org.springframework.security.core.userdetails.MFAUserDetails;
8+
9+
import java.util.Collections;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.mockito.ArgumentMatchers.any;
13+
import static org.mockito.Mockito.mock;
14+
import static org.mockito.Mockito.when;
15+
16+
public class MultiFactorAuthenticationProviderTests {
17+
18+
@Test
19+
public void authenticate_with_singleFactorAuthenticationAllowedOption_false_test(){
20+
AuthenticationProvider delegatedAuthenticationProvider = mock(AuthenticationProvider.class);
21+
MFAUserDetails userDetails = mock(MFAUserDetails.class);
22+
when(userDetails.isSingleFactorAuthenticationAllowed()).thenReturn(true);
23+
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, Collections.emptyList());
24+
authenticationToken.setDetails(userDetails);
25+
when(delegatedAuthenticationProvider.supports(any())).thenReturn(true);
26+
when(delegatedAuthenticationProvider.authenticate(any()))
27+
.thenReturn(new UsernamePasswordAuthenticationToken(
28+
"principal",
29+
"credentials",
30+
Collections.singletonList(new SimpleGrantedAuthority("ROLE_DUMMY"))
31+
));
32+
33+
MultiFactorAuthenticationProvider provider = new MultiFactorAuthenticationProvider(delegatedAuthenticationProvider, new MFATokenEvaluatorImpl());
34+
Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("dummy", "dummy"));
35+
36+
assertThat(result).isInstanceOf(MultiFactorAuthenticationToken.class);
37+
assertThat(result.getPrincipal()).isEqualTo("principal");
38+
assertThat(result.getCredentials()).isEqualTo("credentials");
39+
assertThat(result.getAuthorities()).isEmpty();
40+
41+
}
42+
43+
@Test
44+
public void authenticate_with_singleFactorAuthenticationAllowedOption_true_test(){
45+
AuthenticationProvider delegatedAuthenticationProvider = mock(AuthenticationProvider.class);
46+
MFAUserDetails userDetails = mock(MFAUserDetails.class);
47+
when(userDetails.isSingleFactorAuthenticationAllowed()).thenReturn(true);
48+
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, Collections.emptyList());
49+
authenticationToken.setDetails(userDetails);
50+
when(delegatedAuthenticationProvider.supports(any())).thenReturn(true);
51+
when(delegatedAuthenticationProvider.authenticate(any()))
52+
.thenReturn(authenticationToken);
53+
54+
MultiFactorAuthenticationProvider provider = new MultiFactorAuthenticationProvider(delegatedAuthenticationProvider, new MFATokenEvaluatorImpl());
55+
Authentication result = provider.authenticate(new UsernamePasswordAuthenticationToken("dummy", "dummy"));
56+
57+
assertThat(result).isInstanceOf(UsernamePasswordAuthenticationToken.class);
58+
assertThat(result).isEqualTo(result);
59+
}
60+
61+
@Test(expected = IllegalArgumentException.class)
62+
public void authenticate_with_invalid_AuthenticationToken_test(){
63+
AuthenticationProvider delegatedAuthenticationProvider = mock(AuthenticationProvider.class);
64+
when(delegatedAuthenticationProvider.supports(any())).thenReturn(false);
65+
66+
MultiFactorAuthenticationProvider provider = new MultiFactorAuthenticationProvider(delegatedAuthenticationProvider, new MFATokenEvaluatorImpl());
67+
provider.authenticate(new TestingAuthenticationToken("dummy", "dummy"));
68+
}
69+
70+
}

0 commit comments

Comments
 (0)