AllRequiredFactorsAuthorizationManager.java

/*
 * 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.authorization;

import java.time.Clock;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.jspecify.annotations.Nullable;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.util.Assert;

/**
 * An {@link AuthorizationManager} that determines if the current user is authorized by
 * evaluating if the {@link Authentication} contains a {@link FactorGrantedAuthority} that
 * is not expired for each {@link RequiredFactor}.
 *
 * @author Rob Winch
 * @since 7.0
 * @see AuthorityAuthorizationManager
 */
public final class AllRequiredFactorsAuthorizationManager<T> implements AuthorizationManager<T> {

	private Clock clock = Clock.systemUTC();

	private final List<RequiredFactor> requiredFactors;

	/**
	 * Creates a new instance.
	 * @param requiredFactors the authorities that are required.
	 */
	private AllRequiredFactorsAuthorizationManager(List<RequiredFactor> requiredFactors) {
		Assert.notEmpty(requiredFactors, "requiredFactors cannot be empty");
		Assert.noNullElements(requiredFactors, "requiredFactors must not contain null elements");
		this.requiredFactors = Collections.unmodifiableList(requiredFactors);
	}

	/**
	 * Sets the {@link Clock} to use.
	 * @param clock the {@link Clock} to use. Cannot be null.
	 */
	public void setClock(Clock clock) {
		Assert.notNull(clock, "clock cannot be null");
		this.clock = clock;
	}

	/**
	 * For each {@link RequiredFactor} finds the first
	 * {@link FactorGrantedAuthority#getAuthority()} that matches the
	 * {@link RequiredFactor#getAuthority()}. The
	 * {@link FactorGrantedAuthority#getIssuedAt()} must be more recent than
	 * {@link RequiredFactor#getValidDuration()} (if non-null).
	 * @param authentication the {@link Supplier} of the {@link Authentication} to check
	 * @param object the object to check authorization on (not used).
	 * @return an {@link FactorAuthorizationDecision}
	 */
	@Override
	public FactorAuthorizationDecision authorize(Supplier<? extends @Nullable Authentication> authentication,
			T object) {
		List<GrantedAuthority> currentFactorAuthorities = getFactorGrantedAuthorities(authentication.get());
		List<RequiredFactorError> factorErrors = this.requiredFactors.stream()
			.map((factor) -> requiredFactorError(factor, currentFactorAuthorities))
			.filter(Objects::nonNull)
			.toList();
		return new FactorAuthorizationDecision(factorErrors);
	}

	/**
	 * Given the {@link RequiredFactor} and the current {@link FactorGrantedAuthority}
	 * instances, returns {@link RequiredFactor} or null if granted.
	 * @param requiredFactor the {@link RequiredFactor} to check.
	 * @param currentFactors the current user's {@link FactorGrantedAuthority}.
	 * @return the {@link RequiredFactor} or null if granted.
	 */
	private @Nullable RequiredFactorError requiredFactorError(RequiredFactor requiredFactor,
			List<GrantedAuthority> currentFactors) {
		Optional<GrantedAuthority> matchingAuthority = currentFactors.stream()
			.filter((authority) -> Objects.equals(authority.getAuthority(), requiredFactor.getAuthority()))
			.findFirst();
		if (!matchingAuthority.isPresent()) {
			return RequiredFactorError.createMissing(requiredFactor);
		}
		return matchingAuthority.map((authority) -> {
			if (requiredFactor.getValidDuration() == null) {
				// granted (only requires authority to match)
				return null;
			}
			else if (authority instanceof FactorGrantedAuthority factorAuthority) {
				Instant now = this.clock.instant();
				Instant expiresAt = factorAuthority.getIssuedAt().plus(requiredFactor.getValidDuration());
				if (now.isBefore(expiresAt)) {
					// granted
					return null;
				}
			}

			// denied (expired or no issuedAt to compare)
			return RequiredFactorError.createExpired(requiredFactor);
		}).orElse(null);
	}

	/**
	 * Extracts all of the {@link FactorGrantedAuthority} instances from
	 * {@link Authentication#getAuthorities()}. If {@link Authentication} is null, or
	 * {@link Authentication#isAuthenticated()} is false, then an empty {@link List} is
	 * returned.
	 * @param authentication the {@link Authentication} (possibly null).
	 * @return all of the {@link FactorGrantedAuthority} instances from
	 * {@link Authentication#getAuthorities()}.
	 */
	private List<GrantedAuthority> getFactorGrantedAuthorities(@Nullable Authentication authentication) {
		if (authentication == null || !authentication.isAuthenticated()) {
			return Collections.emptyList();
		}
		// @formatter:off
		return authentication.getAuthorities().stream()
			.collect(Collectors.toList());
		// @formatter:on
	}

	/**
	 * Creates a new {@link Builder}
	 * @return
	 */
	public static <T> Builder<T> builder() {
		return new Builder<>();
	}

	/**
	 * A builder for {@link AllRequiredFactorsAuthorizationManager}.
	 *
	 * @author Rob Winch
	 * @since 7.0
	 */
	public static final class Builder<T> {

		private List<RequiredFactor> requiredFactors = new ArrayList<>();

		/**
		 * Allows the user to consume the {@link RequiredFactor.Builder} that is passed in
		 * and then adds the result to the {@link #requireFactor(RequiredFactor)}.
		 * @param requiredFactor the {@link Consumer} to invoke.
		 * @return the builder.
		 */
		public Builder<T> requireFactor(Consumer<RequiredFactor.Builder> requiredFactor) {
			Assert.notNull(requiredFactor, "requiredFactor cannot be null");
			RequiredFactor.Builder builder = RequiredFactor.builder();
			requiredFactor.accept(builder);
			return requireFactor(builder.build());
		}

		/**
		 * The {@link RequiredFactor} to add.
		 * @param requiredFactor the requiredFactor to add. Cannot be null.
		 * @return the builder.
		 */
		public Builder<T> requireFactor(RequiredFactor requiredFactor) {
			Assert.notNull(requiredFactor, "requiredFactor cannot be null");
			this.requiredFactors.add(requiredFactor);
			return this;
		}

		/**
		 * Builds the {@link AllRequiredFactorsAuthorizationManager}.
		 * @return the {@link AllRequiredFactorsAuthorizationManager}
		 */
		public AllRequiredFactorsAuthorizationManager<T> build() {
			Assert.state(!this.requiredFactors.isEmpty(), "requiredFactors cannot be empty");
			return new AllRequiredFactorsAuthorizationManager<T>(this.requiredFactors);
		}

	}

}