@@ -1148,8 +1148,292 @@ OpaqueTokenIntrospector introspector() {
1148
1148
}
1149
1149
----
1150
1150
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>>.
1153
1437
1154
1438
[[oauth2resourceserver-bearertoken-resolver]]
1155
1439
=== Bearer Token Resolution
0 commit comments