Skip to content
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

Add JwtIssuerReactiveAuthenticationManagerResolver #7887

Merged
merged 1 commit into from
Feb 6, 2020

Conversation

jzheaux
Copy link
Contributor

@jzheaux jzheaux commented Feb 1, 2020

Fixes gh-7857

@jzheaux
Copy link
Contributor Author

jzheaux commented Feb 4, 2020

@davidmelia Let me know if this PR suits your needs.

* @since 5.3
*/
public final class JwtIssuerReactiveAuthenticationManagerResolver
implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
Copy link

@ghost ghost Feb 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jzheaux it is normal that JwtIssuerReactiveAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerWebExchange> instead of ReactiveAuthenticationManagerResolver<ServerHttpRequest> ??

/**
		 * Configures the {@link ReactiveAuthenticationManagerResolver}
		 *
		 * @param authenticationManagerResolver the {@link ReactiveAuthenticationManagerResolver}
		 * @return the {@link OAuth2ResourceServerSpec} for additional configuration
		 * @since 5.2
		 */
		public OAuth2ResourceServerSpec authenticationManagerResolver(
				ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver) {
			Assert.notNull(authenticationManagerResolver, "authenticationManagerResolver cannot be null");
			this.authenticationManagerResolver = authenticationManagerResolver;
			return this;
		}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, @rmakhlouf, that's correct, thanks for checking. Actually, it's the AuthenticationWebFilter that is mistaken, which you can see in #7872.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jzheaux thanks for this looks good. I'm using spring boot 5.2 so I temporarily back-ported your class into my project (and created a temporary custom ServerBearerTokenAuthenticationConverter) and all is working great in our test environment where we can now support multi tenants :-)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidmelia It will be awesome if you can share you adaptation to spring boot 5.2 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, @davidmelia! Glad to hear it is working for you.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davidmelia It will be awesome if you can share you adaptation to spring boot 5.2 :)

Simply copied and slightly amended ServerBearerTokenAuthenticationConverter and JwtIssuerReactiveAuthenticationManagerResolver

package .....;

import com.nimbusds.jwt.JWTParser;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

/**
 * Spring Security 5.3 will contain this resolver so please replace then. This class is to support
 * Auth0 multi tenants.
 */
public final class JwtIssuerReactiveAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerHttpRequest> {
  private static final OAuth2Error DEFAULT_INVALID_TOKEN = invalidToken("Invalid token");

  private final ReactiveAuthenticationManagerResolver<String> issuerAuthenticationManagerResolver;
  private final Converter<ServerHttpRequest, Mono<String>> issuerConverter = new JwtClaimIssuerConverter();


  public JwtIssuerReactiveAuthenticationManagerResolver(String... trustedIssuers) {
    this(Arrays.asList(trustedIssuers));
  }

  public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trustedIssuers) {
    Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
    this.issuerAuthenticationManagerResolver = new TrustedIssuerJwtAuthenticationManagerResolver(Collections.unmodifiableCollection(trustedIssuers)::contains);
  }


  public JwtIssuerReactiveAuthenticationManagerResolver(ReactiveAuthenticationManagerResolver<String> issuerAuthenticationManagerResolver) {

    Assert.notNull(issuerAuthenticationManagerResolver, "issuerAuthenticationManagerResolver cannot be null");
    this.issuerAuthenticationManagerResolver = issuerAuthenticationManagerResolver;
  }


  @Override
  public Mono<ReactiveAuthenticationManager> resolve(ServerHttpRequest exchange) {
    return this.issuerConverter.convert(exchange)
        .flatMap(issuer -> this.issuerAuthenticationManagerResolver.resolve(issuer).switchIfEmpty(Mono.error(new OAuth2AuthenticationException(invalidToken("Invalid issuer " + issuer)))));
  }

  private static class JwtClaimIssuerConverter implements Converter<ServerHttpRequest, Mono<String>> {

    private final ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter();

    @Override
    public Mono<String> convert(@NonNull ServerHttpRequest exchange) {
      return this.converter.convert(exchange).cast(BearerTokenAuthenticationToken.class).flatMap(this::issuer);
    }

    private Mono<String> issuer(BearerTokenAuthenticationToken token) {
      try {
        String issuer = JWTParser.parse(token.getToken()).getJWTClaimsSet().getIssuer();
        return Mono.justOrEmpty(issuer).switchIfEmpty(Mono.error(new OAuth2AuthenticationException(invalidToken("Missing issuer"))));
      } catch (Exception e) {
        return Mono.error(new OAuth2AuthenticationException(invalidToken(e.getMessage())));
      }
    }
  }

  private static class TrustedIssuerJwtAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<String> {

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

    TrustedIssuerJwtAuthenticationManagerResolver(Predicate<String> trustedIssuer) {
      this.trustedIssuer = trustedIssuer;
    }

    @Override
    public Mono<ReactiveAuthenticationManager> resolve(String issuer) {
      return Mono.just(issuer).filter(this.trustedIssuer).flatMap(iss -> this.authenticationManagers.computeIfAbsent(iss,
          k -> Mono.fromCallable(() -> ReactiveJwtDecoders.fromIssuerLocation(iss)).subscribeOn(Schedulers.boundedElastic()).map(JwtReactiveAuthenticationManager::new).cache()));
    }
  }

  private static OAuth2Error invalidToken(String message) {
    try {
      return new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, message, "https://tools.ietf.org/html/rfc6750#section-3.1");
    } catch (IllegalArgumentException malformed) {
      // some third-party library error messages are not suitable for RFC 6750's error message charset
      return DEFAULT_INVALID_TOKEN;
    }
  }
}
package ...;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.BearerTokenError;
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;

/**
 * Cut and paste of springs ServerBearerTokenAuthenticationConverter. Not needed when Spring 5.3
 * comes in.
 */
class ServerBearerTokenAuthenticationConverter {
  private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+)=*$", Pattern.CASE_INSENSITIVE);

  private boolean allowUriQueryParameter = false;

  public Mono<Authentication> convert(ServerHttpRequest exchange) {
    return Mono.justOrEmpty(token(exchange)).map(token -> {
      if (token.isEmpty()) {
        BearerTokenError error = invalidTokenError();
        throw new OAuth2AuthenticationException(error);
      }
      return new BearerTokenAuthenticationToken(token);
    });
  }

  private String token(ServerHttpRequest request) {
    String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders());
    String parameterToken = request.getQueryParams().getFirst("access_token");
    if (authorizationHeaderToken != null) {
      if (parameterToken != null) {
        BearerTokenError error =
            new BearerTokenError(BearerTokenErrorCodes.INVALID_REQUEST, HttpStatus.BAD_REQUEST, "Found multiple bearer tokens in the request", "https://tools.ietf.org/html/rfc6750#section-3.1");
        throw new OAuth2AuthenticationException(error);
      }
      return authorizationHeaderToken;
    } else if (parameterToken != null && isParameterTokenSupportedForRequest(request)) {
      return parameterToken;
    }
    return null;
  }

  public void setAllowUriQueryParameter(boolean allowUriQueryParameter) {
    this.allowUriQueryParameter = allowUriQueryParameter;
  }

  private static String resolveFromAuthorizationHeader(HttpHeaders headers) {
    String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION);
    if (StringUtils.startsWithIgnoreCase(authorization, "bearer")) {
      Matcher matcher = authorizationPattern.matcher(authorization);

      if (!matcher.matches()) {
        BearerTokenError error = invalidTokenError();
        throw new OAuth2AuthenticationException(error);
      }

      return matcher.group("token");
    }
    return null;
  }

  private static BearerTokenError invalidTokenError() {
    return new BearerTokenError(BearerTokenErrorCodes.INVALID_TOKEN, HttpStatus.UNAUTHORIZED, "Bearer token is malformed", "https://tools.ietf.org/html/rfc6750#section-3.1");
  }

  private boolean isParameterTokenSupportedForRequest(ServerHttpRequest request) {
    return this.allowUriQueryParameter && HttpMethod.GET.equals(request.getMethod());
  }
}

@jzheaux jzheaux merged commit a90e579 into spring-projects:master Feb 6, 2020
@jzheaux jzheaux deleted the gh-7857 branch February 6, 2020 20:45
@jzheaux jzheaux added in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement labels Mar 4, 2020
@jzheaux jzheaux added this to the 5.3.0 milestone Mar 4, 2020
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(authenticationManagerResolver)
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, would you mind update this document/example with a bit more details please?

I followed the guide, but the method authenticationManagerResolver() in OAuth2ResourceServerConfigurer complained that it's expecting an AuthenticationManagerResolver, but was receiving JwtIssuerReactiveAuthenticationManagerResolver.

I tried to have my resource server may accept bearer tokens from two different authorization servers, using Spring Boot 2.3.1.RELEASE and spring-security-oauth2-resource-server 5.3.3.RELEASE

Sorry if I have missed something from the guide. Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to hear you are having trouble, @cmhuynh.

I believe the issue you've described is because you are trying to use a WebFlux class with the Web DSL. AuthenticationManagerResolver matches with JwtIssuerAuthenticationManagerResolver while ReactiveAuthenticationManagerResolver matches with JwtIssuerReactiveAuthenticationManagerResolver.

If you've got a concrete suggestion for how the docs can be improved to help with this issue, please open a separate ticket.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: oauth2 An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose) type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add ReactiveJwtIssuerAuthenticationManagerResolver and Reactive Multi Tentant Examples
3 participants