diff --git a/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java new file mode 100644 index 0000000000..d32b1f19f5 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authentication/AbstractUserDetailsReactiveAuthenticationManager.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2019 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.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.SpringSecurityMessageSource; +import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsChecker; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.Assert; + +/** + * A base {@link ReactiveAuthenticationManager} that allows subclasses to override and work with + * {@link UserDetails} objects. + * + *

+ * Upon successful validation, a UsernamePasswordAuthenticationToken will be + * created and returned to the caller. The token will include as its principal either a + * String representation of the username, or the {@link UserDetails} that was + * returned from the authentication repository. + * + * @author Eddú Meléndez + * @since 5.2 + */ +public abstract class AbstractUserDetailsReactiveAuthenticationManager implements ReactiveAuthenticationManager { + + protected final Log logger = LogFactory.getLog(getClass()); + + protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); + + private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + + private ReactiveUserDetailsPasswordService userDetailsPasswordService; + + private Scheduler scheduler = Schedulers.parallel(); + + private UserDetailsChecker preAuthenticationChecks = user -> { + if (!user.isAccountNonLocked()) { + logger.debug("User account is locked"); + + throw new LockedException(this.messages.getMessage( + "AbstractUserDetailsAuthenticationProvider.locked", + "User account is locked")); + } + + if (!user.isEnabled()) { + logger.debug("User account is disabled"); + + throw new DisabledException(this.messages.getMessage( + "AbstractUserDetailsAuthenticationProvider.disabled", + "User is disabled")); + } + + if (!user.isAccountNonExpired()) { + logger.debug("User account is expired"); + + throw new AccountExpiredException(this.messages.getMessage( + "AbstractUserDetailsAuthenticationProvider.expired", + "User account has expired")); + } + }; + + private UserDetailsChecker postAuthenticationChecks = user -> { + if (!user.isCredentialsNonExpired()) { + logger.debug("User account credentials have expired"); + + throw new CredentialsExpiredException(this.messages.getMessage( + "AbstractUserDetailsAuthenticationProvider.credentialsExpired", + "User credentials have expired")); + } + }; + + @Override + public Mono authenticate(Authentication authentication) { + final String username = authentication.getName(); + final String presentedPassword = (String) authentication.getCredentials(); + return retrieveUser(username) + .doOnNext(this.preAuthenticationChecks::check) + .publishOn(this.scheduler) + .filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword())) + .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) + .flatMap(u -> { + boolean upgradeEncoding = this.userDetailsPasswordService != null + && this.passwordEncoder.upgradeEncoding(u.getPassword()); + if (upgradeEncoding) { + String newPassword = this.passwordEncoder.encode(presentedPassword); + return this.userDetailsPasswordService.updatePassword(u, newPassword); + } + return Mono.just(u); + }) + .doOnNext(this.postAuthenticationChecks::check) + .map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); + } + + /** + * The {@link PasswordEncoder} that is used for validating the password. The default is + * {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()} + * @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null + */ + public void setPasswordEncoder(PasswordEncoder passwordEncoder) { + Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); + this.passwordEncoder = passwordEncoder; + } + + /** + * Sets the {@link Scheduler} used by the {@link UserDetailsRepositoryReactiveAuthenticationManager}. + * The default is {@code Schedulers.parallel()} because modern password encoding is + * a CPU intensive task that is non blocking. This means validation is bounded by the + * number of CPUs. Some applications may want to customize the {@link Scheduler}. For + * example, if users are stuck using the insecure {@link org.springframework.security.crypto.password.NoOpPasswordEncoder} + * they might want to leverage {@code Schedulers.immediate()}. + * + * @param scheduler the {@link Scheduler} to use. Cannot be null. + * @since 5.0.6 + */ + public void setScheduler(Scheduler scheduler) { + Assert.notNull(scheduler, "scheduler cannot be null"); + this.scheduler = scheduler; + } + + /** + * Sets the service to use for upgrading passwords on successful authentication. + * @param userDetailsPasswordService the service to use + */ + public void setUserDetailsPasswordService( + ReactiveUserDetailsPasswordService userDetailsPasswordService) { + this.userDetailsPasswordService = userDetailsPasswordService; + } + + /** + * Sets the strategy which will be used to validate the loaded UserDetails + * object after authentication occurs. + * + * @param postAuthenticationChecks The {@link UserDetailsChecker} + * @since 5.2 + */ + public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) { + Assert.notNull(this.postAuthenticationChecks, "postAuthenticationChecks cannot be null"); + this.postAuthenticationChecks = postAuthenticationChecks; + } + + /** + * Allows subclasses to retrieve the UserDetails + * from an implementation-specific location. + * + * @param username The username to retrieve + * @return the user information. If authentication fails, a Mono error is returned. + */ + protected abstract Mono retrieveUser(String username); + +} diff --git a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java index d593e202c0..5d273beb4b 100644 --- a/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java +++ b/core/src/main/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -17,15 +17,9 @@ package org.springframework.security.authentication; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; -import org.springframework.security.core.userdetails.UserDetailsChecker; -import org.springframework.security.crypto.factory.PasswordEncoderFactories; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.util.Assert; /** @@ -33,18 +27,12 @@ import org.springframework.util.Assert; * username and password. * * @author Rob Winch + * @author Eddú Meléndez * @since 5.0 */ -public class UserDetailsRepositoryReactiveAuthenticationManager implements ReactiveAuthenticationManager { - private final ReactiveUserDetailsService userDetailsService; +public class UserDetailsRepositoryReactiveAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager { - private PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); - - private ReactiveUserDetailsPasswordService userDetailsPasswordService; - - private Scheduler scheduler = Schedulers.parallel(); - - private UserDetailsChecker postAuthenticationChecks = userDetails -> {}; + private ReactiveUserDetailsService userDetailsService; public UserDetailsRepositoryReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) { Assert.notNull(userDetailsService, "userDetailsService cannot be null"); @@ -52,70 +40,8 @@ public class UserDetailsRepositoryReactiveAuthenticationManager implements React } @Override - public Mono authenticate(Authentication authentication) { - final String username = authentication.getName(); - final String presentedPassword = (String) authentication.getCredentials(); - return this.userDetailsService.findByUsername(username) - .publishOn(this.scheduler) - .filter(u -> this.passwordEncoder.matches(presentedPassword, u.getPassword())) - .switchIfEmpty(Mono.defer(() -> Mono.error(new BadCredentialsException("Invalid Credentials")))) - .flatMap(u -> { - boolean upgradeEncoding = this.userDetailsPasswordService != null - && this.passwordEncoder.upgradeEncoding(u.getPassword()); - if (upgradeEncoding) { - String newPassword = this.passwordEncoder.encode(presentedPassword); - return this.userDetailsPasswordService.updatePassword(u, newPassword); - } - return Mono.just(u); - }) - .doOnNext(this.postAuthenticationChecks::check) - .map(u -> new UsernamePasswordAuthenticationToken(u, u.getPassword(), u.getAuthorities()) ); + protected Mono retrieveUser(String username) { + return this.userDetailsService.findByUsername(username); } - /** - * The {@link PasswordEncoder} that is used for validating the password. The default is - * {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()} - * @param passwordEncoder the {@link PasswordEncoder} to use. Cannot be null - */ - public void setPasswordEncoder(PasswordEncoder passwordEncoder) { - Assert.notNull(passwordEncoder, "passwordEncoder cannot be null"); - this.passwordEncoder = passwordEncoder; - } - - /** - * Sets the {@link Scheduler} used by the {@link UserDetailsRepositoryReactiveAuthenticationManager}. - * The default is {@code Schedulers.parallel()} because modern password encoding is - * a CPU intensive task that is non blocking. This means validation is bounded by the - * number of CPUs. Some applications may want to customize the {@link Scheduler}. For - * example, if users are stuck using the insecure {@link org.springframework.security.crypto.password.NoOpPasswordEncoder} - * they might want to leverage {@code Schedulers.immediate()}. - * - * @param scheduler the {@link Scheduler} to use. Cannot be null. - * @since 5.0.6 - */ - public void setScheduler(Scheduler scheduler) { - Assert.notNull(scheduler, "scheduler cannot be null"); - this.scheduler = scheduler; - } - - /** - * Sets the service to use for upgrading passwords on successful authentication. - * @param userDetailsPasswordService the service to use - */ - public void setUserDetailsPasswordService( - ReactiveUserDetailsPasswordService userDetailsPasswordService) { - this.userDetailsPasswordService = userDetailsPasswordService; - } - - /** - * Sets the strategy which will be used to validate the loaded UserDetails - * object after authentication occurs. - * - * @param postAuthenticationChecks The {@link UserDetailsChecker} - * @since 5.2 - */ - public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) { - Assert.notNull(this.postAuthenticationChecks, "postAuthenticationChecks cannot be null"); - this.postAuthenticationChecks = postAuthenticationChecks; - } } diff --git a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java index 69cb6f9d28..793edd5db3 100644 --- a/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authentication/UserDetailsRepositoryReactiveAuthenticationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -39,6 +39,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; /** * @author Rob Winch + * @author Eddú Meléndez * @since 5.1 */ @RunWith(MockitoJUnitRunner.class) @@ -171,4 +172,56 @@ public class UserDetailsRepositoryReactiveAuthenticationManagerTests { verifyZeroInteractions(this.postAuthenticationChecks); } + + @Test(expected = AccountExpiredException.class) + public void authenticateWhenAccountExpiredThenException() { + this.manager.setPasswordEncoder(this.encoder); + + UserDetails expiredUser = User.withUsername("user") + .password("password") + .roles("USER") + .accountExpired(true) + .build(); + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(expiredUser)); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + expiredUser, expiredUser.getPassword()); + + this.manager.authenticate(token).block(); + } + + @Test(expected = LockedException.class) + public void authenticateWhenAccountLockedThenException() { + this.manager.setPasswordEncoder(this.encoder); + + UserDetails lockedUser = User.withUsername("user") + .password("password") + .roles("USER") + .accountLocked(true) + .build(); + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(lockedUser)); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + lockedUser, lockedUser.getPassword()); + + this.manager.authenticate(token).block(); + } + + @Test(expected = DisabledException.class) + public void authenticateWhenAccountDisabledThenException() { + this.manager.setPasswordEncoder(this.encoder); + + UserDetails disabledUser = User.withUsername("user") + .password("password") + .roles("USER") + .disabled(true) + .build(); + when(this.userDetailsService.findByUsername(any())).thenReturn(Mono.just(disabledUser)); + + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( + disabledUser, disabledUser.getPassword()); + + this.manager.authenticate(token).block(); + } + }