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:
Ioannis Kakavas 2019-05-30 17:33:06 +03:00
parent 5a76f46ac6
commit 7cabe8acc9
2 changed files with 19 additions and 2 deletions

View File

@ -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);
} }

View File

@ -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));