-
Notifications
You must be signed in to change notification settings - Fork 6k
Add JwtIssuerReactiveAuthenticationManagerResolver #7887
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
Conversation
@davidmelia Let me know if this PR suits your needs. |
* @since 5.3 | ||
*/ | ||
public final class JwtIssuerReactiveAuthenticationManagerResolver | ||
implements ReactiveAuthenticationManagerResolver<ServerWebExchange> { |
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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());
}
}
) | ||
.oauth2ResourceServer(oauth2 -> oauth2 | ||
.authenticationManagerResolver(authenticationManagerResolver) | ||
); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
Fixes gh-7857