Skip to content

Commit 22624cf

Browse files
authored
Integrate api keys with the authentication service (#35555)
This commit integrates usage of the api key service into the authentication service for verification of api keys. A bug was fixed in the validation of api keys where the structure of the document was not being used properly. Additionally, unit tests have been added for authentication with api keys.
1 parent 9133299 commit 22624cf

File tree

5 files changed

+154
-23
lines changed

5 files changed

+154
-23
lines changed

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ Collection<Object> createComponents(Client client, ThreadPool threadPool, Cluste
459459
final AuthenticationFailureHandler failureHandler = createAuthenticationFailureHandler(realms);
460460

461461
authcService.set(new AuthenticationService(settings, realms, auditTrailService, failureHandler, threadPool,
462-
anonymousUser, tokenService));
462+
anonymousUser, tokenService, apiKeyService));
463463
components.add(authcService.get());
464464

465465
final NativePrivilegeStore privilegeStore = new NativePrivilegeStore(settings, client, securityIndex.get());

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,13 @@ void authenticateWithApiKeyIfPresent(ThreadContext ctx, ActionListener<Authentic
178178
}
179179
} else {
180180
credentials.close();
181-
listener.onResponse(AuthenticationResult.unsuccessful("unable to authenticate", null));
181+
listener.onResponse(
182+
AuthenticationResult.unsuccessful("unable to find apikey with id " + credentials.getId(), null));
182183
}
183184
}, e -> {
184185
credentials.close();
185-
listener.onResponse(AuthenticationResult.unsuccessful("apikey auth encountered a failure", e));
186+
listener.onResponse(AuthenticationResult.unsuccessful("apikey authentication for id " + credentials.getId() +
187+
" encountered a failure", e));
186188
}), client::get);
187189
} else {
188190
listener.onResponse(AuthenticationResult.notHandled());
@@ -209,8 +211,9 @@ static void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredenti
209211
if (verified) {
210212
final Long expirationEpochMilli = (Long) source.get("expiration_time");
211213
if (expirationEpochMilli == null || Instant.ofEpochMilli(expirationEpochMilli).isAfter(clock.instant())) {
212-
final String principal = Objects.requireNonNull((String) source.get("principal"));
213-
final Map<String, Object> metadata = (Map<String, Object>) source.get("metadata");
214+
final Map<String, Object> creator = Objects.requireNonNull((Map<String, Object>) source.get("creator"));
215+
final String principal = Objects.requireNonNull((String) creator.get("principal"));
216+
final Map<String, Object> metadata = (Map<String, Object>) creator.get("metadata");
214217
final List<Map<String, Object>> roleDescriptors = (List<Map<String, Object>>) source.get("role_descriptors");
215218
final String[] roleNames = roleDescriptors.stream()
216219
.map(rdSource -> (String) rdSource.get("name"))
@@ -219,7 +222,7 @@ static void validateApiKeyCredentials(Map<String, Object> source, ApiKeyCredenti
219222
final User apiKeyUser = new User(principal, roleNames, null, null, metadata, true);
220223
listener.onResponse(AuthenticationResult.success(apiKeyUser));
221224
} else {
222-
listener.onResponse(AuthenticationResult.unsuccessful("api key is expired", null));
225+
listener.onResponse(AuthenticationResult.terminate("api key is expired", null));
223226
}
224227
} else {
225228
listener.onResponse(AuthenticationResult.unsuccessful("invalid credentials", null));

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,13 @@ public class AuthenticationService {
6161
private final String nodeName;
6262
private final AnonymousUser anonymousUser;
6363
private final TokenService tokenService;
64+
private final ApiKeyService apiKeyService;
6465
private final boolean runAsEnabled;
6566
private final boolean isAnonymousUserEnabled;
6667

6768
public AuthenticationService(Settings settings, Realms realms, AuditTrailService auditTrail,
6869
AuthenticationFailureHandler failureHandler, ThreadPool threadPool,
69-
AnonymousUser anonymousUser, TokenService tokenService) {
70+
AnonymousUser anonymousUser, TokenService tokenService, ApiKeyService apiKeyService) {
7071
this.nodeName = Node.NODE_NAME_SETTING.get(settings);
7172
this.realms = realms;
7273
this.auditTrail = auditTrail;
@@ -76,6 +77,7 @@ public AuthenticationService(Settings settings, Realms realms, AuditTrailService
7677
this.runAsEnabled = AuthenticationServiceField.RUN_AS_ENABLED.get(settings);
7778
this.isAnonymousUserEnabled = AnonymousUser.isAnonymousEnabled(settings);
7879
this.tokenService = tokenService;
80+
this.apiKeyService = apiKeyService;
7981
}
8082

8183
/**
@@ -181,7 +183,7 @@ private void authenticateAsync() {
181183
if (userToken != null) {
182184
writeAuthToContext(userToken.getAuthentication());
183185
} else {
184-
extractToken(this::consumeToken);
186+
checkForApiKey();
185187
}
186188
}, e -> {
187189
if (e instanceof ElasticsearchSecurityException &&
@@ -196,6 +198,31 @@ private void authenticateAsync() {
196198
});
197199
}
198200

201+
private void checkForApiKey() {
202+
apiKeyService.authenticateWithApiKeyIfPresent(threadContext, ActionListener.wrap(authResult -> {
203+
if (authResult.isAuthenticated()) {
204+
final User user = authResult.getUser();
205+
authenticatedBy = new RealmRef("_es_api_key", "_es_api_key", nodeName);
206+
writeAuthToContext(new Authentication(user, authenticatedBy, null));
207+
} else if (authResult.getStatus() == AuthenticationResult.Status.TERMINATE) {
208+
Exception e = (authResult.getException() != null) ? authResult.getException()
209+
: Exceptions.authenticationError(authResult.getMessage());
210+
listener.onFailure(e);
211+
} else {
212+
if (authResult.getMessage() != null) {
213+
if (authResult.getException() != null) {
214+
logger.warn(new ParameterizedMessage("Authentication using apikey failed - {}", authResult.getMessage()),
215+
authResult.getException());
216+
} else {
217+
logger.warn("Authentication using apikey failed - {}", authResult.getMessage());
218+
}
219+
}
220+
extractToken(this::consumeToken);
221+
}
222+
},
223+
e -> listener.onFailure(request.exceptionProcessingRequest(e, null))));
224+
}
225+
199226
/**
200227
* Looks to see if the request contains an existing {@link Authentication} and if so, that authentication will be used. The
201228
* consumer is called if no exception was thrown while trying to read the authentication and may be called with a {@code null}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,11 @@ public void testValidateApiKey() throws Exception {
6969

7070
Map<String, Object> sourceMap = new HashMap<>();
7171
sourceMap.put("api_key_hash", new String(hash));
72-
sourceMap.put("principal", "test_user");
73-
sourceMap.put("metadata", Collections.emptyMap());
7472
sourceMap.put("role_descriptors", Collections.singletonList(Collections.singletonMap("name", "a role")));
75-
73+
Map<String, Object> creatorMap = new HashMap<>();
74+
creatorMap.put("principal", "test_user");
75+
creatorMap.put("metadata", Collections.emptyMap());
76+
sourceMap.put("creator", creatorMap);
7677

7778
ApiKeyService.ApiKeyCredentials creds =
7879
new ApiKeyService.ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(apiKey.toCharArray()));

0 commit comments

Comments
 (0)