-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
/* | ||
* Copyright 2002-2020 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.oauth2.server.resource.authentication; | ||
|
||
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 com.nimbusds.jwt.JWTParser; | ||
import reactor.core.publisher.Mono; | ||
import reactor.core.scheduler.Schedulers; | ||
|
||
import org.springframework.core.convert.converter.Converter; | ||
import org.springframework.lang.NonNull; | ||
import org.springframework.security.authentication.AuthenticationManager; | ||
import org.springframework.security.authentication.ReactiveAuthenticationManager; | ||
import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver; | ||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; | ||
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; | ||
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; | ||
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; | ||
import org.springframework.security.oauth2.server.resource.web.server.ServerBearerTokenAuthenticationConverter; | ||
import org.springframework.util.Assert; | ||
import org.springframework.web.server.ServerWebExchange; | ||
|
||
/** | ||
* An implementation of {@link ReactiveAuthenticationManagerResolver} that resolves a JWT-based | ||
* {@link ReactiveAuthenticationManager} based on the | ||
* <a href="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 whitelist of trusted issuers in the constructor. | ||
* | ||
* This class derives the Issuer from the `iss` claim found in the {@link ServerWebExchange}'s | ||
* <a href="https://tools.ietf.org/html/rfc6750#section-1.2" target="_blank">Bearer Token</a>. | ||
* | ||
* @author Josh Cummings | ||
* @since 5.3 | ||
*/ | ||
public final class JwtIssuerReactiveAuthenticationManagerResolver | ||
implements ReactiveAuthenticationManagerResolver<ServerWebExchange> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> ??
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, @rmakhlouf, that's correct, thanks for checking. Actually, it's the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more.
Simply copied and slightly amended ServerBearerTokenAuthenticationConverter and JwtIssuerReactiveAuthenticationManagerResolver
|
||
|
||
private final ReactiveAuthenticationManagerResolver<String> issuerAuthenticationManagerResolver; | ||
private final Converter<ServerWebExchange, Mono<String>> issuerConverter = new JwtClaimIssuerConverter(); | ||
|
||
/** | ||
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the provided parameters | ||
* | ||
* @param trustedIssuers a whitelist of trusted issuers | ||
*/ | ||
public JwtIssuerReactiveAuthenticationManagerResolver(String... trustedIssuers) { | ||
this(Arrays.asList(trustedIssuers)); | ||
} | ||
|
||
/** | ||
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the provided parameters | ||
* | ||
* @param trustedIssuers a whitelist of trusted issuers | ||
*/ | ||
public JwtIssuerReactiveAuthenticationManagerResolver(Collection<String> trustedIssuers) { | ||
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty"); | ||
this.issuerAuthenticationManagerResolver = | ||
new TrustedIssuerJwtAuthenticationManagerResolver | ||
(Collections.unmodifiableCollection(trustedIssuers)::contains); | ||
} | ||
|
||
/** | ||
* Construct a {@link JwtIssuerReactiveAuthenticationManagerResolver} using the provided parameters | ||
* | ||
* Note that the {@link ReactiveAuthenticationManagerResolver} provided in this constructor will need to | ||
* verify that the issuer is trusted. This should be done via a whitelist. | ||
* | ||
* One way to achieve this is with a {@link Map} where the keys are the known issuers: | ||
* <pre> | ||
* Map<String, ReactiveAuthenticationManager> authenticationManagers = new HashMap<>(); | ||
* authenticationManagers.put("https://issuerOne.example.org", managerOne); | ||
* authenticationManagers.put("https://issuerTwo.example.org", managerTwo); | ||
* JwtIssuerReactiveAuthenticationManagerResolver resolver = new JwtIssuerReactiveAuthenticationManagerResolver | ||
* (issuer -> Mono.justOrEmpty(authenticationManagers.get(issuer)); | ||
* </pre> | ||
* | ||
* The keys in the {@link Map} are the whitelist. | ||
* | ||
* @param issuerAuthenticationManagerResolver a strategy for resolving the {@link ReactiveAuthenticationManager} | ||
* by the issuer | ||
*/ | ||
public JwtIssuerReactiveAuthenticationManagerResolver | ||
(ReactiveAuthenticationManagerResolver<String> issuerAuthenticationManagerResolver) { | ||
|
||
Assert.notNull(issuerAuthenticationManagerResolver, "issuerAuthenticationManagerResolver cannot be null"); | ||
this.issuerAuthenticationManagerResolver = issuerAuthenticationManagerResolver; | ||
} | ||
|
||
/** | ||
* Return an {@link AuthenticationManager} based off of the `iss` claim found in the request's bearer token | ||
* | ||
* @throws OAuth2AuthenticationException if the bearer token is malformed or an {@link ReactiveAuthenticationManager} | ||
* can't be derived from the issuer | ||
*/ | ||
@Override | ||
public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) { | ||
return this.issuerConverter.convert(exchange) | ||
.flatMap(issuer -> | ||
this.issuerAuthenticationManagerResolver.resolve(issuer).switchIfEmpty( | ||
Mono.error(new InvalidBearerTokenException("Invalid issuer " + issuer))) | ||
); | ||
} | ||
|
||
private static class JwtClaimIssuerConverter | ||
implements Converter<ServerWebExchange, Mono<String>> { | ||
|
||
private final ServerBearerTokenAuthenticationConverter converter = | ||
new ServerBearerTokenAuthenticationConverter(); | ||
|
||
@Override | ||
public Mono<String> convert(@NonNull ServerWebExchange 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 InvalidBearerTokenException("Missing issuer"))); | ||
} catch (Exception e) { | ||
return Mono.error(new InvalidBearerTokenException(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()) | ||
); | ||
} | ||
} | ||
} |
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 anAuthenticationManagerResolver
, but was receivingJwtIssuerReactiveAuthenticationManagerResolver
.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 withJwtIssuerAuthenticationManagerResolver
whileReactiveAuthenticationManagerResolver
matches withJwtIssuerReactiveAuthenticationManagerResolver
.If you've got a concrete suggestion for how the docs can be improved to help with this issue, please open a separate ticket.