@@ -1148,8 +1148,292 @@ OpaqueTokenIntrospector introspector() {
11481148}
11491149----
11501150
1151- Thus far we have only taken a look at the most basic authentication configuration.
1152- Let's take a look at a few slightly more advanced options for configuring authentication.
1151+ [[oauth2reourceserver-opaqueandjwt]]
1152+ === Supporting both JWT and Opaque Token
1153+
1154+ In some cases, you may have a need to access both kinds of tokens.
1155+ For example, you may support more than one tenant where one tenant issues JWTs and the other issues opaque tokens.
1156+
1157+ If this decision must be made at request-time, then you can use an `AuthenticationManagerResolver` to achieve it, like so:
1158+
1159+ [source,java]
1160+ ----
1161+ @Bean
1162+ AuthenticationManagerResolver<HttpServletRequest> tokenAuthenticationManagerResolver() {
1163+ BearerTokenResolver bearerToken = new DefaultBearerTokenResolver();
1164+ JwtAuthenticationProvider jwt = jwt();
1165+ OpaqueTokenAuthenticationProvider opaqueToken = opaqueToken();
1166+
1167+ return request -> {
1168+ String token = bearerToken.resolve(request);
1169+ if (isAJwt(token)) {
1170+ return jwt::authenticate;
1171+ } else {
1172+ return opaqueToken::authenticate;
1173+ }
1174+ }
1175+ }
1176+ ----
1177+
1178+ And then specify this `AuthenticationManagerResolver` in the DSL:
1179+
1180+ [source,java]
1181+ ----
1182+ http
1183+ .authorizeRequests()
1184+ .anyRequest().authenticated()
1185+ .and()
1186+ .oauth2ResourceServer()
1187+ .authenticationManagerResolver(this.tokenAuthenticationManagerResolver);
1188+ ----
1189+
1190+ [[oauth2resourceserver-multitenancy]]
1191+ === Multi-tenancy
1192+
1193+ A resource server is considered multi-tenant when there are multiple strategies for verifying a bearer token, keyed by some tenant identifier.
1194+
1195+ For example, your resource server may accept bearer tokens from two different authorization servers.
1196+ Or, your authorization server may represent a multiplicity of issuers.
1197+
1198+ In each case, there are two things that need to be done and trade-offs associated with how you choose to do them:
1199+
1200+ 1. Resolve the tenant
1201+ 2. Propagate the tenant
1202+
1203+ ==== Resolving the Tenant By Request Material
1204+
1205+ Resolving the tenant by request material can be done my implementing an `AuthenticationManagerResolver`, which determines the `AuthenticationManager` at runtime, like so:
1206+
1207+ [source,java]
1208+ ----
1209+ @Component
1210+ public class TenantAuthenticationManagerResolver
1211+ implements AuthenticationManagerResolver<HttpServletRequest> {
1212+ private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
1213+ private final TenantRepository tenants; <1>
1214+
1215+ private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); <2>
1216+
1217+ public TenantAuthenticationManagerResolver(TenantRepository tenants) {
1218+ this.tenants = tenants;
1219+ }
1220+
1221+ @Override
1222+ public AuthenticationManager resolve(HttpServletRequest request) {
1223+ return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant);
1224+ }
1225+
1226+ private String toTenant(HttpServletRequest request) {
1227+ String[] pathParts = request.getRequestURI().split("/");
1228+ return pathParts.length > 0 ? pathParts[1] : null;
1229+ }
1230+
1231+ private AuthenticationManager fromTenant(String tenant) {
1232+ return Optional.ofNullable(this.tenants.get(tenant)) <3>
1233+ .map(JwtDecoders::fromIssuerLocation) <4>
1234+ .map(JwtAuthenticationProvider::new)
1235+ .orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
1236+ }
1237+ }
1238+ ----
1239+ <1> A hypothetical source for tenant information
1240+ <2> A cache for `AuthenticationManager`s, keyed by tenant identifier
1241+ <3> Looking up the tenant is more secure than simply computing the issuer location on the fly - the lookup acts as a tenant whitelist
1242+ <4> Create a `JwtDecoder` via the discovery endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
1243+
1244+ And then specify this `AuthenticationManagerResolver` in the DSL:
1245+
1246+ [source,java]
1247+ ----
1248+ http
1249+ .authorizeRequests()
1250+ .anyRequest().authenticated()
1251+ .and()
1252+ .oauth2ResourceServer()
1253+ .authenticationManagerResolver(this.tenantAuthenticationManagerResolver);
1254+ ----
1255+
1256+ ==== Resolving the Tenant By Claim
1257+
1258+ Resolving the tenant by claim is similar to doing so by request material.
1259+ The only real difference is the `toTenant` method implementation:
1260+
1261+ [source,java]
1262+ ----
1263+ @Component
1264+ public class TenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
1265+ private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
1266+ private final TenantRepository tenants; <1>
1267+
1268+ private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>(); <2>
1269+
1270+ public TenantAuthenticationManagerResolver(TenantRepository tenants) {
1271+ this.tenants = tenants;
1272+ }
1273+
1274+ @Override
1275+ public AuthenticationManager resolve(HttpServletRequest request) {
1276+ return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant); <3>
1277+ }
1278+
1279+ private String toTenant(HttpServletRequest request) {
1280+ try {
1281+ String token = this.resolver.resolve(request);
1282+ return (String) JWTParser.parse(token).getJWTClaimsSet().getIssuer();
1283+ } catch (Exception e) {
1284+ throw new IllegalArgumentException(e);
1285+ }
1286+ }
1287+
1288+ private AuthenticationManager fromTenant(String tenant) {
1289+ return Optional.ofNullable(this.tenants.get(tenant)) <3>
1290+ .map(JwtDecoders::fromIssuerLocation) <4>
1291+ .map(JwtAuthenticationProvider::new)
1292+ .orElseThrow(() -> new IllegalArgumentException("unknown tenant"))::authenticate;
1293+ }
1294+ }
1295+ ----
1296+ <1> A hypothetical source for tenant information
1297+ <2> A cache for `AuthenticationManager`s, keyed by tenant identifier
1298+ <3> Looking up the tenant is more secure than simply computing the issuer location on the fly - the lookup acts as a tenant whitelist
1299+ <4> Create a `JwtDecoder` via the discovery endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
1300+
1301+ [source,java]
1302+ ----
1303+ http
1304+ .authorizeRequests()
1305+ .anyRequest().authenticated()
1306+ .and()
1307+ .oauth2ResourceServer()
1308+ .authenticationManagerResolver(this.tenantAuthenticationManagerResolver);
1309+ ----
1310+
1311+ ==== Parsing the Claim Only Once
1312+
1313+ 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`.
1314+
1315+ This extra parsing can be alleviated by configuring the `JwtDecoder` directly with a `JWTClaimSetAwareJWSKeySelector` from Nimbus:
1316+
1317+ [source,java]
1318+ ----
1319+ @Component
1320+ public class TenantJWSKeySelector
1321+ implements JWTClaimSetAwareJWSKeySelector<SecurityContext> {
1322+
1323+ private final TenantRepository tenants; <1>
1324+ private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); <2>
1325+
1326+ public TenantJWSKeySelector(TenantRepository tenants) {
1327+ this.tenants = tenants;
1328+ }
1329+
1330+ @Override
1331+ public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext)
1332+ throws KeySourceException {
1333+ return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant)
1334+ .selectJWSKeys(jwsHeader, securityContext);
1335+ }
1336+
1337+ private String toTenant(JWTClaimsSet claimSet) {
1338+ return (String) claimSet.getClaim("iss");
1339+ }
1340+
1341+ private JWSKeySelector<SecurityContext> fromTenant(String tenant) {
1342+ return Optional.ofNullable(this.tenantRepository.findById(tenant)) <3>
1343+ .map(t -> t.getAttrbute("jwks_uri"))
1344+ .map(this::fromUri)
1345+ .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
1346+ }
1347+
1348+ private JWSKeySelector<SecurityContext> fromUri(String uri) {
1349+ try {
1350+ return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); <4>
1351+ } catch (Exception e) {
1352+ throw new IllegalArgumentException(e);
1353+ }
1354+ }
1355+ }
1356+ ----
1357+ <1> A hypothetical source for tenant information
1358+ <2> A cache for `JWKKeySelector`s, keyed by tenant identifier
1359+ <3> Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a tenant whitelist
1360+ <4> Create a `JWSKeySelector` via the types of keys that come back from the JWK Set endpoint - the lazy lookup here means that you don't need to configure all tenants at startup
1361+
1362+ The above key selector is a composition of many key selectors.
1363+ It chooses which key selector to use based on the `iss` claim in the JWT.
1364+
1365+ NOTE: To use this approach, make sure that the authorization server is configured to include the claim set as part of the token's signature.
1366+ Without this, you have no guarantee that the issuer hasn't been altered by a bad actor.
1367+
1368+ Next, we can construct a `JWTProcessor`:
1369+
1370+ [source,java]
1371+ ----
1372+ @Bean
1373+ JWTProcessor jwtProcessor(JWTClaimSetJWSKeySelector keySelector) {
1374+ ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
1375+ new DefaultJWTProcessor();
1376+ jwtProcessor.setJWTClaimSetJWSKeySelector(keySelector);
1377+ return jwtProcessor;
1378+ }
1379+ ----
1380+
1381+ As you are already seeing, the trade-off for moving tenant-awareness down to this level is more configuration.
1382+ We have just a bit more.
1383+
1384+ Next, we still want to make sure you are validating the issuer.
1385+ But, since the issuer may be different per JWT, then you'll need a tenant-aware validator, too:
1386+
1387+ [source,java]
1388+ ----
1389+ @Component
1390+ public class TenantJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
1391+ private final TenantRepository tenants;
1392+ private final Map<String, JwtIssuerValidator> validators = new ConcurrentHashMap<>();
1393+
1394+ public TenantJwtIssuerValidator(TenantRepository tenants) {
1395+ this.tenants = tenants;
1396+ }
1397+
1398+ @Override
1399+ public OAuth2TokenValidatorResult validate(Jwt token) {
1400+ return this.validators.computeIfAbsent(toTenant(token), this::fromTenant)
1401+ .validate(token);
1402+ }
1403+
1404+ private String toTenant(Jwt jwt) {
1405+ return jwt.getIssuer();
1406+ }
1407+
1408+ private JwtIssuerValidator fromTenant(String tenant) {
1409+ return Optional.ofNullable(this.tenants.findById(tenant))
1410+ .map(t -> t.getAttribute("issuer"))
1411+ .map(JwtIssuerValidator::new)
1412+ .orElseThrow(() -> new IllegalArgumentException("unknown tenant"));
1413+ }
1414+ }
1415+ ----
1416+
1417+ Now that we have a tenant-aware processor and a tenant-aware validator, we can proceed with creating our `JwtDecoder`:
1418+
1419+ [source,java]
1420+ ----
1421+ @Bean
1422+ JwtDecoder jwtDecoder(JWTProcessor jwtProcessor, OAuth2TokenValidator<Jwt> jwtValidator) {
1423+ NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor);
1424+ OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>
1425+ (JwtValidators.createDefault(), this.jwtValidator);
1426+ decoder.setJwtValidator(validator);
1427+ return decoder;
1428+ }
1429+ ----
1430+
1431+ We've finished talking about resolving the tenant.
1432+
1433+ 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.
1434+ For example, if you are resolving it by subdomain, you'll need to address the downstream resource server using the same subdomain.
1435+
1436+ 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>>.
11531437
11541438[[oauth2resourceserver-bearertoken-resolver]]
11551439=== Bearer Token Resolution
0 commit comments