فهرست منبع

Prevent authentication when user is inactive for reactive apps

Currently, reactive applications doesn't perform validation when user
is locked, disabled or expired. This commit introduces these validations.

Fixes gh-7113
Eddú Meléndez Gonzales 6 سال پیش
والد
کامیت
8e6e975e86

+ 174 - 0
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.
+ *
+ * <p>
+ * Upon successful validation, a <code>UsernamePasswordAuthenticationToken</code> will be
+ * created and returned to the caller. The token will include as its principal either a
+ * <code>String</code> 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<Authentication> 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 <tt>UserDetails</tt>
+	 * 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 <code>UserDetails</code>
+	 * 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<UserDetails> retrieveUser(String username);
+
+}

+ 7 - 81
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<Authentication> 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()) );
-	}
-
-	/**
-	 * 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;
+	protected Mono<UserDetails> retrieveUser(String username) {
+		return this.userDetailsService.findByUsername(username);
 	}
 
-	/**
-	 * 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 <tt>UserDetails</tt>
-	 * 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;
-	}
 }

+ 54 - 1
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();
+	}
+
 }