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.
This commit is contained in:
parent
5a76f46ac6
commit
7cabe8acc9
|
@ -12,6 +12,7 @@ import com.nimbusds.jose.jwk.JWKSelector;
|
||||||
import com.nimbusds.jose.jwk.JWKSet;
|
import com.nimbusds.jose.jwk.JWKSet;
|
||||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||||
import com.nimbusds.jose.proc.BadJOSEException;
|
import com.nimbusds.jose.proc.BadJOSEException;
|
||||||
|
import com.nimbusds.jose.proc.BadJWSException;
|
||||||
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
|
||||||
import com.nimbusds.jose.proc.SecurityContext;
|
import com.nimbusds.jose.proc.SecurityContext;
|
||||||
import com.nimbusds.jose.util.IOUtils;
|
import com.nimbusds.jose.util.IOUtils;
|
||||||
|
@ -240,7 +241,7 @@ public class OpenIdConnectAuthenticator {
|
||||||
}
|
}
|
||||||
claimsListener.onResponse(enrichedVerifiedIdTokenClaims);
|
claimsListener.onResponse(enrichedVerifiedIdTokenClaims);
|
||||||
}
|
}
|
||||||
} catch (BadJOSEException e) {
|
} catch (BadJWSException e) {
|
||||||
// We only try to update the cached JWK set once if a remote source is used and
|
// We only try to update the cached JWK set once if a remote source is used and
|
||||||
// RSA or ECDSA is used for signatures
|
// RSA or ECDSA is used for signatures
|
||||||
if (shouldRetry
|
if (shouldRetry
|
||||||
|
@ -256,7 +257,7 @@ public class OpenIdConnectAuthenticator {
|
||||||
} else {
|
} else {
|
||||||
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
|
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
|
||||||
}
|
}
|
||||||
} catch (com.nimbusds.oauth2.sdk.ParseException | ParseException | JOSEException e) {
|
} catch (com.nimbusds.oauth2.sdk.ParseException | ParseException | BadJOSEException | JOSEException e) {
|
||||||
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
|
claimsListener.onFailure(new ElasticsearchSecurityException("Failed to parse or validate the ID Token", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -777,6 +778,7 @@ public class OpenIdConnectAuthenticator {
|
||||||
StandardCharsets.UTF_8));
|
StandardCharsets.UTF_8));
|
||||||
reloadFutureRef.set(null);
|
reloadFutureRef.set(null);
|
||||||
LOGGER.trace("Successfully refreshed and cached remote JWKSet");
|
LOGGER.trace("Successfully refreshed and cached remote JWKSet");
|
||||||
|
future.onResponse(null);
|
||||||
} catch (IOException | ParseException e) {
|
} catch (IOException | ParseException e) {
|
||||||
failed(e);
|
failed(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
private Settings globalSettings;
|
private Settings globalSettings;
|
||||||
private Environment env;
|
private Environment env;
|
||||||
private ThreadContext threadContext;
|
private ThreadContext threadContext;
|
||||||
|
private int callsToReloadJwk;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
|
@ -95,6 +96,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
.put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate").build();
|
.put("xpack.security.authc.realms.oidc.oidc-realm.ssl.verification_mode", "certificate").build();
|
||||||
env = TestEnvironment.newEnvironment(globalSettings);
|
env = TestEnvironment.newEnvironment(globalSettings);
|
||||||
threadContext = new ThreadContext(globalSettings);
|
threadContext = new ThreadContext(globalSettings);
|
||||||
|
callsToReloadJwk = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
@ -278,6 +280,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
authenticator.authenticate(token, future);
|
authenticator.authenticate(token, future);
|
||||||
JWTClaimsSet claimsSet = future.actionGet();
|
JWTClaimsSet claimsSet = future.actionGet();
|
||||||
assertThat(claimsSet.getSubject(), equalTo(subject));
|
assertThat(claimsSet.getSubject(), equalTo(subject));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testImplicitFlowFailsWithExpiredToken() throws Exception {
|
public void testImplicitFlowFailsWithExpiredToken() throws Exception {
|
||||||
|
@ -317,6 +320,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Expired JWT"));
|
assertThat(e.getCause().getMessage(), containsString("Expired JWT"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testImplicitFlowFailsNotYetIssuedToken() throws Exception {
|
public void testImplicitFlowFailsNotYetIssuedToken() throws Exception {
|
||||||
|
@ -356,6 +360,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("JWT issue time ahead of current time"));
|
assertThat(e.getCause().getMessage(), containsString("JWT issue time ahead of current time"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testImplicitFlowFailsInvalidIssuer() throws Exception {
|
public void testImplicitFlowFailsInvalidIssuer() throws Exception {
|
||||||
|
@ -394,6 +399,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT issuer"));
|
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT issuer"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testImplicitFlowFailsInvalidAudience() throws Exception {
|
public void testImplicitFlowFailsInvalidAudience() throws Exception {
|
||||||
|
@ -432,6 +438,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT audience"));
|
assertThat(e.getCause().getMessage(), containsString("Unexpected JWT audience"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testAuthenticateImplicitFlowFailsWithForgedRsaIdToken() throws Exception {
|
public void testAuthenticateImplicitFlowFailsWithForgedRsaIdToken() throws Exception {
|
||||||
|
@ -456,6 +463,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testAuthenticateImplicitFlowFailsWithForgedEcsdsaIdToken() throws Exception {
|
public void testAuthenticateImplicitFlowFailsWithForgedEcsdsaIdToken() throws Exception {
|
||||||
|
@ -480,6 +488,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testAuthenticateImplicitFlowFailsWithForgedHmacIdToken() throws Exception {
|
public void testAuthenticateImplicitFlowFailsWithForgedHmacIdToken() throws Exception {
|
||||||
|
@ -503,6 +512,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
assertThat(e.getCause(), instanceOf(BadJWSException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
assertThat(e.getCause().getMessage(), containsString("Signed JWT rejected: Invalid signature"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testAuthenticateImplicitFlowFailsWithForgedAccessToken() throws Exception {
|
public void testAuthenticateImplicitFlowFailsWithForgedAccessToken() throws Exception {
|
||||||
|
@ -532,6 +542,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to verify access token"));
|
assertThat(e.getMessage(), containsString("Failed to verify access token"));
|
||||||
assertThat(e.getCause(), instanceOf(InvalidHashException.class));
|
assertThat(e.getCause(), instanceOf(InvalidHashException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Access token hash (at_hash) mismatch"));
|
assertThat(e.getCause().getMessage(), containsString("Access token hash (at_hash) mismatch"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testImplicitFlowFailsWithNoneAlgorithm() throws Exception {
|
public void testImplicitFlowFailsWithNoneAlgorithm() throws Exception {
|
||||||
|
@ -569,6 +580,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
|
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
|
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -599,6 +611,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
|
assertThat(e.getCause(), instanceOf(BadJOSEException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
|
assertThat(e.getCause().getMessage(), containsString("Another algorithm expected, or no matching key(s) found"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testImplicitFlowFailsWithUnsignedJwt() throws Exception {
|
public void testImplicitFlowFailsWithUnsignedJwt() throws Exception {
|
||||||
|
@ -635,6 +648,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
assertThat(e.getMessage(), containsString("Failed to parse or validate the ID Token"));
|
||||||
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
assertThat(e.getCause(), instanceOf(BadJWTException.class));
|
||||||
assertThat(e.getCause().getMessage(), containsString("Signed ID token expected"));
|
assertThat(e.getCause().getMessage(), containsString("Signed ID token expected"));
|
||||||
|
assertThat(callsToReloadJwk, equalTo(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testJsonObjectMerging() throws Exception {
|
public void testJsonObjectMerging() throws Exception {
|
||||||
|
@ -832,6 +846,7 @@ public class OpenIdConnectAuthenticatorTests extends OpenIdConnectTestCase {
|
||||||
Mockito.doAnswer(invocation -> {
|
Mockito.doAnswer(invocation -> {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
ActionListener<Void> listener = (ActionListener<Void>) invocation.getArguments()[0];
|
ActionListener<Void> listener = (ActionListener<Void>) invocation.getArguments()[0];
|
||||||
|
callsToReloadJwk += 1;
|
||||||
listener.onResponse(null);
|
listener.onResponse(null);
|
||||||
return null;
|
return null;
|
||||||
}).when(jwkSource).triggerReload(any(ActionListener.class));
|
}).when(jwkSource).triggerReload(any(ActionListener.class));
|
||||||
|
|
Loading…
Reference in New Issue