From 3c55befd54d52bda68b038aef325f33bc1d7157c Mon Sep 17 00:00:00 2001 From: Arjan Vlek Date: Fri, 17 Dec 2021 09:50:21 +0100 Subject: [PATCH] feat(MFA): Added support for custom verification checks Added support for custom MFA verification checks, such as receiving MFA codes using e-mail and SMS. These usually are valid for a longer period, so the default TOTP validator cannot be used. The current version of rest-secure did not allow such checks, since if it detected a `verificationCode` and the user's `isMfaConfigured()` returned true, it would throw a bad credentials exception. Since these checks are usually in some part dependent on the implementing application, I've chosen not to implement the checks in the library, but made it easy to extend the default flow by adding support for custom checks. --- CHANGELOG.md | 3 + README.md | 12 + pom.xml | 12 +- .../mfa/MfaAuthenticationProvider.java | 36 ++- .../mfa/MfaTotpVerificationCheck.java | 26 +++ .../mfa/MfaVerificationCheck.java | 20 ++ .../mfa/MfaAuthenticationProviderTest.java | 208 +++++++++++++++++- .../mfa/MfaTotpVerificationCheckTest.java | 35 +++ .../test/ActiveUserAndMfaConfig.java | 1 - 9 files changed, 345 insertions(+), 8 deletions(-) create mode 100644 src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheck.java create mode 100644 src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaVerificationCheck.java create mode 100644 src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheckTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a93ead..3b44a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [10.0.0] - 2021-12-17 +- Added: Support for custom MFA checks, e.g. to also accept codes from email and SMS. + ## [9.0.1] - 2021-11-18 - Fixed: #20 The label of the 2FA QR code should get the issuer added as well to work properly with all authenticator apps. diff --git a/README.md b/README.md index 8337fe6..1874d35 100644 --- a/README.md +++ b/README.md @@ -363,6 +363,18 @@ class CustomSecurity { } ``` +### Adding custom MFA verification checks (e.g. email, SMS) +The `MfaAuthenticationProvider` supports custom authentication checks in addition to the default TOTP validator. +If, for example, some portion of users receive their code through other means, it may be needed to add a custom verification check. + +Note that the default TOTP check *always* gets added to the chain. +If it is not already supplied in the list of custom checks, it will be added after the last custom verification check. + +A verification check should behave as following: +- If the user is successfully authenticated using the given verification code, return `true`. The user will be logged in and no other checks will be performed. +- If the check is not applicable to this user (e.g. this user does not receive codes using SMS), return `false`. The next check will then be executed. +- If the user has supplied incorrect credentials, the check must throw a subclass of `AuthenticationException` to abort the chain of checks and return a login error. + ### Remember me (single sign on) - Register a `RememberMeServices` bean, this will be picked up automatically and used in the login filter diff --git a/pom.xml b/pom.xml index 6f409e5..44bf4bd 100644 --- a/pom.xml +++ b/pom.xml @@ -59,7 +59,7 @@ UTF-8 11 11 - 2.5.6 + 2.5.7 1.7.1 6.5.0 @@ -122,6 +122,16 @@ + + + + + org.apache.logging.log4j + log4j-bom + 2.16.0 + import + pom + org.springframework.boot spring-boot-dependencies diff --git a/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProvider.java b/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProvider.java index 4a12ec9..e7603e5 100644 --- a/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProvider.java +++ b/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProvider.java @@ -1,10 +1,11 @@ package nl._42.restsecure.autoconfigure.authentication.mfa; +import java.util.ArrayList; +import java.util.List; + import nl._42.restsecure.autoconfigure.authentication.RegisteredUser; import nl._42.restsecure.autoconfigure.authentication.UserDetailsAdapter; -import nl._42.restsecure.autoconfigure.errorhandling.DefaultLoginAuthenticationExceptionHandler; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -20,8 +21,15 @@ public class MfaAuthenticationProvider extends DaoAuthenticationProvider { public static final String SERVER_MFA_CODE_REQUIRED_ERROR = "SERVER.MFA_CODE_REQUIRED_ERROR"; public static final String DETAILS_MFA_SETUP_REQUIRED = "DETAILS.MFA_SETUP_REQUIRED"; + private boolean customVerificationStepsRegistered = false; + private List verificationChecks; private MfaValidationService mfaValidationService; + public void setVerificationChecks(List verificationChecks) { + this.customVerificationStepsRegistered = true; + this.verificationChecks = verificationChecks; + } + public void setMfaValidationService(MfaValidationService mfaValidationService) { this.mfaValidationService = mfaValidationService; } @@ -30,6 +38,15 @@ public void setMfaValidationService(MfaValidationService mfaValidationService) { protected void doAfterPropertiesSet() { super.doAfterPropertiesSet(); Assert.notNull(this.mfaValidationService, "A MfaValidationService must be set"); + if (!this.customVerificationStepsRegistered) { + this.verificationChecks = List.of(new MfaTotpVerificationCheck(mfaValidationService)); + } else { + Assert.isTrue(this.verificationChecks != null && !this.verificationChecks.isEmpty(), "At least one verification check must be provided"); + if (verificationChecks.stream().noneMatch(check -> check instanceof MfaTotpVerificationCheck)) { + verificationChecks = new ArrayList<>(verificationChecks); // Ensure we are a mutable list. + verificationChecks.add(new MfaTotpVerificationCheck(mfaValidationService)); + } + } } @Override @@ -45,9 +62,18 @@ protected void additionalAuthenticationChecks(UserDetails userDetails, UsernameP if (mfaAuthenticationToken.getVerificationCode() == null || mfaAuthenticationToken.getVerificationCode().equals("")) { throw new InsufficientAuthenticationException(SERVER_MFA_CODE_REQUIRED_ERROR); } - // If invalid code supplied, authentication has failed. - if (!mfaValidationService.verifyMfaCode(((UserDetailsAdapter) userDetails).getUser().getMfaSecretKey(), mfaAuthenticationToken.getVerificationCode())) { - throw new BadCredentialsException(DefaultLoginAuthenticationExceptionHandler.SERVER_LOGIN_FAILED_ERROR); + + boolean verificationSucceeded = false; + + for (MfaVerificationCheck verificationCheck : verificationChecks) { + if (verificationCheck.validate(userDetailsAdapter.getUser(), mfaAuthenticationToken)) { + verificationSucceeded = true; + break; + } + } + + if (!verificationSucceeded) { + throw new IllegalStateException("At least one verification check must either have succeeded or thrown an AuthenticationException. Check the verifications passed to .setVerificationChecks() for any unmatched scenarios."); } // If mfa is mandatory for this user, but not setup, indicate it must be setup first. } else if (userDetailsAdapter.isMfaMandatory()) { diff --git a/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheck.java b/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheck.java new file mode 100644 index 0000000..47d5959 --- /dev/null +++ b/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheck.java @@ -0,0 +1,26 @@ +package nl._42.restsecure.autoconfigure.authentication.mfa; + +import nl._42.restsecure.autoconfigure.authentication.RegisteredUser; +import nl._42.restsecure.autoconfigure.errorhandling.DefaultLoginAuthenticationExceptionHandler; + +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; + +public class MfaTotpVerificationCheck implements MfaVerificationCheck { + + private final MfaValidationService mfaValidationService; + + public MfaTotpVerificationCheck(MfaValidationService mfaValidationService) { + this.mfaValidationService = mfaValidationService; + } + + @Override + public boolean validate(RegisteredUser user, MfaAuthenticationToken authenticationToken) throws AuthenticationException { + // If no pre-authorized code assigned, validate the code supplied against the currently-valid TOTP code. + if (!mfaValidationService.verifyMfaCode(user.getMfaSecretKey(), authenticationToken.getVerificationCode())) { + throw new BadCredentialsException(DefaultLoginAuthenticationExceptionHandler.SERVER_LOGIN_FAILED_ERROR); + } + + return true; + } +} diff --git a/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaVerificationCheck.java b/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaVerificationCheck.java new file mode 100644 index 0000000..8cd70eb --- /dev/null +++ b/src/main/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaVerificationCheck.java @@ -0,0 +1,20 @@ +package nl._42.restsecure.autoconfigure.authentication.mfa; + +import nl._42.restsecure.autoconfigure.authentication.RegisteredUser; + +import org.springframework.security.core.AuthenticationException; + +public interface MfaVerificationCheck { + + /** + * Validates the MFA Authentication credentials for the given RegisteredUser. + * If the credentials are valid, return true. The user will be logged in and no further checks will take place. + * If this check is not applicable for this user, return false. The next check will then be tried. + * If the credentials are not valid (but this check *is* applicable for this user), throw an AuthenticationException. + * @param user User that is trying to log in. + * @param authenticationToken Supplied authentication credentials (username, password, MFA token) + * @return Returns true if this authentication is valid + * @throws AuthenticationException if the supplied credentials are not valid + */ + boolean validate(RegisteredUser user, MfaAuthenticationToken authenticationToken) throws AuthenticationException; +} diff --git a/src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProviderTest.java b/src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProviderTest.java index a5e53cc..1998e5d 100644 --- a/src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProviderTest.java +++ b/src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaAuthenticationProviderTest.java @@ -2,13 +2,18 @@ import static org.junit.jupiter.api.Assertions.*; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; import nl._42.restsecure.autoconfigure.authentication.InMemoryUserDetailService; +import nl._42.restsecure.autoconfigure.authentication.RegisteredUser; import nl._42.restsecure.autoconfigure.authentication.User; import nl._42.restsecure.autoconfigure.authentication.UserDetailsAdapter; import nl._42.restsecure.autoconfigure.authentication.UserWithPassword; +import nl._42.restsecure.autoconfigure.errorhandling.DefaultLoginAuthenticationExceptionHandler; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -18,6 +23,7 @@ import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.NoOpPasswordEncoder; @@ -38,13 +44,14 @@ class additionalAuthenticationChecks { private MockMfaValidationService mockMfaValidationService; @BeforeEach - void setup() { + void setup() throws Exception { inMemoryUserDetailService = new InMemoryUserDetailService(); mockMfaValidationService = new MockMfaValidationService(); provider = new MfaAuthenticationProvider(); provider.setUserDetailsService(inMemoryUserDetailService); provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); provider.setMfaValidationService(mockMfaValidationService); + provider.afterPropertiesSet(); } @Nested @@ -201,6 +208,205 @@ void shouldThrowIfCodeMissing() { } } + @Nested + class otherVerificationChecks { + + @Test + @DisplayName("should throw if null is passed to setVerificationChecks") + void shouldThrow_ForNullChecks() { + MfaAuthenticationProvider provider = new MfaAuthenticationProvider(); + provider.setUserDetailsService(inMemoryUserDetailService); + provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance()); + provider.setMfaValidationService(mockMfaValidationService); + provider.setVerificationChecks(null); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, provider::doAfterPropertiesSet); + assertEquals("At least one verification check must be provided", e.getMessage()); + + provider.setVerificationChecks(new ArrayList<>()); + e = assertThrows(IllegalArgumentException.class, provider::doAfterPropertiesSet); + assertEquals("At least one verification check must be provided", e.getMessage()); + + provider.setVerificationChecks(List.of(new MfaTotpVerificationCheck(mockMfaValidationService))); + assertDoesNotThrow(provider::doAfterPropertiesSet); + } + + @Test + @DisplayName("should only perform first check if it returns true") + void shouldStop_ifCheckReturnsTrue() { + AtomicBoolean firstCheckPerformed = new AtomicBoolean(false); + AtomicBoolean secondCheckPerformed = new AtomicBoolean(false); + + RegisteredUser[] userFromCheck1 = new RegisteredUser[1]; + MfaAuthenticationToken[] authenticationTokenFromCheck1 = new MfaAuthenticationToken[1]; + + MfaVerificationCheck check1 = (user, authenticationToken) -> { + firstCheckPerformed.compareAndSet(false, true); + userFromCheck1[0] = user; + authenticationTokenFromCheck1[0] = authenticationToken; + return true; + }; + + MfaVerificationCheck check2 = (user, authenticationToken) -> { + secondCheckPerformed.compareAndSet(false, true); + return false; + }; + + provider.setVerificationChecks(List.of(check1, check2)); + provider.doAfterPropertiesSet(); + + User user = new UserWithMfa("username", "password", "secret-key", false, "Hoi"); + inMemoryUserDetailService.register(user); + mockMfaValidationService.register("secret-key", "123456"); + + MfaAuthenticationToken token = new MfaAuthenticationToken("username", "password", "654321"); // This key should not be checked since the custom check succeeds. + Authentication authentication = provider.authenticate(token); + + assertTrue(authentication.isAuthenticated()); + + assertTrue(firstCheckPerformed.get()); + assertFalse(secondCheckPerformed.get()); + + assertEquals(user, userFromCheck1[0]); + assertEquals(token, authenticationTokenFromCheck1[0]); + } + + @Test + @DisplayName("should perform other checks and then MfaTotpVerificationCheck if they return false") + void shouldContinue_ifCheckReturnsFalse() { + AtomicBoolean firstCheckPerformed = new AtomicBoolean(false); + AtomicBoolean secondCheckPerformed = new AtomicBoolean(false); + + RegisteredUser[] userFromCheck1 = new RegisteredUser[1]; + MfaAuthenticationToken[] authenticationTokenFromCheck1 = new MfaAuthenticationToken[1]; + + RegisteredUser[] userFromCheck2 = new RegisteredUser[1]; + MfaAuthenticationToken[] authenticationTokenFromCheck2 = new MfaAuthenticationToken[1]; + + MfaVerificationCheck check1 = (user, authenticationToken) -> { + firstCheckPerformed.compareAndSet(false, true); + userFromCheck1[0] = user; + authenticationTokenFromCheck1[0] = authenticationToken; + return false; + }; + + MfaVerificationCheck check2 = (user, authenticationToken) -> { + secondCheckPerformed.compareAndSet(false, true); + userFromCheck2[0] = user; + authenticationTokenFromCheck2[0] = authenticationToken; + return false; + }; + + provider.setVerificationChecks(List.of(check1, check2)); + provider.doAfterPropertiesSet(); + + User user = new UserWithMfa("username", "password", "secret-key", false, "Hoi"); + inMemoryUserDetailService.register(user); + mockMfaValidationService.register("secret-key", "123456"); + + MfaAuthenticationToken token = new MfaAuthenticationToken("username", "password", "123456"); + Authentication authentication = provider.authenticate(token); + + assertTrue(authentication.isAuthenticated()); + + assertTrue(firstCheckPerformed.get()); + assertTrue(secondCheckPerformed.get()); + + assertEquals(user, userFromCheck1[0]); + assertEquals(token, authenticationTokenFromCheck1[0]); + + assertEquals(user, userFromCheck2[0]); + assertEquals(token, authenticationTokenFromCheck2[0]); + + // Ensure the mfa key is actually checked by trying again with a wrong key - this should throw exception + MfaAuthenticationToken wrongKeyToken = new MfaAuthenticationToken("username", "password", "654321"); + AuthenticationException e = assertThrows(AuthenticationException.class, () -> provider.authenticate(wrongKeyToken)); + assertEquals(DefaultLoginAuthenticationExceptionHandler.SERVER_LOGIN_FAILED_ERROR, e.getMessage()); + } + + @Test + @DisplayName("should put the TotpVerificationCheck in the custom position if it is explicitly passed to setVerificationChecks") + void shouldInsertTotpCheckAtGivenPosition() { + AtomicBoolean checkPerformed = new AtomicBoolean(false); + + RegisteredUser[] userFromCheck = new RegisteredUser[1]; + MfaAuthenticationToken[] authenticationTokenFromCheck = new MfaAuthenticationToken[1]; + + MfaVerificationCheck check = (user, authenticationToken) -> { + checkPerformed.compareAndSet(false, true); + userFromCheck[0] = user; + authenticationTokenFromCheck[0] = authenticationToken; + return false; + }; + + // We create a custom instance of MfaTotpVerificationCheck to allow skipping a certain key. + provider.setVerificationChecks(List.of(new CustomTotpVerificationCheck(mockMfaValidationService), check)); + provider.doAfterPropertiesSet(); + + User user = new UserWithMfa("username", "password", "secret-key", false, "Hoi"); + inMemoryUserDetailService.register(user); + mockMfaValidationService.register("secret-key", "123456"); + + MfaAuthenticationToken token = new MfaAuthenticationToken("username", "password", "123456"); + Authentication authentication = provider.authenticate(token); + + assertTrue(authentication.isAuthenticated()); + assertFalse(checkPerformed.get()); // Mfa key was valid, the second check is not needed. + + // Now, authenticate with a skipped key hardcoded in a custom MfaTotpVerificationCheck (unlikely in practice, but test nonetheless). + // Since none of the checks have been applicable to this user, an IllegalStateException will be thrown (since applications must at least perform one check) + MfaAuthenticationToken tokenInvalid = new MfaAuthenticationToken("username", "password", "654321"); + IllegalStateException e = assertThrows(IllegalStateException.class, () -> provider.authenticate(tokenInvalid)); + + assertEquals("At least one verification check must either have succeeded or thrown an AuthenticationException. Check the verifications passed to .setVerificationChecks() for any unmatched scenarios.", e.getMessage()); + assertTrue(checkPerformed.get()); + assertEquals(user, userFromCheck[0]); + assertEquals(tokenInvalid, authenticationTokenFromCheck[0]); + } + + @Test + @DisplayName("should not perform other checks if first check throws") + void shouldStop_ifCheckThrows() { + AtomicBoolean secondCheckPerformed = new AtomicBoolean(false); + + MfaVerificationCheck check1 = (user, authenticationToken) -> { + throw new BadCredentialsException("Unable to sign in this user"); + }; + + MfaVerificationCheck check2 = (user, authenticationToken) -> { + secondCheckPerformed.compareAndSet(false, true); + return false; + }; + + provider.setVerificationChecks(List.of(check1, check2)); + provider.doAfterPropertiesSet(); + + User user = new UserWithMfa("username", "password", "secret-key", false, "Hoi"); + inMemoryUserDetailService.register(user); + mockMfaValidationService.register("secret-key", "123456"); + + MfaAuthenticationToken token = new MfaAuthenticationToken("username", "password", "123456"); + BadCredentialsException e = assertThrows(BadCredentialsException.class, () -> provider.authenticate(token)); + assertEquals("Unable to sign in this user", e.getMessage()); + + assertFalse(secondCheckPerformed.get()); + } + + class CustomTotpVerificationCheck extends MfaTotpVerificationCheck { + + public CustomTotpVerificationCheck(MfaValidationService mfaValidationService) { + super(mfaValidationService); + } + + @Override + public boolean validate(RegisteredUser user, MfaAuthenticationToken authenticationToken) throws AuthenticationException { + if (authenticationToken.getVerificationCode() != null && authenticationToken.getVerificationCode().equals("654321")) { + return false; + } + return super.validate(user, authenticationToken); + } + } + } + @Nested class otherUserTypes { diff --git a/src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheckTest.java b/src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheckTest.java new file mode 100644 index 0000000..934017b --- /dev/null +++ b/src/test/java/nl/_42/restsecure/autoconfigure/authentication/mfa/MfaTotpVerificationCheckTest.java @@ -0,0 +1,35 @@ +package nl._42.restsecure.autoconfigure.authentication.mfa; + +import static org.junit.jupiter.api.Assertions.*; + +import nl._42.restsecure.autoconfigure.authentication.UserDetailsAdapter; +import nl._42.restsecure.autoconfigure.errorhandling.DefaultLoginAuthenticationExceptionHandler; + +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.BadCredentialsException; + +class MfaTotpVerificationCheckTest { + + @Test + void validate() { + MfaValidationService mfaValidationService = (secret, code) -> secret != null && secret.equals("secret-key") && code != null && code.equals("123456"); + + MfaTotpVerificationCheck check = new MfaTotpVerificationCheck(mfaValidationService); + + UserWithMfa userValid = new UserWithMfa("user", "pw", "secret-key", true, "testRole"); + UserWithMfa userInvalid = new UserWithMfa("user", "pw", "stolen-key", true, "testRole"); + + MfaAuthenticationToken tokenValid = new MfaAuthenticationToken(new UserDetailsAdapter<>(userValid), "*****", "123456"); + MfaAuthenticationToken tokenInvalidCode = new MfaAuthenticationToken(new UserDetailsAdapter<>(userValid), "*****", "654321"); + + assertTrue(check.validate(userValid, tokenValid)); + + BadCredentialsException e1 = assertThrows(BadCredentialsException.class, () -> check.validate(userValid, tokenInvalidCode)); + BadCredentialsException e2 = assertThrows(BadCredentialsException.class, () -> check.validate(userInvalid, tokenValid)); + BadCredentialsException e3 = assertThrows(BadCredentialsException.class, () -> check.validate(userInvalid, tokenInvalidCode)); + + assertEquals(DefaultLoginAuthenticationExceptionHandler.SERVER_LOGIN_FAILED_ERROR, e1.getMessage()); + assertEquals(DefaultLoginAuthenticationExceptionHandler.SERVER_LOGIN_FAILED_ERROR, e2.getMessage()); + assertEquals(DefaultLoginAuthenticationExceptionHandler.SERVER_LOGIN_FAILED_ERROR, e3.getMessage()); + } +} diff --git a/src/test/java/nl/_42/restsecure/autoconfigure/test/ActiveUserAndMfaConfig.java b/src/test/java/nl/_42/restsecure/autoconfigure/test/ActiveUserAndMfaConfig.java index 0b6e039..049668b 100644 --- a/src/test/java/nl/_42/restsecure/autoconfigure/test/ActiveUserAndMfaConfig.java +++ b/src/test/java/nl/_42/restsecure/autoconfigure/test/ActiveUserAndMfaConfig.java @@ -9,7 +9,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration