Skip to content

Allow JWT issuer AuthenticationManagerResolvers to accept predicate issuer validator #13428

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@

package org.springframework.security.oauth2.server.resource.authentication;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;

Expand Down Expand Up @@ -47,10 +46,10 @@
* "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> in
* a signed JWT (JWS).
*
* To use, this class must be able to determine whether or not the `iss` claim is trusted.
* Recall that anyone can stand up an authorization server and issue valid tokens to a
* resource server. The simplest way to achieve this is to supply a list of trusted
* issuers in the constructor.
* To use, this class must be able to determine whether the `iss` claim is trusted. Recall
* that anyone can stand up an authorization server and issue valid tokens to a resource
* server. The simplest way to achieve this is to supply a set of trusted issuers in the
* constructor.
*
* This class derives the Issuer from the `iss` claim found in the
* {@link HttpServletRequest}'s
Expand All @@ -67,31 +66,67 @@ public final class JwtIssuerAuthenticationManagerResolver implements Authenticat
/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
* parameters
* @param trustedIssuers a list of trusted issuers
* @param trustedIssuers an array of trusted issuers
* @deprecated use {@link #fromTrustedIssuers(String...)}
*/
@Deprecated(since = "6.2", forRemoval = true)
public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) {
this(Arrays.asList(trustedIssuers));
this(Set.of(trustedIssuers));
}

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
* parameters
* @param trustedIssuers a list of trusted issuers
* @param trustedIssuers a collection of trusted issuers
* @deprecated use {@link #fromTrustedIssuers(Collection)}
*/
@Deprecated(since = "6.2", forRemoval = true)
public JwtIssuerAuthenticationManagerResolver(Collection<String> trustedIssuers) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
this.authenticationManager = new ResolvingAuthenticationManager(
new TrustedIssuerJwtAuthenticationManagerResolver(
Collections.unmodifiableCollection(trustedIssuers)::contains));
new TrustedIssuerJwtAuthenticationManagerResolver(Set.copyOf(trustedIssuers)::contains));
}

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
* parameters
* @param trustedIssuers an array of trusted issuers
* @since 6.2
*/
public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(String... trustedIssuers) {
return fromTrustedIssuers(Set.of(trustedIssuers));
}

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
* parameters
* @param trustedIssuers a collection of trusted issuers
* @since 6.2
*/
public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(Collection<String> trustedIssuers) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
return fromTrustedIssuers(Set.copyOf(trustedIssuers)::contains);
}

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
* parameters
* @param trustedIssuers a predicate to validate issuers
* @since 6.2
*/
public static JwtIssuerAuthenticationManagerResolver fromTrustedIssuers(Predicate<String> trustedIssuers) {
Assert.notNull(trustedIssuers, "trustedIssuers cannot be null");
return new JwtIssuerAuthenticationManagerResolver(
new TrustedIssuerJwtAuthenticationManagerResolver(trustedIssuers));
}

/**
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided
* parameters
*
* Note that the {@link AuthenticationManagerResolver} provided in this constructor
* will need to verify that the issuer is trusted. This should be done via an
* allowlist.
* will need to verify that the issuer is trusted. This should be done via an allowed
* set of issuers.
*
* One way to achieve this is with a {@link Map} where the keys are the known issuers:
* <pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@
package org.springframework.security.oauth2.server.resource.authentication;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;

import com.nimbusds.jwt.JWTParser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import org.springframework.core.convert.converter.Converter;
import org.springframework.core.log.LogMessage;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
Expand All @@ -46,10 +48,10 @@
* "https://openid.net/specs/openid-connect-core-1_0.html#IssuerIdentifier">Issuer</a> in
* a signed JWT (JWS).
*
* To use, this class must be able to determine whether or not the `iss` claim is trusted.
* Recall that anyone can stand up an authorization server and issue valid tokens to a
* resource server. The simplest way to achieve this is to supply a list of trusted
* issuers in the constructor.
* To use, this class must be able to determine whether the `iss` claim is trusted. Recall
* that anyone can stand up an authorization server and issue valid tokens to a resource
* server. The simplest way to achieve this is to supply a set of trusted issuers in the
* constructor.
*
* This class derives the Issuer from the `iss` claim found in the
* {@link ServerWebExchange}'s
Expand All @@ -68,21 +70,58 @@ public final class JwtIssuerReactiveAuthenticationManagerResolver
/**
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
* provided parameters
* @param trustedIssuers a list of trusted issuers
* @param trustedIssuers an array of trusted issuers
* @deprecated use {@link #fromTrustedIssuers(String...)}
*/
@Deprecated(since = "6.2", forRemoval = true)
public JwtIssuerReactiveAuthenticationManagerResolver(String... trustedIssuers) {
this(Arrays.asList(trustedIssuers));
this(Set.of(trustedIssuers));
}

/**
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
* provided parameters
* @param trustedIssuers a collection of trusted issuers
* @deprecated use {@link #fromTrustedIssuers(Collection)}
*/
@Deprecated(since = "6.2", forRemoval = true)
public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trustedIssuers) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
this.authenticationManager = new ResolvingAuthenticationManager(
new TrustedIssuerJwtAuthenticationManagerResolver(new ArrayList<>(trustedIssuers)::contains));
new TrustedIssuerJwtAuthenticationManagerResolver(Set.copyOf(trustedIssuers)::contains));
}

/**
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
* provided parameters
* @param trustedIssuers an array of trusted issuers
* @since 6.2
*/
public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(String... trustedIssuers) {
return fromTrustedIssuers(Set.of(trustedIssuers));
}

/**
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
* provided parameters
* @param trustedIssuers a collection of trusted issuers
* @since 6.2
*/
public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(Collection<String> trustedIssuers) {
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
return fromTrustedIssuers(Set.copyOf(trustedIssuers)::contains);
}

/**
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the
* provided parameters
* @param trustedIssuers a predicate to validate issuers
* @since 6.2
*/
public static JwtIssuerReactiveAuthenticationManagerResolver fromTrustedIssuers(Predicate<String> trustedIssuers) {
Assert.notNull(trustedIssuers, "trustedIssuers cannot be null");
return new JwtIssuerReactiveAuthenticationManagerResolver(
new TrustedIssuerJwtAuthenticationManagerResolver(trustedIssuers));
}

/**
Expand All @@ -91,7 +130,7 @@ public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trusted
*
* Note that the {@link ReactiveAuthenticationManagerResolver} provided in this
* constructor will need to verify that the issuer is trusted. This should be done via
* an allowed list of issuers.
* an allowed set of issuers.
*
* One way to achieve this is with a {@link Map} where the keys are the known issuers:
* <pre>
Expand Down Expand Up @@ -169,6 +208,8 @@ public Mono<String> convert(@NonNull BearerTokenAuthenticationToken token) {
static class TrustedIssuerJwtAuthenticationManagerResolver
implements ReactiveAuthenticationManagerResolver<String> {

private final Log logger = LogFactory.getLog(getClass());

private final Map<String, Mono<ReactiveAuthenticationManager>> authenticationManagers = new ConcurrentHashMap<>();

private final Predicate<String> trustedIssuer;
Expand All @@ -180,11 +221,13 @@ static class TrustedIssuerJwtAuthenticationManagerResolver
@Override
public Mono<ReactiveAuthenticationManager> resolve(String issuer) {
if (!this.trustedIssuer.test(issuer)) {
this.logger.debug("Did not resolve AuthenticationManager since issuer is not trusted");
return Mono.empty();
}
// @formatter:off
return this.authenticationManagers.computeIfAbsent(issuer,
(k) -> Mono.<ReactiveAuthenticationManager>fromCallable(() -> new JwtReactiveAuthenticationManager(ReactiveJwtDecoders.fromIssuerLocation(k)))
.doOnNext((manager) -> this.logger.debug(LogMessage.format("Resolved AuthenticationManager for issuer '%s'", issuer)))
.subscribeOn(Schedulers.boundedElastic())
.cache((manager) -> Duration.ofMillis(Long.MAX_VALUE), (ex) -> Duration.ZERO, () -> Duration.ZERO)
);
Expand Down
Loading