12 changed files with 417 additions and 3 deletions
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
/* |
||||
* Copyright 2004-present 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.authorization; |
||||
|
||||
/** |
||||
* Condition under which multi-factor authentication is required. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 7.1 |
||||
* @see EnableMultiFactorAuthentication#when() |
||||
*/ |
||||
public enum MultiFactorCondition { |
||||
|
||||
/** |
||||
* Require multi-factor authentication only when the user has a WebAuthn credential |
||||
* record registered. |
||||
* <p> |
||||
* When this condition is specified, |
||||
* {@link EnableMultiFactorAuthentication#authorities()} must include |
||||
* {@link org.springframework.security.core.authority.FactorGrantedAuthority#WEBAUTHN_AUTHORITY}. |
||||
* Failing to include it results in an {@link IllegalArgumentException} when the |
||||
* configuration is processed. |
||||
* <p> |
||||
* Using this condition also requires both a |
||||
* {@link org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository} |
||||
* Bean and a |
||||
* {@link org.springframework.security.web.webauthn.management.UserCredentialRepository} |
||||
* Bean to be published. |
||||
* |
||||
* <pre> |
||||
* @Bean |
||||
* public PublicKeyCredentialUserEntityRepository userEntityRepository() { |
||||
* return new InMemoryPublicKeyCredentialUserEntityRepository(); |
||||
* } |
||||
* |
||||
* @Bean |
||||
* public UserCredentialRepository userCredentialRepository() { |
||||
* return new InMemoryUserCredentialRepository(); |
||||
* } |
||||
* </pre> |
||||
*/ |
||||
WEBAUTHN_REGISTERED |
||||
|
||||
} |
||||
@ -0,0 +1,90 @@
@@ -0,0 +1,90 @@
|
||||
/* |
||||
* Copyright 2004-present 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.authorization; |
||||
|
||||
import java.util.function.Predicate; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.security.authorization.AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder; |
||||
import org.springframework.security.config.Customizer; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.web.webauthn.api.PublicKeyCredentialUserEntity; |
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository; |
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository; |
||||
|
||||
/** |
||||
* Configuration that provides a |
||||
* {@link Customizer}<{@link AdditionalRequiredFactorsBuilder}> for |
||||
* {@link MultiFactorCondition#WEBAUTHN_REGISTERED}, requiring multi-factor authentication |
||||
* only when the user has a WebAuthn credential record. |
||||
* |
||||
* @author Rob Winch |
||||
* @since 7.1 |
||||
* @see EnableMultiFactorAuthentication#when() |
||||
* @see MultiFactorCondition#WEBAUTHN_REGISTERED |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
class WhenWebAuthnRegisteredMfaConfiguration { |
||||
|
||||
@Bean |
||||
Customizer<AdditionalRequiredFactorsBuilder<Object>> additionalRequiredFactorsCustomizer( |
||||
PublicKeyCredentialUserEntityRepository userEntityRepository, |
||||
UserCredentialRepository userCredentialRepository) { |
||||
return (builder) -> builder.withWhen((current) -> { |
||||
Predicate<Authentication> webAuthnRegisteredPredicate = new WebAuthnRegisteredPredicate( |
||||
userEntityRepository, userCredentialRepository); |
||||
if (current == null) { |
||||
return webAuthnRegisteredPredicate; |
||||
} |
||||
return current.and(webAuthnRegisteredPredicate); |
||||
}); |
||||
} |
||||
|
||||
private static final class WebAuthnRegisteredPredicate implements Predicate<Authentication> { |
||||
|
||||
private final PublicKeyCredentialUserEntityRepository userEntityRepository; |
||||
|
||||
private final UserCredentialRepository userCredentialRepository; |
||||
|
||||
private WebAuthnRegisteredPredicate(PublicKeyCredentialUserEntityRepository userEntityRepository, |
||||
UserCredentialRepository userCredentialRepository) { |
||||
this.userEntityRepository = userEntityRepository; |
||||
this.userCredentialRepository = userCredentialRepository; |
||||
} |
||||
|
||||
@Override |
||||
public boolean test(Authentication authentication) { |
||||
if (authentication == null || authentication.getName() == null) { |
||||
return false; |
||||
} |
||||
PublicKeyCredentialUserEntity userEntity = this.userEntityRepository |
||||
.findByUsername(authentication.getName()); |
||||
if (userEntity == null) { |
||||
return false; |
||||
} |
||||
return !this.userCredentialRepository.findByUserId(userEntity.getId()).isEmpty(); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "WEBAUTHN_REGISTERED"; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,108 @@
@@ -0,0 +1,108 @@
|
||||
/* |
||||
* Copyright 2004-present 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.authorization; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.core.type.AnnotationMetadata; |
||||
import org.springframework.security.core.authority.FactorGrantedAuthority; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.Mockito.mock; |
||||
|
||||
/** |
||||
* Tests for {@link MultiFactorAuthenticationSelector}. |
||||
* |
||||
* @author Rob Winch |
||||
*/ |
||||
class MultiFactorAuthenticationSelectorTests { |
||||
|
||||
private final MultiFactorAuthenticationSelector selector = new MultiFactorAuthenticationSelector(); |
||||
|
||||
@Test |
||||
void selectImportsWhenWhenIsEmptyAndAuthoritiesSpecifiedThenReturnsImportsWithoutWebAuthnConfig() { |
||||
AnnotationMetadata metadata = metadata(new MultiFactorCondition[0], FactorGrantedAuthority.OTT_AUTHORITY, |
||||
FactorGrantedAuthority.PASSWORD_AUTHORITY); |
||||
String[] imports = this.selector.selectImports(metadata); |
||||
assertThat(imports).isNotEmpty(); |
||||
assertThat(imports).doesNotContain(WhenWebAuthnRegisteredMfaConfiguration.class.getName()); |
||||
} |
||||
|
||||
@Test |
||||
void selectImportsWhenWhenOmittedThenDefaultsToEmptyAndReturnsImports() { |
||||
AnnotationMetadata metadata = metadataWithoutWhen(FactorGrantedAuthority.OTT_AUTHORITY, |
||||
FactorGrantedAuthority.PASSWORD_AUTHORITY); |
||||
String[] imports = this.selector.selectImports(metadata); |
||||
assertThat(imports).isNotEmpty(); |
||||
assertThat(imports).doesNotContain(WhenWebAuthnRegisteredMfaConfiguration.class.getName()); |
||||
} |
||||
|
||||
@Test |
||||
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesIncludesWebAuthnThenReturnsImports() { |
||||
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED }, |
||||
FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY, |
||||
FactorGrantedAuthority.WEBAUTHN_AUTHORITY); |
||||
String[] imports = this.selector.selectImports(metadata); |
||||
assertThat(imports).isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesOnlyWebAuthnThenReturnsImports() { |
||||
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED }, |
||||
FactorGrantedAuthority.WEBAUTHN_AUTHORITY); |
||||
String[] imports = this.selector.selectImports(metadata); |
||||
assertThat(imports).isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesEmptyThenThrowsException() { |
||||
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED }); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.selector.selectImports(metadata)) |
||||
.withMessageContaining("authorities() must include " + FactorGrantedAuthority.WEBAUTHN_AUTHORITY); |
||||
} |
||||
|
||||
@Test |
||||
void selectImportsWhenHasWebAuthnConditionAndAuthoritiesExcludesWebAuthnThenThrowsException() { |
||||
AnnotationMetadata metadata = metadata(new MultiFactorCondition[] { MultiFactorCondition.WEBAUTHN_REGISTERED }, |
||||
FactorGrantedAuthority.OTT_AUTHORITY, FactorGrantedAuthority.PASSWORD_AUTHORITY); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.selector.selectImports(metadata)) |
||||
.withMessageContaining("authorities() must include " + FactorGrantedAuthority.WEBAUTHN_AUTHORITY); |
||||
} |
||||
|
||||
private static AnnotationMetadata metadata(MultiFactorCondition[] when, String... authorities) { |
||||
AnnotationMetadata metadata = mock(AnnotationMetadata.class); |
||||
Map<String, Object> attrs = new HashMap<>(); |
||||
attrs.put("authorities", authorities); |
||||
attrs.put("when", when); |
||||
given(metadata.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName())).willReturn(attrs); |
||||
return metadata; |
||||
} |
||||
|
||||
private static AnnotationMetadata metadataWithoutWhen(String... authorities) { |
||||
AnnotationMetadata metadata = mock(AnnotationMetadata.class); |
||||
Map<String, Object> attrs = new HashMap<>(); |
||||
attrs.put("authorities", authorities); |
||||
given(metadata.getAnnotationAttributes(EnableMultiFactorAuthentication.class.getName())).willReturn(attrs); |
||||
return metadata; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package org.springframework.security.docs.servlet.authentication.mfawhencustomconditions; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.security.authorization.AuthorizationManagerFactories; |
||||
import org.springframework.security.config.Customizer; |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
class CustomizerAuthorizationManagerFactoryConfiguration { |
||||
|
||||
// tag::customizer[]
|
||||
@Bean |
||||
Customizer<AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Object>> additionalRequiredFactorsCustomizer() { |
||||
return (builder) -> builder.when((auth) -> "admin".equals(auth.getName())); |
||||
} |
||||
// end::customizer[]
|
||||
|
||||
} |
||||
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
package org.springframework.security.docs.servlet.authentication.mfawhenwebauthnregistered; |
||||
|
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication; |
||||
import org.springframework.security.config.annotation.authorization.MultiFactorCondition; |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
||||
import org.springframework.security.core.authority.FactorGrantedAuthority; |
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository; |
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository; |
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository; |
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository; |
||||
|
||||
@EnableWebSecurity |
||||
@Configuration(proxyBeanMethods = false) |
||||
// tag::enable-mfa-webauthn[]
|
||||
@EnableMultiFactorAuthentication( |
||||
authorities = { |
||||
FactorGrantedAuthority.PASSWORD_AUTHORITY, |
||||
FactorGrantedAuthority.WEBAUTHN_AUTHORITY |
||||
}, |
||||
when = MultiFactorCondition.WEBAUTHN_REGISTERED |
||||
) |
||||
public class WebAuthnConditionConfiguration { |
||||
|
||||
@Bean |
||||
public PublicKeyCredentialUserEntityRepository userEntityRepository() { |
||||
return new MapPublicKeyCredentialUserEntityRepository(); |
||||
} |
||||
|
||||
@Bean |
||||
public UserCredentialRepository userCredentialRepository() { |
||||
return new MapUserCredentialRepository(); |
||||
} |
||||
|
||||
} |
||||
// end::enable-mfa-webauthn[]
|
||||
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.mfawhencustomconditions |
||||
|
||||
import org.springframework.context.annotation.Bean |
||||
import org.springframework.context.annotation.Configuration |
||||
import org.springframework.security.authorization.AuthorizationManagerFactories |
||||
import org.springframework.security.config.Customizer |
||||
|
||||
@Configuration(proxyBeanMethods = false) |
||||
internal class CustomizerAuthorizationManagerFactoryConfiguration { |
||||
|
||||
// tag::customizer[] |
||||
@Bean |
||||
fun additionalRequiredFactorsCustomizer(): Customizer<AuthorizationManagerFactories.AdditionalRequiredFactorsBuilder<Any>> { |
||||
return Customizer { builder -> builder.`when` { auth -> "admin" == auth.name } } |
||||
} |
||||
// end::customizer[] |
||||
|
||||
} |
||||
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
package org.springframework.security.kt.docs.servlet.authentication.mfawhenwebauthnregistered |
||||
|
||||
import org.springframework.context.annotation.Bean |
||||
import org.springframework.context.annotation.Configuration |
||||
import org.springframework.security.config.annotation.authorization.EnableMultiFactorAuthentication |
||||
import org.springframework.security.config.annotation.authorization.MultiFactorCondition |
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity |
||||
import org.springframework.security.core.authority.FactorGrantedAuthority |
||||
import org.springframework.security.web.webauthn.management.MapPublicKeyCredentialUserEntityRepository |
||||
import org.springframework.security.web.webauthn.management.MapUserCredentialRepository |
||||
import org.springframework.security.web.webauthn.management.PublicKeyCredentialUserEntityRepository |
||||
import org.springframework.security.web.webauthn.management.UserCredentialRepository |
||||
|
||||
@EnableWebSecurity |
||||
@Configuration(proxyBeanMethods = false) |
||||
// tag::enable-mfa-webauthn[] |
||||
@EnableMultiFactorAuthentication( |
||||
authorities = [ |
||||
FactorGrantedAuthority.PASSWORD_AUTHORITY, |
||||
FactorGrantedAuthority.WEBAUTHN_AUTHORITY |
||||
], |
||||
`when` = [MultiFactorCondition.WEBAUTHN_REGISTERED] |
||||
) |
||||
internal class WebAuthnConditionConfiguration { |
||||
|
||||
@Bean |
||||
fun userEntityRepository(): PublicKeyCredentialUserEntityRepository { |
||||
return MapPublicKeyCredentialUserEntityRepository() |
||||
} |
||||
|
||||
@Bean |
||||
fun userCredentialRepository(): UserCredentialRepository { |
||||
return MapUserCredentialRepository() |
||||
} |
||||
|
||||
} |
||||
// end::enable-mfa-webauthn[] |
||||
Loading…
Reference in new issue