Skip to content

Commit 64e9c88

Browse files
committed
Fix refresh remote JWKS logic (#42662)
This change ensures that: - We only attempt to refresh the remote JWKS when there is a signature related error only ( BadJWSException instead of the geric BadJOSEException ) - We do call OpenIDConnectAuthenticator#getUserClaims upon successful refresh. - We test this in OpenIdConnectAuthenticatorTests. Without this fix, when using the OpenID Connect realm with a remote JWKSet configured in `op.jwks_path`, the refresh would be triggered for most configuration errors ( i.e. wrong value for `op.issuer` ) and the kibana wouldn't get a response and timeout since `getUserClaims` wouldn't be called because `ReloadableJWKSource#reloadAsync` wouldn't call `onResponse` on the future.
1 parent 05bc599 commit 64e9c88

File tree

2 files changed

+19
-2
lines changed

2 files changed

+19
-2
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticator.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.nimbusds.jose.jwk.JWKSet;
1313
import com.nimbusds.jose.jwk.source.JWKSource;
1414
import com.nimbusds.jose.proc.BadJOSEException;
15+
import com.nimbusds.jose.proc.BadJWSException;
1516
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
1617
import com.nimbusds.jose.proc.SecurityContext;
1718
import com.nimbusds.jose.util.IOUtils;
@@ -240,7 +241,7 @@ private void getUserClaims(@Nullable AccessToken accessToken, JWT idToken, Nonce
240241
}
241242
claimsListener.onResponse(enrichedVerifiedIdTokenClaims);
242243
}
243-
} catch (BadJOSEException e) {
244+
} catch (BadJWSException e) {
244245
// We only try to update the cached JWK set once if a remote source is used and
245246
// RSA or ECDSA is used for signatures
246247
if (shouldRetry
@@ -256,7 +257,7 @@ private void getUserClaims(@Nullable AccessToken accessToken, JWT idToken, Nonce
256257
} else {
257258
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
258259
}
259-
} catch (com.nimbusds.oauth2.sdk.ParseException | ParseException | JOSEException e) {
260+
} catch (com.nimbusds.oauth2.sdk.ParseException | ParseException | BadJOSEException | JOSEException e) {
260261
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
261262
}
262263
}
@@ -777,6 +778,7 @@ public void completed(HttpResponse result) {
777778
StandardCharsets.UTF_8));
778779
reloadFutureRef.set(null);
779780
LOGGER.trace("Successfully refreshed and cached remote JWKSet");
781+
future.onResponse(null);
780782
} catch (IOException | ParseException e) {
781783
failed(e);
782784
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthenticatorTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,15 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
8888
private Settings globalSettings;
8989
private Environment env;
9090
private ThreadContext threadContext;
91+
private int callsToReloadJwk;
9192

9293
@Before
9394
public void setup() {
9495
globalSettings = Settings.builder().put("path.home", createTempDir())
9596
.put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate").build();
9697
env = TestEnvironment.newEnvironment(globalSettings);
9798
threadContext = new ThreadContext(globalSettings);
99+
callsToReloadJwk = 0;
98100
}
99101

100102
@After
@@ -278,6 +280,7 @@ public void testClockSkewIsHonored() throws Exception {
278280
authenticator.authenticate(token, future);
279281
JWTClaimsSet claimsSet = future.actionGet();
280282
assertThat(claimsSet.getSubject(), equalTo(subject));
283+
assertThat(callsToReloadJwk, equalTo(0));
281284
}
282285

283286
public void testImplicitFlowFailsWithExpiredToken() throws Exception {
@@ -317,6 +320,7 @@ public void testImplicitFlowFailsWithExpiredToken() throws Exception {
317320
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
318321
assertThat(e.getCause(), instanceOf(BadJWTException.class));
319322
assertThat(e.getCause().getMessage(), containsString("Expired JWT"));
323+
assertThat(callsToReloadJwk, equalTo(0));
320324
}
321325

322326
public void testImplicitFlowFailsNotYetIssuedToken() throws Exception {
@@ -356,6 +360,7 @@ public void testImplicitFlowFailsNotYetIssuedToken() throws Exception {
356360
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
357361
assertThat(e.getCause(), instanceOf(BadJWTException.class));
358362
assertThat(e.getCause().getMessage(), containsString("JWT issue time ahead of current time"));
363+
assertThat(callsToReloadJwk, equalTo(0));
359364
}
360365

361366
public void testImplicitFlowFailsInvalidIssuer() throws Exception {
@@ -394,6 +399,7 @@ public void testImplicitFlowFailsInvalidIssuer() throws Exception {
394399
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
395400
assertThat(e.getCause(), instanceOf(BadJWTException.class));
396401
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT issuer"));
402+
assertThat(callsToReloadJwk, equalTo(0));
397403
}
398404

399405
public void testImplicitFlowFailsInvalidAudience() throws Exception {
@@ -432,6 +438,7 @@ public void testImplicitFlowFailsInvalidAudience() throws Exception {
432438
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
433439
assertThat(e.getCause(), instanceOf(BadJWTException.class));
434440
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT audience"));
441+
assertThat(callsToReloadJwk, equalTo(0));
435442
}
436443

437444
public void testAuthenticateImplicitFlowFailsWithForgedRsaIdToken() throws Exception {
@@ -456,6 +463,7 @@ public void testAuthenticateImplicitFlowFailsWithForgedRsaIdToken() throws Excep
456463
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
457464
assertThat(e.getCause(), instanceOf(BadJWSException.class));
458465
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
466+
assertThat(callsToReloadJwk, equalTo(1));
459467
}
460468

461469
public void testAuthenticateImplicitFlowFailsWithForgedEcsdsaIdToken() throws Exception {
@@ -480,6 +488,7 @@ public void testAuthenticateImplicitFlowFailsWithForgedEcsdsaIdToken() throws Ex
480488
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
481489
assertThat(e.getCause(), instanceOf(BadJWSException.class));
482490
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
491+
assertThat(callsToReloadJwk, equalTo(1));
483492
}
484493

485494
public void testAuthenticateImplicitFlowFailsWithForgedHmacIdToken() throws Exception {
@@ -503,6 +512,7 @@ public void testAuthenticateImplicitFlowFailsWithForgedHmacIdToken() throws Exce
503512
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
504513
assertThat(e.getCause(), instanceOf(BadJWSException.class));
505514
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
515+
assertThat(callsToReloadJwk, equalTo(0));
506516
}
507517

508518
public void testAuthenticateImplicitFlowFailsWithForgedAccessToken() throws Exception {
@@ -532,6 +542,7 @@ public void testAuthenticateImplicitFlowFailsWithForgedAccessToken() throws Exce
532542
assertThat(e.getMessage(), containsString("Failed to verify access token"));
533543
assertThat(e.getCause(), instanceOf(InvalidHashException.class));
534544
assertThat(e.getCause().getMessage(), containsString("Access token hash (at_hash) mismatch"));
545+
assertThat(callsToReloadJwk, equalTo(0));
535546
}
536547

537548
public void testImplicitFlowFailsWithNoneAlgorithm() throws Exception {
@@ -569,6 +580,7 @@ public void testImplicitFlowFailsWithNoneAlgorithm() throws Exception {
569580
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
570581
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
571582
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
583+
assertThat(callsToReloadJwk, equalTo(0));
572584
}
573585

574586
/**
@@ -599,6 +611,7 @@ public void testImplicitFlowFailsWithAlgorithmMixupAttack() throws Exception {
599611
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
600612
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
601613
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
614+
assertThat(callsToReloadJwk, equalTo(0));
602615
}
603616

604617
public void testImplicitFlowFailsWithUnsignedJwt() throws Exception {
@@ -635,6 +648,7 @@ public void testImplicitFlowFailsWithUnsignedJwt() throws Exception {
635648
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
636649
assertThat(e.getCause(), instanceOf(BadJWTException.class));
637650
assertThat(e.getCause().getMessage(), containsString("Signed ID token expected"));
651+
assertThat(callsToReloadJwk, equalTo(0));
638652
}
639653

640654
public void testJsonObjectMerging() throws Exception {
@@ -832,6 +846,7 @@ private OpenIdConnectAuthenticator.ReloadableJWKSource mockSource(JWK jwk) {
832846
Mockito.doAnswer(invocation -> {
833847
@SuppressWarnings("unchecked")
834848
ActionListener<Void> listener = (ActionListener<Void>) invocation.getArguments()[0];
849+
callsToReloadJwk += 1;
835850
listener.onResponse(null);
836851
return null;
837852
}).when(jwkSource).triggerReload(any(ActionListener.class));

0 commit comments

Comments
 (0)