From 9938986491b3e4fb676289a8efd6023868e904ad Mon Sep 17 00:00:00 2001 From: jaymode Date: Mon, 5 Nov 2018 11:56:49 -0700 Subject: [PATCH 1/5] Implement verification of API keys This change implements the verification of api keys in the ApiKeyService. There is no integration into the AuthenticationService as part of this change; this will be done in a future change. Verification of an API key involves validating the provided key with the hash stored in the document and then ensuring that the token is not expired. A conscious decision has been made to always validate the hash and then check expiration. This is done to prevent leaking that a given key has expired. --- .../xpack/security/authc/ApiKeyService.java | 133 ++++++++++++++++++ .../security/authc/ApiKeyServiceTests.java | 114 +++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index ba9086870addb..62e0402d1a36f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -8,24 +8,31 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.CharArrays; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import javax.crypto.SecretKeyFactory; @@ -34,11 +41,17 @@ import java.time.Clock; import java.time.Instant; import java.util.Arrays; +import java.util.Base64; +import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Objects; import java.util.function.Function; +import java.util.stream.Collectors; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; +import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; public class ApiKeyService { @@ -142,6 +155,107 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ } } + /** + * Checks for the presence of a {@code Authorization} header with a value that starts with + * {@code ApiKey }. If found this will attempt to authenticate the key. + */ + void authenticateWithApiKeyIfPresent(ThreadContext ctx, ActionListener listener) { + if (enabled) { + final ApiKeyCredentials credentials; + try { + credentials = getCredentialsFromHeader(ctx); + } catch (ElasticsearchSecurityException ese) { + listener.onResponse(AuthenticationResult.terminate(ese.getMessage(), ese)); + return; + } + + if (credentials != null) { + final GetRequest getRequest = client.prepareGet(SecurityIndexManager.SECURITY_INDEX_NAME, TYPE, credentials.getId()) + .setFetchSource(true).request(); + executeAsyncWithOrigin(ctx, SECURITY_ORIGIN, getRequest, ActionListener.wrap(response -> { + if (response.isExists()) { + validateApiKeyCredentials(response.getSource(), credentials, clock, listener); + } else { + listener.onResponse(AuthenticationResult.unsuccessful("unable to authenticate", null)); + } + }, e -> listener.onResponse(AuthenticationResult.unsuccessful("apikey auth encountered a failure", e))), client::get); + } else { + listener.onResponse(AuthenticationResult.notHandled()); + } + } else { + listener.onResponse(AuthenticationResult.notHandled()); + } + } + + /** + * Validates the ApiKey using the source map + * @param source the source map from a get of the ApiKey document + * @param credentials the credentials provided by the user + * @param listener the listener to notify after verification + */ + static void validateApiKeyCredentials(Map source, ApiKeyCredentials credentials, Clock clock, + ActionListener listener) { + final String apiKeyHash = (String) source.get("api_key_hash"); + if (apiKeyHash == null) { + throw new IllegalStateException("api key hash is missing"); + } + final char[] apiKeyHashChars = apiKeyHash.toCharArray(); + Hasher hasher = Hasher.resolveFromHash(apiKeyHash.toCharArray()); + final boolean verified = hasher.verify(credentials.getKey(), apiKeyHashChars); + if (verified) { + final Long expirationEpochMilli = (Long) source.get("expiration_time"); + if (expirationEpochMilli == null || Instant.ofEpochMilli(expirationEpochMilli).isAfter(clock.instant())) { + final String principal = Objects.requireNonNull((String) source.get("principal")); + final Map metadata = (Map) source.get("metadata"); + final List> roleDescriptors = (List>) source.get("role_descriptors"); + final String[] roleNames = roleDescriptors.stream() + .map(rdSource -> (String) rdSource.get("name")) + .collect(Collectors.toList()) + .toArray(Strings.EMPTY_ARRAY); + final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true); + listener.onResponse(AuthenticationResult.success(apiKeyUser)); + } else { + listener.onResponse(AuthenticationResult.unsuccessful("api key is expired", null)); + } + } else { + listener.onResponse(AuthenticationResult.unsuccessful("invalid credentials", null)); + } + } + + /** + * Gets the API Key from the Authorization header if the header begins with + * ApiKey + */ + static ApiKeyCredentials getCredentialsFromHeader(ThreadContext threadContext) { + String header = threadContext.getHeader("Authorization"); + if (Strings.hasText(header) && header.regionMatches(true, 0, "ApiKey ", 0, "ApiKey ".length()) + && header.length() > "ApiKey ".length()) { + final byte[] decodedApiKeyCredBytes = Base64.getDecoder().decode(header.substring("ApiKey ".length())); + char[] apiKeyCredChars = null; + try { + apiKeyCredChars = CharArrays.utf8BytesToChars(decodedApiKeyCredBytes); + int colonIndex = -1; + for (int i = 0; i < apiKeyCredChars.length; i++) { + if (apiKeyCredChars[i] == ':') { + colonIndex = i; + break; + } + } + + if (colonIndex < 1) { + throw authenticationError("invalid ApiKey value"); + } + return new ApiKeyCredentials(new String(Arrays.copyOfRange(apiKeyCredChars, 0, colonIndex)), + new SecureString(Arrays.copyOfRange(apiKeyCredChars, colonIndex + 1, apiKeyCredChars.length))); + } finally { + if (apiKeyCredChars != null) { + Arrays.fill(apiKeyCredChars, (char) 0); + } + } + } + return null; + } + private Instant getApiKeyExpiration(Instant now, CreateApiKeyRequest request) { if (request.getExpiration() != null) { return now.plusSeconds(request.getExpiration().getSeconds()); @@ -155,4 +269,23 @@ private void ensureEnabled() { throw new IllegalStateException("tokens are not enabled"); } } + + // package private class for testing + static final class ApiKeyCredentials { + private final String id; + private final SecureString key; + + ApiKeyCredentials(String id, SecureString key) { + this.id = id; + this.key = key; + } + + String getId() { + return id; + } + + SecureString getKey() { + return key; + } + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java new file mode 100644 index 0000000000000..6d596027ef6ea --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.security.authc; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; + +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.is; + +public class ApiKeyServiceTests extends ESTestCase { + + public void testGetCredentialsFromThreadContext() { + ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + assertNull(ApiKeyService.getCredentialsFromHeader(threadContext)); + + final String apiKeyAuthScheme = randomFrom("apikey", "apiKey", "ApiKey", "APikey", "APIKEY"); + final String id = randomAlphaOfLength(12); + final String key = randomAlphaOfLength(16); + String headerValue = apiKeyAuthScheme + " " + Base64.getEncoder().encodeToString((id + ":" + key).getBytes(StandardCharsets.UTF_8)); + + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + threadContext.putHeader("Authorization", headerValue); + ApiKeyService.ApiKeyCredentials creds = ApiKeyService.getCredentialsFromHeader(threadContext); + assertNotNull(creds); + assertEquals(id, creds.getId()); + assertEquals(key, creds.getKey().toString()); + } + + // missing space + headerValue = apiKeyAuthScheme + Base64.getEncoder().encodeToString((id + ":" + key).getBytes(StandardCharsets.UTF_8)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + threadContext.putHeader("Authorization", headerValue); + ApiKeyService.ApiKeyCredentials creds = ApiKeyService.getCredentialsFromHeader(threadContext); + assertNull(creds); + } + + // missing colon + headerValue = apiKeyAuthScheme + " " + Base64.getEncoder().encodeToString((id + key).getBytes(StandardCharsets.UTF_8)); + try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { + threadContext.putHeader("Authorization", headerValue); + ElasticsearchSecurityException e = + expectThrows(ElasticsearchSecurityException.class, () -> ApiKeyService.getCredentialsFromHeader(threadContext)); + assertEquals("invalid ApiKey value", e.getMessage()); + } + } + + public void testValidateApiKey() throws Exception { + final String apiKey = randomAlphaOfLength(16); + Hasher hasher = randomFrom(Hasher.PBKDF2, Hasher.BCRYPT4, Hasher.BCRYPT); + final char[] hash = hasher.hash(new SecureString(apiKey.toCharArray())); + + Map sourceMap = new HashMap<>(); + sourceMap.put("api_key_hash", new String(hash)); + sourceMap.put("principal", "test_user"); + sourceMap.put("metadata", Collections.emptyMap()); + sourceMap.put("role_descriptors", Collections.singletonList(Collections.singletonMap("name", "a role"))); + + + ApiKeyService.ApiKeyCredentials creds = + new ApiKeyService.ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(apiKey.toCharArray())); + PlainActionFuture future = new PlainActionFuture<>(); + ApiKeyService.validateApiKeyCredentials(sourceMap, creds, Clock.systemUTC(), future); + AuthenticationResult result = future.get(); + assertNotNull(result); + assertTrue(result.isAuthenticated()); + assertThat(result.getUser().principal(), is("test_user")); + assertThat(result.getUser().roles(), arrayContaining("a role")); + assertThat(result.getUser().metadata(), is(Collections.emptyMap())); + + sourceMap.put("expiration_time", Clock.systemUTC().instant().plus(1L, ChronoUnit.HOURS).toEpochMilli()); + future = new PlainActionFuture<>(); + ApiKeyService.validateApiKeyCredentials(sourceMap, creds, Clock.systemUTC(), future); + result = future.get(); + assertNotNull(result); + assertTrue(result.isAuthenticated()); + assertThat(result.getUser().principal(), is("test_user")); + assertThat(result.getUser().roles(), arrayContaining("a role")); + assertThat(result.getUser().metadata(), is(Collections.emptyMap())); + + sourceMap.put("expiration_time", Clock.systemUTC().instant().minus(1L, ChronoUnit.HOURS).toEpochMilli()); + future = new PlainActionFuture<>(); + ApiKeyService.validateApiKeyCredentials(sourceMap, creds, Clock.systemUTC(), future); + result = future.get(); + assertNotNull(result); + assertFalse(result.isAuthenticated()); + + sourceMap.remove("expiration_time"); + creds = new ApiKeyService.ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(randomAlphaOfLength(15).toCharArray())); + future = new PlainActionFuture<>(); + ApiKeyService.validateApiKeyCredentials(sourceMap, creds, Clock.systemUTC(), future); + result = future.get(); + assertNotNull(result); + assertFalse(result.isAuthenticated()); + } +} From 0fb01ba4ec0d9e592dc968789186a4de8f3a27f6 Mon Sep 17 00:00:00 2001 From: jaymode Date: Wed, 7 Nov 2018 08:51:50 -0700 Subject: [PATCH 2/5] implement closeable --- .../xpack/security/authc/ApiKeyService.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 62e0402d1a36f..4c59b71b4a08b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -36,6 +36,7 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import javax.crypto.SecretKeyFactory; +import java.io.Closeable; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.time.Clock; @@ -174,11 +175,17 @@ void authenticateWithApiKeyIfPresent(ThreadContext ctx, ActionListenerwrap(response -> { if (response.isExists()) { - validateApiKeyCredentials(response.getSource(), credentials, clock, listener); + try (ApiKeyCredentials ignore = credentials) { + validateApiKeyCredentials(response.getSource(), credentials, clock, listener); + } } else { + credentials.close(); listener.onResponse(AuthenticationResult.unsuccessful("unable to authenticate", null)); } - }, e -> listener.onResponse(AuthenticationResult.unsuccessful("apikey auth encountered a failure", e))), client::get); + }, e -> { + credentials.close(); + listener.onResponse(AuthenticationResult.unsuccessful("apikey auth encountered a failure", e)); + }), client::get); } else { listener.onResponse(AuthenticationResult.notHandled()); } @@ -271,7 +278,7 @@ private void ensureEnabled() { } // package private class for testing - static final class ApiKeyCredentials { + static final class ApiKeyCredentials implements Closeable { private final String id; private final SecureString key; @@ -287,5 +294,10 @@ String getId() { SecureString getKey() { return key; } + + @Override + public void close() { + key.close(); + } } } From cd23f20ffa31228f05eb27a1eadc1f20b2fe7a1e Mon Sep 17 00:00:00 2001 From: jaymode Date: Thu, 8 Nov 2018 13:12:17 -0700 Subject: [PATCH 3/5] add apikey auth scheme to failure handler --- .../authc/DefaultAuthenticationFailureHandler.java | 6 ++++-- .../authc/DefaultAuthenticationFailureHandlerTests.java | 5 +++-- .../java/org/elasticsearch/xpack/security/Security.java | 8 ++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index 736b9378e3876..a7c51b206db9c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -77,10 +77,12 @@ private static Integer authSchemePriority(final String headerValue) { return 0; } else if (headerValue.regionMatches(true, 0, "bearer", 0, "bearer".length())) { return 1; - } else if (headerValue.regionMatches(true, 0, "basic", 0, "basic".length())) { + } else if (headerValue.regionMatches(true, 0, "apikey", 0, "apikey".length())) { return 2; - } else { + } else if (headerValue.regionMatches(true, 0, "basic", 0, "basic".length())) { return 3; + } else { + return 4; } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java index 15593f0b82ea5..24f9d16324f2d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java @@ -111,8 +111,9 @@ public void testSortsWWWAuthenticateHeaderValues() { final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; final String negotiateAuthScheme = randomFrom("Negotiate", "Negotiate Ijoijksdk"); + final String apiKeyAuthScheme = "ApiKey"; final Map> failureResponeHeaders = new HashMap<>(); - final List supportedSchemes = Arrays.asList(basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); + final List supportedSchemes = Arrays.asList(basicAuthScheme, bearerAuthScheme, negotiateAuthScheme, apiKeyAuthScheme); Collections.shuffle(supportedSchemes, random()); failureResponeHeaders.put("WWW-Authenticate", supportedSchemes); final DefaultAuthenticationFailureHandler failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders); @@ -123,7 +124,7 @@ public void testSortsWWWAuthenticateHeaderValues() { assertThat(ese, is(notNullValue())); assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); assertThat(ese.getMessage(), equalTo("error attempting to authenticate request")); - assertWWWAuthenticateWithSchemes(ese, negotiateAuthScheme, bearerAuthScheme, basicAuthScheme); + assertWWWAuthenticateWithSchemes(ese, negotiateAuthScheme, bearerAuthScheme, apiKeyAuthScheme, basicAuthScheme); } private void assertWWWAuthenticateWithSchemes(final ElasticsearchSecurityException ese, final String... schemes) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 0a4df97b826b0..42b02cb2c6203 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -251,6 +251,7 @@ import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_FORMAT_SETTING; +import static org.elasticsearch.xpack.core.XPackSettings.API_KEY_SERVICE_ENABLED_SETTING; import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.INTERNAL_INDEX_FORMAT; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.SECURITY_INDEX_NAME; @@ -540,6 +541,13 @@ private AuthenticationFailureHandler createAuthenticationFailureHandler(final Re defaultFailureResponseHeaders.get("WWW-Authenticate").add(bearerScheme); } } + if (API_KEY_SERVICE_ENABLED_SETTING.get(settings)) { + final String apiKeyScheme = "ApiKey"; + if (defaultFailureResponseHeaders.computeIfAbsent("WWW-Authenticate", x -> new ArrayList<>()) + .contains(apiKeyScheme) == false) { + defaultFailureResponseHeaders.get("WWW-Authenticate").add(apiKeyScheme); + } + } failureHandler = new DefaultAuthenticationFailureHandler(defaultFailureResponseHeaders); } else { logger.debug("Using authentication failure handler from extension [" + extensionName + "]"); From 39e07d477d94d8ccfe8ec5785608545400d12302 Mon Sep 17 00:00:00 2001 From: jaymode Date: Fri, 9 Nov 2018 08:24:12 -0700 Subject: [PATCH 4/5] clear char[] --- .../xpack/security/authc/ApiKeyService.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 158f7cb3fdbc7..7bfc446534cf9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -206,9 +206,8 @@ static void validateApiKeyCredentials(Map source, ApiKeyCredenti if (apiKeyHash == null) { throw new IllegalStateException("api key hash is missing"); } - final char[] apiKeyHashChars = apiKeyHash.toCharArray(); - Hasher hasher = Hasher.resolveFromHash(apiKeyHash.toCharArray()); - final boolean verified = hasher.verify(credentials.getKey(), apiKeyHashChars); + final boolean verified = verifyKeyAgainstHash(apiKeyHash, credentials); + if (verified) { final Long expirationEpochMilli = (Long) source.get("expiration_time"); if (expirationEpochMilli == null || Instant.ofEpochMilli(expirationEpochMilli).isAfter(clock.instant())) { @@ -263,6 +262,16 @@ static ApiKeyCredentials getCredentialsFromHeader(ThreadContext threadContext) { return null; } + private static boolean verifyKeyAgainstHash(String apiKeyHash, ApiKeyCredentials credentials) { + final char[] apiKeyHashChars = apiKeyHash.toCharArray(); + try { + Hasher hasher = Hasher.resolveFromHash(apiKeyHash.toCharArray()); + return hasher.verify(credentials.getKey(), apiKeyHashChars); + } finally { + Arrays.fill(apiKeyHashChars, (char) 0); + } + } + private Instant getApiKeyExpiration(Instant now, CreateApiKeyRequest request) { if (request.getExpiration() != null) { return now.plusSeconds(request.getExpiration().getSeconds()); From e9c5fdba1b938f49f26352a11ac020510582189a Mon Sep 17 00:00:00 2001 From: jaymode Date: Fri, 9 Nov 2018 08:27:22 -0700 Subject: [PATCH 5/5] don't terminate, fall back to authc chain --- .../elasticsearch/xpack/security/authc/ApiKeyService.java | 8 +++----- .../xpack/security/authc/ApiKeyServiceTests.java | 5 ++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 7bfc446534cf9..69b3121acb638 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -8,7 +8,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.get.GetRequest; @@ -52,7 +51,6 @@ import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; -import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; public class ApiKeyService { @@ -165,8 +163,8 @@ void authenticateWithApiKeyIfPresent(ThreadContext ctx, ActionListener ApiKeyService.getCredentialsFromHeader(threadContext)); + IllegalArgumentException e = + expectThrows(IllegalArgumentException.class, () -> ApiKeyService.getCredentialsFromHeader(threadContext)); assertEquals("invalid ApiKey value", e.getMessage()); } }