Hi,
I'm trying to add oauth resource server multi tenancy support (by issuer) to my existing webflux stack (Boot 2.2, Spring Security 5.2.1) and am really struggling.
I have a couple of requests:
-
Could you add a reactive counterpart to https://github.com/spring-projects/spring-security/pull/7733
-
All the examples in https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2resourceserver-multitenancy are none reactive and I'm really struggling to get reactive to work. I've figured out the below but have had to cut and paste code from ServerBearerTokenAuthenticationConverter which seems wrong:
public class TenantAuthenticationManagerResolver implements ReactiveAuthenticationManagerResolver<ServerHttpRequest> {
private static final Pattern authorizationPattern = Pattern.compile("^Bearer (?<token>[a-zA-Z0-9-._~+/]+)=*$", Pattern.CASE_INSENSITIVE);
private final Map<String, String> tenants;
private final Map<String, JwtReactiveAuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
public TenantAuthenticationManagerResolver() {
this.tenants = Map.of("https://url.ii.co.uk/", "https://url.ii.co.uk/");
}
@Override
public Mono<ReactiveAuthenticationManager> resolve(ServerHttpRequest request) {
return Mono.just(this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant));
}
private String toTenant(ServerHttpRequest request) {
try {
String token = token(request);
return JWTParser.parse(token).getJWTClaimsSet().getIssuer();
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
private JwtReactiveAuthenticationManager fromTenant(String tenant) {
return Optional.ofNullable(this.tenants.get(tenant)).map(ReactiveJwtDecoders::fromIssuerLocation).map(JwtReactiveAuthenticationManager::new)
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
}
private String token(ServerHttpRequest request) {
String authorizationHeaderToken = resolveFromAuthorizationHeader(request.getHeaders());
if (authorizationHeaderToken != null) {
return authorizationHeaderToken;
}
return null;
}
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");
}
}
Ideally I would like to implement a reactive version of https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#parsing-the-claim-only-once but cannot figure out NimbusReactiveJwtDecoder. Could you give me any pointers?
Thanks
Comment From: jzheaux
Hi, @davidmelia, thanks for reaching out about this - I think a reactive version of JwtIssuerAuthenticationManagerResolver is something that makes sense to add. I agree that you shouldn't have to copy from ServerBearerTokenAuthenticationConverter. I wonder if something like this would work:
public Mono<AuthenticationManager> resolve(ServerWebExchange exchange) {
BearerTokenAuthenticationToken bearerToken = (BearerTokenAuthenticationToken)
this.authenticationConverter.convert(exchange);
String token = bearerToken.getToken();
// extract issuer claim
return this.issuerAuthenticationManagerResolver.resolve(exchange);
}
Would you be interested in working with me on a PR for JwtIssuerReactiveAuthenticationManagerResolver?
As for your question about parsing only once - I'd first recommend assessing how much of your request is actually being spent on parsing the JWT. Because Nimbus doesn't yet have a reactive API, the amount of time and complexity involved in ironing that out is most likely an early optimization.
Comment From: davidmelia
@jzheaux I could work on the PR with you.
I think one big issue here to your suggestion is that AuthenticationWebFilter requires the following
private final ReactiveAuthenticationManagerResolver<ServerHttpRequest> authenticationManagerResolver;
and therefore we cannot get hold of the ServerWebExchange unless I write a new AuthenticationWebFilter which I don't really want to do.
Comment From: jzheaux
Good point, @davidmelia. I believe https://github.com/spring-projects/spring-security/issues/7872 will address that.
If you agree that such would address your concern, maybe we start with that ticket?
Thinking ahead just a bit, it'd be nice to address these tickets before RC1 next week. Do you think you've got time to submit a PR by Friday? If not, no worries, and I can submit the PR and have you review it.
Comment From: davidmelia
@jzheaux I won't have time to submit the PR but I would definitely help review your PR by Friday.