mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-05-30 00:32:14 +00:00
Add JwtIssuerAuthenticationManagerResolver
Fixes gh-7724
This commit is contained in:
parent
09810b8df9
commit
de87675f6d
@ -28,10 +28,19 @@ import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSObject;
|
||||
import com.nimbusds.jose.Payload;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import net.minidev.json.JSONObject;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.hamcrest.core.AllOf;
|
||||
@ -82,14 +91,15 @@ import org.springframework.security.oauth2.core.DefaultOAuth2AuthenticatedPrinci
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtException;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.NimbusOpaqueTokenIntrospector;
|
||||
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
|
||||
@ -127,6 +137,8 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
import static org.springframework.security.oauth2.core.TestOAuth2AccessTokens.noScopes;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
|
||||
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
|
||||
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withPublicKey;
|
||||
import static org.springframework.security.oauth2.jwt.TestJwts.jwt;
|
||||
@ -149,7 +161,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
||||
public class OAuth2ResourceServerConfigurerTests {
|
||||
private static final String JWT_TOKEN = "token";
|
||||
private static final String JWT_SUBJECT = "mock-test-subject";
|
||||
private static final Map<String, Object> JWT_CLAIMS = Collections.singletonMap(JwtClaimNames.SUB, JWT_SUBJECT);
|
||||
private static final Map<String, Object> JWT_CLAIMS = Collections.singletonMap(SUB, JWT_SUBJECT);
|
||||
private static final Jwt JWT = jwt().build();
|
||||
private static final String JWK_SET_URI = "https://mock.org";
|
||||
private static final JwtAuthenticationToken JWT_AUTHENTICATION_TOKEN =
|
||||
@ -1332,6 +1344,50 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
verify(http, never()).authenticationProvider(any(AuthenticationProvider.class));
|
||||
}
|
||||
|
||||
// -- authentication manager resolver
|
||||
|
||||
@Test
|
||||
public void getWhenMultipleIssuersThenUsesIssuerClaimToDifferentiate() throws Exception {
|
||||
this.spring.register(WebServerConfig.class, MultipleIssuersConfig.class, BasicController.class).autowire();
|
||||
|
||||
MockWebServer server = this.spring.getContext().getBean(MockWebServer.class);
|
||||
String metadata = "{\n"
|
||||
+ " \"issuer\": \"%s\", \n"
|
||||
+ " \"jwks_uri\": \"%s/.well-known/jwks.json\" \n"
|
||||
+ "}";
|
||||
String jwkSet = jwkSet();
|
||||
String issuerOne = server.url("/issuerOne").toString();
|
||||
String issuerTwo = server.url("/issuerTwo").toString();
|
||||
String issuerThree = server.url("/issuerThree").toString();
|
||||
String jwtOne = jwtFromIssuer(issuerOne);
|
||||
String jwtTwo = jwtFromIssuer(issuerTwo);
|
||||
String jwtThree = jwtFromIssuer(issuerThree);
|
||||
|
||||
mockWebServer(String.format(metadata, issuerOne, issuerOne));
|
||||
mockWebServer(jwkSet);
|
||||
|
||||
this.mvc.perform(get("/authenticated")
|
||||
.with(bearerToken(jwtOne)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("test-subject"));
|
||||
|
||||
mockWebServer(String.format(metadata, issuerTwo, issuerTwo));
|
||||
mockWebServer(jwkSet);
|
||||
|
||||
this.mvc.perform(get("/authenticated")
|
||||
.with(bearerToken(jwtTwo)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("test-subject"));
|
||||
|
||||
mockWebServer(String.format(metadata, issuerThree, issuerThree));
|
||||
mockWebServer(jwkSet);
|
||||
|
||||
this.mvc.perform(get("/authenticated")
|
||||
.with(bearerToken(jwtThree)))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(invalidTokenHeader("Invalid issuer"));
|
||||
}
|
||||
|
||||
// -- Incorrect Configuration
|
||||
|
||||
@Test
|
||||
@ -2070,6 +2126,26 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class MultipleIssuersConfig extends WebSecurityConfigurerAdapter {
|
||||
@Autowired
|
||||
MockWebServer web;
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
String issuerOne = this.web.url("/issuerOne").toString();
|
||||
String issuerTwo = this.web.url("/issuerTwo").toString();
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver(issuerOne, issuerTwo);
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.oauth2ResourceServer()
|
||||
.authenticationManagerResolver(authenticationManagerResolver);
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class AuthenticationManagerResolverPlusOtherConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
@ -2257,6 +2333,23 @@ public class OAuth2ResourceServerConfigurerTests {
|
||||
", error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\"");
|
||||
}
|
||||
|
||||
private String jwkSet() {
|
||||
return new JWKSet(new RSAKey.Builder(TestKeys.DEFAULT_PUBLIC_KEY)
|
||||
.keyID("1").build()).toString();
|
||||
}
|
||||
|
||||
private String jwtFromIssuer(String issuer) throws Exception {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put(ISS, issuer);
|
||||
claims.put(SUB, "test-subject");
|
||||
claims.put("scope", "message:read");
|
||||
JWSObject jws = new JWSObject(
|
||||
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID("1").build(),
|
||||
new Payload(new JSONObject(claims)));
|
||||
jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
|
||||
return jws.serialize();
|
||||
}
|
||||
|
||||
private void mockWebServer(String response) {
|
||||
this.web.enqueue(new MockResponse()
|
||||
.setResponseCode(200)
|
||||
|
@ -1243,109 +1243,15 @@ In each case, there are two things that need to be done and trade-offs associate
|
||||
1. Resolve the tenant
|
||||
2. Propagate the tenant
|
||||
|
||||
==== Resolving the Tenant By Request Material
|
||||
|
||||
Resolving the tenant by request material can be done my implementing an `AuthenticationManagerResolver`, which determines the `AuthenticationManager` at runtime, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Component
|
||||
public class TenantAuthenticationManagerResolver
|
||||
implements AuthenticationManagerResolver<HttpServletRequest> {
|
||||
private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
|
||||
private final TenantRepository tenants; <1>
|
||||
|
||||
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); <2>
|
||||
|
||||
public TenantAuthenticationManagerResolver(TenantRepository tenants) {
|
||||
this.tenants = tenants;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationManager resolve(HttpServletRequest request) {
|
||||
return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant);
|
||||
}
|
||||
|
||||
private String toTenant(HttpServletRequest request) {
|
||||
String[] pathParts = request.getRequestURI().split("/");
|
||||
return pathParts.length > 0 ? pathParts[1] : null;
|
||||
}
|
||||
|
||||
private AuthenticationManager fromTenant(String tenant) {
|
||||
return Optional.ofNullable(this.tenants.get(tenant)) <3>
|
||||
.map(JwtDecoders::fromIssuerLocation) <4>
|
||||
.map(JwtAuthenticationProvider::new)
|
||||
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> A hypothetical source for tenant information
|
||||
<2> A cache for `AuthenticationManager`s, keyed by tenant identifier
|
||||
<3> Looking up the tenant is more secure than simply computing the issuer location on the fly - the lookup acts as a tenant whitelist
|
||||
<4> Create a `JwtDecoder` via the discovery endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
|
||||
|
||||
And then specify this `AuthenticationManagerResolver` in the DSL:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
http
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer
|
||||
.authenticationManagerResolver(this.tenantAuthenticationManagerResolver)
|
||||
);
|
||||
----
|
||||
|
||||
==== Resolving the Tenant By Claim
|
||||
|
||||
Resolving the tenant by claim is similar to doing so by request material.
|
||||
The only real difference is the `toTenant` method implementation:
|
||||
One way to differentiate tenants is by the issuer claim. Since the issuer claim accompanies signed JWTs, this can be done with the `JwtIssuerAuthenticationManagerResolver`, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Component
|
||||
public class TenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
|
||||
private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
|
||||
private final TenantRepository tenants; <1>
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
|
||||
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
|
||||
|
||||
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); <2>
|
||||
|
||||
public TenantAuthenticationManagerResolver(TenantRepository tenants) {
|
||||
this.tenants = tenants;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationManager resolve(HttpServletRequest request) {
|
||||
return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant); <3>
|
||||
}
|
||||
|
||||
private String toTenant(HttpServletRequest request) {
|
||||
try {
|
||||
String token = this.resolver.resolve(request);
|
||||
return (String) JWTParser.parse(token).getJWTClaimsSet().getIssuer();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationManager fromTenant(String tenant) {
|
||||
return Optional.ofNullable(this.tenants.get(tenant)) <3>
|
||||
.map(JwtDecoders::fromIssuerLocation) <4>
|
||||
.map(JwtAuthenticationProvider::new)
|
||||
.orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> A hypothetical source for tenant information
|
||||
<2> A cache for `AuthenticationManager`s, keyed by tenant identifier
|
||||
<3> Looking up the tenant is more secure than simply computing the issuer location on the fly - the lookup acts as a tenant whitelist
|
||||
<4> Create a `JwtDecoder` via the discovery endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
|
||||
|
||||
[source,java]
|
||||
----
|
||||
http
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests
|
||||
@ -1353,13 +1259,52 @@ http
|
||||
)
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer
|
||||
.authenticationManagerResolver(this.tenantAuthenticationManagerResolver)
|
||||
.authenticationManagerResolver(authenticationManagerResolver)
|
||||
);
|
||||
----
|
||||
|
||||
==== Parsing the Claim Only Once
|
||||
This is nice because the issuer endpoints are loaded lazily.
|
||||
In fact, the corresponding `JwtAuthenticationProvider` is instantiated only when the first request with the corresponding issuer is sent.
|
||||
This allows for an application startup that is independent from those authorization servers being up and available.
|
||||
|
||||
You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the `JwtDecoder`.
|
||||
===== Dynamic Tenants
|
||||
|
||||
Of course, you may not want to restart the application each time a new tenant is added.
|
||||
In this case, you can configure the `JwtIssuerAuthenticationManagerResolver` with a repository of `AuthenticationManager` instances, which you can edit at runtime, like so:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
private void addManager(Map<String, AuthenticationManager> authenticationManagers, String issuer) {
|
||||
JwtAuthenticationProvider authenticationProvider = new JwtAuthenticationProvider
|
||||
(JwtDecoders.fromIssuerLocation(issuer));
|
||||
authenticationManagers.put(issuer, authenticationProvider::authenticate);
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
|
||||
|
||||
http
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2ResourceServer ->
|
||||
oauth2ResourceServer
|
||||
.authenticationManagerResolver(authenticationManagerResolver)
|
||||
);
|
||||
----
|
||||
|
||||
In this case, you construct `JwtIssuerAuthenticationManagerResolver` with a strategy for obtaining the `AuthenticationManager` given the issuer.
|
||||
This approach allows us to add and remove elements from the repository (shown as a `Map` in the snippet) at runtime.
|
||||
|
||||
NOTE: It would be unsafe to simply take any issuer and construct an `AuthenticationManager` from it.
|
||||
The issuer should be one that the code can verify from a trusted source like a whitelist.
|
||||
|
||||
===== Parsing the Claim Only Once
|
||||
|
||||
You may have observed that this strategy, while simple, comes with the trade-off that the JWT is parsed once by the `AuthenticationManagerResolver` and then again by the `JwtDecoder` later on in the request.
|
||||
|
||||
This extra parsing can be alleviated by configuring the `JwtDecoder` directly with a `JWTClaimSetAwareJWSKeySelector` from Nimbus:
|
||||
|
||||
@ -1479,8 +1424,8 @@ JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtVa
|
||||
|
||||
We've finished talking about resolving the tenant.
|
||||
|
||||
If you've chosen to resolve the tenant by request material, then you'll need to make sure you address your downstream resource servers in the same way.
|
||||
For example, if you are resolving it by subdomain, you'll need to address the downstream resource server using the same subdomain.
|
||||
If you've chosen to resolve the tenant by something other than a JWT claim, then you'll need to make sure you address your downstream resource servers in the same way.
|
||||
For example, if you are resolving it by subdomain, you may need to address the downstream resource server using the same subdomain.
|
||||
|
||||
However, if you resolve it by a claim in the bearer token, read on to learn about <<oauth2resourceserver-bearertoken-resolver,Spring Security's support for bearer token propagation>>.
|
||||
|
||||
|
@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2002-2019 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 javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import com.nimbusds.jwt.JWTParser;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoders;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenError;
|
||||
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
|
||||
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
|
||||
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of {@link AuthenticationManagerResolver} that resolves a JWT-based {@link AuthenticationManager}
|
||||
* 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 HttpServletRequest}'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 JwtIssuerAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
|
||||
private static final OAuth2Error DEFAULT_INVALID_TOKEN = invalidToken("Invalid token");
|
||||
|
||||
private final AuthenticationManagerResolver<String> issuerAuthenticationManagerResolver;
|
||||
private final Converter<HttpServletRequest, String> issuerConverter = new JwtClaimIssuerConverter();
|
||||
|
||||
/**
|
||||
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided parameters
|
||||
*
|
||||
* @param trustedIssuers a whitelist of trusted issuers
|
||||
*/
|
||||
public JwtIssuerAuthenticationManagerResolver(String... trustedIssuers) {
|
||||
this(Arrays.asList(trustedIssuers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a {@link JwtIssuerAuthenticationManagerResolver} using the provided parameters
|
||||
*
|
||||
* @param trustedIssuers a whitelist of trusted issuers
|
||||
*/
|
||||
public JwtIssuerAuthenticationManagerResolver(Collection<String> trustedIssuers) {
|
||||
Assert.notEmpty(trustedIssuers, "trustedIssuers cannot be empty");
|
||||
this.issuerAuthenticationManagerResolver =
|
||||
new TrustedIssuerJwtAuthenticationManagerResolver
|
||||
(Collections.unmodifiableCollection(trustedIssuers)::contains);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a whitelist.
|
||||
*
|
||||
* One way to achieve this is with a {@link Map} where the keys are the known issuers:
|
||||
* <pre>
|
||||
* Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
|
||||
* authenticationManagers.put("https://issuerOne.example.org", managerOne);
|
||||
* authenticationManagers.put("https://issuerTwo.example.org", managerTwo);
|
||||
* JwtAuthenticationManagerResolver resolver = new JwtAuthenticationManagerResolver
|
||||
* (authenticationManagers::get);
|
||||
* </pre>
|
||||
*
|
||||
* The keys in the {@link Map} are the whitelist.
|
||||
*
|
||||
* @param issuerAuthenticationManagerResolver a strategy for resolving the {@link AuthenticationManager} by the issuer
|
||||
*/
|
||||
public JwtIssuerAuthenticationManagerResolver(AuthenticationManagerResolver<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 AuthenticationManager}
|
||||
* can't be derived from the issuer
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationManager resolve(HttpServletRequest request) {
|
||||
String issuer = this.issuerConverter.convert(request);
|
||||
AuthenticationManager authenticationManager = this.issuerAuthenticationManagerResolver.resolve(issuer);
|
||||
if (authenticationManager == null) {
|
||||
throw new OAuth2AuthenticationException(invalidToken("Invalid issuer " + issuer));
|
||||
}
|
||||
return authenticationManager;
|
||||
}
|
||||
|
||||
private static class JwtClaimIssuerConverter
|
||||
implements Converter<HttpServletRequest, String> {
|
||||
|
||||
private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
|
||||
|
||||
@Override
|
||||
public String convert(@NonNull HttpServletRequest request) {
|
||||
String token = this.resolver.resolve(request);
|
||||
try {
|
||||
String issuer = JWTParser.parse(token).getJWTClaimsSet().getIssuer();
|
||||
if (issuer != null) {
|
||||
return issuer;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new OAuth2AuthenticationException(invalidToken(e.getMessage()));
|
||||
}
|
||||
throw new OAuth2AuthenticationException(invalidToken("Missing issuer"));
|
||||
}
|
||||
}
|
||||
|
||||
private static class TrustedIssuerJwtAuthenticationManagerResolver
|
||||
implements AuthenticationManagerResolver<String> {
|
||||
|
||||
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
|
||||
private final Predicate<String> trustedIssuer;
|
||||
|
||||
TrustedIssuerJwtAuthenticationManagerResolver(Predicate<String> trustedIssuer) {
|
||||
this.trustedIssuer = trustedIssuer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationManager resolve(String issuer) {
|
||||
if (this.trustedIssuer.test(issuer)) {
|
||||
return this.authenticationManagers.computeIfAbsent(issuer, k -> {
|
||||
JwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuer);
|
||||
return new JwtAuthenticationProvider(jwtDecoder)::authenticate;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2002-2019 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.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.JWSAlgorithm;
|
||||
import com.nimbusds.jose.JWSHeader;
|
||||
import com.nimbusds.jose.JWSObject;
|
||||
import com.nimbusds.jose.Payload;
|
||||
import com.nimbusds.jose.crypto.RSASSASigner;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import com.nimbusds.jwt.PlainJWT;
|
||||
import net.minidev.json.JSONObject;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationManagerResolver;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.jose.TestKeys;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
|
||||
|
||||
/**
|
||||
* Tests for {@link JwtIssuerAuthenticationManagerResolver}
|
||||
*/
|
||||
public class JwtIssuerAuthenticationManagerResolverTests {
|
||||
private static final String DEFAULT_RESPONSE_TEMPLATE = "{\n"
|
||||
+ " \"issuer\": \"%s\", \n"
|
||||
+ " \"jwks_uri\": \"%s/.well-known/jwks.json\" \n"
|
||||
+ "}";
|
||||
|
||||
private String jwt = jwt("iss", "trusted");
|
||||
private String evil = jwt("iss", "\"");
|
||||
private String noIssuer = jwt("sub", "sub");
|
||||
|
||||
@Test
|
||||
public void resolveWhenUsingTrustedIssuerThenReturnsAuthenticationManager() throws Exception {
|
||||
try (MockWebServer server = new MockWebServer()) {
|
||||
server.start();
|
||||
String issuer = server.url("").toString();
|
||||
server.enqueue(new MockResponse()
|
||||
.setResponseCode(200)
|
||||
.setHeader("Content-Type", "application/json")
|
||||
.setBody(String.format(DEFAULT_RESPONSE_TEMPLATE, issuer, issuer)));
|
||||
JWSObject jws = new JWSObject(new JWSHeader(JWSAlgorithm.RS256),
|
||||
new Payload(new JSONObject(Collections.singletonMap(ISS, issuer))));
|
||||
jws.sign(new RSASSASigner(TestKeys.DEFAULT_PRIVATE_KEY));
|
||||
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver(issuer);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + jws.serialize());
|
||||
|
||||
AuthenticationManager authenticationManager =
|
||||
authenticationManagerResolver.resolve(request);
|
||||
assertThat(authenticationManager).isNotNull();
|
||||
|
||||
AuthenticationManager cachedAuthenticationManager =
|
||||
authenticationManagerResolver.resolve(request);
|
||||
assertThat(authenticationManager).isSameAs(cachedAuthenticationManager);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenUsingUntrustedIssuerThenException() {
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver("other", "issuers");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + this.jwt);
|
||||
|
||||
assertThatCode(() -> authenticationManagerResolver.resolve(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("Invalid issuer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenUsingCustomIssuerAuthenticationManagerResolverThenUses() {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver(issuer -> authenticationManager);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + this.jwt);
|
||||
|
||||
assertThat(authenticationManagerResolver.resolve(request))
|
||||
.isSameAs(authenticationManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenUsingExternalSourceThenRespondsToChanges() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + this.jwt);
|
||||
|
||||
Map<String, AuthenticationManager> authenticationManagers = new HashMap<>();
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver(authenticationManagers::get);
|
||||
assertThatCode(() -> authenticationManagerResolver.resolve(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("Invalid issuer");
|
||||
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
authenticationManagers.put("trusted", authenticationManager);
|
||||
assertThat(authenticationManagerResolver.resolve(request))
|
||||
.isSameAs(authenticationManager);
|
||||
|
||||
authenticationManagers.clear();
|
||||
assertThatCode(() -> authenticationManagerResolver.resolve(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("Invalid issuer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenBearerTokenMalformedThenException() {
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver("trusted");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer jwt");
|
||||
assertThatCode(() -> authenticationManagerResolver.resolve(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageNotContaining("Invalid issuer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenBearerTokenNoIssuerThenException() {
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver("trusted");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + this.noIssuer);
|
||||
assertThatCode(() -> authenticationManagerResolver.resolve(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessageContaining("Missing issuer");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenBearerTokenEvilThenGenericException() {
|
||||
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver =
|
||||
new JwtIssuerAuthenticationManagerResolver("trusted");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer " + this.evil);
|
||||
assertThatCode(() -> authenticationManagerResolver.resolve(request))
|
||||
.isInstanceOf(OAuth2AuthenticationException.class)
|
||||
.hasMessage("Invalid token");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenNullOrEmptyIssuersThenException() {
|
||||
assertThatCode(() -> new JwtIssuerAuthenticationManagerResolver((Collection) null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatCode(() -> new JwtIssuerAuthenticationManagerResolver(Collections.emptyList()))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenNullAuthenticationManagerResolverThenException() {
|
||||
assertThatCode(() -> new JwtIssuerAuthenticationManagerResolver((AuthenticationManagerResolver) null))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
private String jwt(String claim, String value) {
|
||||
PlainJWT jwt = new PlainJWT(new JWTClaimsSet.Builder().claim(claim, value).build());
|
||||
return jwt.serialize();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user