Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2739f57
initial commit - tests pending potentially
ankit--sethi Aug 19, 2025
832f469
[CI] Auto commit changes from spotless
Aug 19, 2025
1bd116a
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 19, 2025
aa9b2fb
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 20, 2025
6a8fbb2
fix syntax
ankit--sethi Aug 20, 2025
767c34c
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 20, 2025
e4fa7d0
Merge remote-tracking branch 'origin/feature/session-tokens' into fea…
ankit--sethi Aug 20, 2025
118705f
correct javadoc
ankit--sethi Aug 20, 2025
e466371
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 20, 2025
239d510
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 21, 2025
b174e5a
fix style issue
ankit--sethi Aug 21, 2025
7c3c8a3
Merge remote-tracking branch 'origin/feature/session-tokens' into fea…
ankit--sethi Aug 21, 2025
0f28ac0
fix tests
ankit--sethi Aug 21, 2025
2a03dc9
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 21, 2025
fac7f3b
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 21, 2025
113b4ba
[PoC] Pluggable authenticator chain
slobodanadamovic Aug 22, 2025
41f3714
Merge branch 'main' of github.com:elastic/elasticsearch into poc-cust…
slobodanadamovic Aug 22, 2025
040a9aa
[CI] Auto commit changes from spotless
Aug 22, 2025
b2b6404
spotless + remove unused method
slobodanadamovic Aug 25, 2025
c782a2c
fix javadoc line lenght
slobodanadamovic Aug 25, 2025
b2d3938
Merge branch 'main' of github.com:elastic/elasticsearch into poc-cust…
slobodanadamovic Aug 25, 2025
692d8e3
Merge branch 'main' of github.com:ankit--sethi/elasticsearch into fea…
ankit--sethi Aug 25, 2025
2c74a18
Merge remote-tracking branch 'slobodan/poc-custom-authenticator-chain…
ankit--sethi Aug 25, 2025
cf543eb
refactor with code review feedback and new validation for cloud-saml-…
ankit--sethi Aug 25, 2025
ebd4188
[CI] Auto commit changes from spotless
Aug 25, 2025
f22bf54
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 26, 2025
31b6b56
Merge branch 'main' into feature/session-tokens
ankit--sethi Aug 27, 2025
b7411f2
Merge branch 'main' of github.com:ankit--sethi/elasticsearch into fea…
ankit--sethi Aug 28, 2025
49e4d66
Merge branch 'main' of github.com:ankit--sethi/elasticsearch into fea…
ankit--sethi Aug 28, 2025
01a3f18
Merge branch 'main' of github.com:ankit--sethi/elasticsearch into fea…
ankit--sethi Sep 3, 2025
4a32400
code review stuff
ankit--sethi Sep 3, 2025
14ccac1
Merge branch 'main' into feature/session-tokens
ankit--sethi Sep 3, 2025
2d716c6
[CI] Auto commit changes from spotless
Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.security.authc.AuthenticationFailureHandler;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomTokenAuthenticator;
import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
Expand Down Expand Up @@ -129,7 +129,7 @@ default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents
return null;
}

default CustomApiKeyAuthenticator getCustomApiKeyAuthenticator(SecurityComponents components) {
default List<CustomTokenAuthenticator> getCustomApiKeyAuthenticator(SecurityComponents components) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,17 @@ public static Authentication newRealmAuthentication(User user, RealmRef realmRef
return authentication;
}

public static Authentication newCloudAccessTokenAuthentication(AuthenticationResult<User> authResult, String nodeName) {
assert authResult.isAuthenticated() : "cloud token authn result must be successful";
final User user = authResult.getValue();
// #TODO is this right?
final Authentication.RealmRef authenticatedBy = new RealmRef("cloud-saml-kibana", "saml", nodeName, null);
return new Authentication(
new Subject(user, authenticatedBy, TransportVersion.current(), authResult.getMetadata()),
AuthenticationType.TOKEN
);
}

public static Authentication newCloudApiKeyAuthentication(AuthenticationResult<User> authResult, String nodeName) {
assert authResult.isAuthenticated() : "cloud API Key authn result must be successful";
final User apiKeyUser = authResult.getValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,31 @@
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;

/**
* An extension point to provide a custom API key authenticator implementation.
* The implementation is wrapped by a core `Authenticator` class and included in the authenticator chain _before_ the
* default API key authenticator.
* An extension point to provide a custom token authenticator implementation. For example, a custom API key or a custom OAuth2 token
* implementation. The implementation is wrapped by a core `Authenticator` class and included in the authenticator chain _before_ the
* respective "standard" authenticator(s).
*/
public interface CustomApiKeyAuthenticator {
public interface CustomTokenAuthenticator {

String CLIENT_AUTHENTICATION_HEADER = "X-Client-Authentication";

String name();

AuthenticationToken extractCredentials(@Nullable SecureString apiKeyCredentials);
AuthenticationToken extractCredentials(@Nullable SecureString tokenCredentials);

void authenticate(@Nullable AuthenticationToken authenticationToken, ActionListener<AuthenticationResult<Authentication>> listener);

/**
* A no-op implementation of {@link CustomApiKeyAuthenticator} that is effectively skipped in the authenticator chain.
* A no-op implementation of {@link CustomTokenAuthenticator} that is effectively skipped in the authenticator chain.
*/
class Noop implements CustomApiKeyAuthenticator {
class Noop implements CustomTokenAuthenticator {
@Override
public String name() {
return "noop";
}

@Override
public AuthenticationToken extractCredentials(@Nullable SecureString apiKeyCredentials) {
public AuthenticationToken extractCredentials(@Nullable SecureString tokenCredentials) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
import org.elasticsearch.xpack.core.security.authc.Subject;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomTokenAuthenticator;
import org.elasticsearch.xpack.core.security.authc.service.NodeLocalServiceAccountTokenStore;
import org.elasticsearch.xpack.core.security.authc.service.ServiceAccountTokenStore;
import org.elasticsearch.xpack.core.security.authc.support.UserRoleMapper;
Expand Down Expand Up @@ -1068,9 +1068,9 @@ Collection<Object> createComponents(
operatorPrivilegesService.set(OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE);
}

final CustomApiKeyAuthenticator customApiKeyAuthenticator = createCustomApiKeyAuthenticator(extensionComponents);
final Collection<CustomTokenAuthenticator> customTokenAuthenticator = createCustomApiKeyAuthenticator(extensionComponents);

components.add(customApiKeyAuthenticator);
components.add(customTokenAuthenticator);

authcService.set(
new AuthenticationService(
Expand All @@ -1084,7 +1084,7 @@ Collection<Object> createComponents(
apiKeyService,
serviceAccountService,
operatorPrivilegesService.get(),
customApiKeyAuthenticator,
customTokenAuthenticator,
telemetryProvider.getMeterRegistry()
)
);
Expand Down Expand Up @@ -1220,11 +1220,11 @@ Collection<Object> createComponents(
return components;
}

private CustomApiKeyAuthenticator createCustomApiKeyAuthenticator(SecurityExtension.SecurityComponents extensionComponents) {
final Map<String, CustomApiKeyAuthenticator> customApiKeyAuthenticatorByExtension = new HashMap<>();
private List<CustomTokenAuthenticator> createCustomApiKeyAuthenticator(SecurityExtension.SecurityComponents extensionComponents) {
final Map<String, List<CustomTokenAuthenticator>> customApiKeyAuthenticatorByExtension = new HashMap<>();
for (final SecurityExtension extension : securityExtensions) {
final CustomApiKeyAuthenticator customApiKeyAuthenticator = extension.getCustomApiKeyAuthenticator(extensionComponents);
if (customApiKeyAuthenticator != null) {
final List<CustomTokenAuthenticator> customTokenAuthenticator = extension.getCustomApiKeyAuthenticator(extensionComponents);
if (customTokenAuthenticator != null) {
if (false == isInternalExtension(extension)) {
throw new IllegalStateException(
"The ["
Expand All @@ -1233,16 +1233,16 @@ private CustomApiKeyAuthenticator createCustomApiKeyAuthenticator(SecurityExtens
+ "This functionality is not available to external extensions."
);
}
customApiKeyAuthenticatorByExtension.put(extension.extensionName(), customApiKeyAuthenticator);
customApiKeyAuthenticatorByExtension.put(extension.extensionName(), customTokenAuthenticator);
}
}

if (customApiKeyAuthenticatorByExtension.isEmpty()) {
logger.debug(
"No custom implementation for [{}]. Falling-back to noop implementation.",
CustomApiKeyAuthenticator.class.getCanonicalName()
CustomTokenAuthenticator.class.getCanonicalName()
);
return new CustomApiKeyAuthenticator.Noop();
return List.of(new CustomTokenAuthenticator.Noop());

} else if (customApiKeyAuthenticatorByExtension.size() > 1) {
throw new IllegalStateException(
Expand All @@ -1251,14 +1251,16 @@ private CustomApiKeyAuthenticator createCustomApiKeyAuthenticator(SecurityExtens

} else {
final var authenticatorByExtensionEntry = customApiKeyAuthenticatorByExtension.entrySet().iterator().next();
final CustomApiKeyAuthenticator customApiKeyAuthenticator = authenticatorByExtensionEntry.getValue();
final List<CustomTokenAuthenticator> customTokenAuthenticators = authenticatorByExtensionEntry.getValue();
final String extensionName = authenticatorByExtensionEntry.getKey();
logger.debug(
"CustomApiKeyAuthenticator implementation [{}] provided by extension [{}]",
customApiKeyAuthenticator.getClass().getCanonicalName(),
extensionName
);
return customApiKeyAuthenticator;
for (CustomTokenAuthenticator authenticator : customTokenAuthenticators) {
logger.debug(
"CustomApiKeyAuthenticator implementation [{}] provided by extension [{}]",
authenticator.getClass().getCanonicalName(),
extensionName
);
}
return customTokenAuthenticators;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security.authc;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomTokenAuthenticator;

public abstract class AbstractPluggableAuthenticator implements Authenticator {

@Override
public String name() {
return getAuthenticator().name();
}

public void authenticate(Authenticator.Context context, ActionListener<AuthenticationResult<Authentication>> listener) {
final AuthenticationToken authenticationToken = context.getMostRecentAuthenticationToken();
getAuthenticator().authenticate(authenticationToken, ActionListener.wrap(response -> {
if (response.isAuthenticated()) {
listener.onResponse(response);
} else if (response.getStatus() == AuthenticationResult.Status.TERMINATE) {
final Exception ex = response.getException();
if (ex == null) {
listener.onFailure(context.getRequest().authenticationFailed(authenticationToken));
} else {
listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, authenticationToken));
}
} else if (response.getStatus() == AuthenticationResult.Status.CONTINUE) {
listener.onResponse(AuthenticationResult.notHandled());
}
}, ex -> listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, authenticationToken))));
}

public abstract CustomTokenAuthenticator getAuthenticator();
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import org.elasticsearch.xpack.core.security.authc.AuthenticationServiceField;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.Realm;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomTokenAuthenticator;
import org.elasticsearch.xpack.core.security.authc.support.AuthenticationContextSerializer;
import org.elasticsearch.xpack.core.security.authz.AuthorizationEngine.EmptyAuthorizationInfo;
import org.elasticsearch.xpack.core.security.user.AnonymousUser;
Expand All @@ -42,6 +42,7 @@
import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService;
import org.elasticsearch.xpack.security.support.SecurityIndexManager;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -93,7 +94,7 @@ public AuthenticationService(
ApiKeyService apiKeyService,
ServiceAccountService serviceAccountService,
OperatorPrivilegesService operatorPrivilegesService,
CustomApiKeyAuthenticator customApiKeyAuthenticator,
Collection<CustomTokenAuthenticator> customTokenAuthenticators,
MeterRegistry meterRegistry
) {
this.realms = realms;
Expand All @@ -110,14 +111,23 @@ public AuthenticationService(
}

final String nodeName = Node.NODE_NAME_SETTING.get(settings);
CustomTokenAuthenticator oauth2Authenticator = customTokenAuthenticators.stream()
.filter(t -> t.name().contains("oauth2"))
.findAny()
.orElse(new CustomTokenAuthenticator.Noop());
CustomTokenAuthenticator apiKeyAuthenticator = customTokenAuthenticators.stream()
.filter(t -> t.name().contains("api key"))
.findAny()
.orElse(new CustomTokenAuthenticator.Noop());
this.authenticatorChain = new AuthenticatorChain(
settings,
operatorPrivilegesService,
anonymousUser,
new AuthenticationContextSerializer(),
new ServiceAccountAuthenticator(serviceAccountService, nodeName, meterRegistry),
new PluggableOAuth2TokenAuthenticator(oauth2Authenticator),
new OAuth2TokenAuthenticator(tokenService, meterRegistry),
new PluggableApiKeyAuthenticator(customApiKeyAuthenticator),
new PluggableApiKeyAuthenticator(apiKeyAuthenticator),
new ApiKeyAuthenticator(apiKeyService, nodeName, meterRegistry),
new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache, meterRegistry)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class AuthenticatorChain {
AnonymousUser anonymousUser,
AuthenticationContextSerializer authenticationSerializer,
ServiceAccountAuthenticator serviceAccountAuthenticator,
PluggableOAuth2TokenAuthenticator pluggableOAuth2TokenAuthenticator,
OAuth2TokenAuthenticator oAuth2TokenAuthenticator,
PluggableApiKeyAuthenticator pluggableApiKeyAuthenticator,
ApiKeyAuthenticator apiKeyAuthenticator,
Expand All @@ -67,6 +68,7 @@ class AuthenticatorChain {
this.realmsAuthenticator = realmsAuthenticator;
this.allAuthenticators = List.of(
serviceAccountAuthenticator,
pluggableOAuth2TokenAuthenticator,
oAuth2TokenAuthenticator,
pluggableApiKeyAuthenticator,
apiKeyAuthenticator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,50 +7,34 @@

package org.elasticsearch.xpack.security.authc;

import org.elasticsearch.action.ActionListener;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomApiKeyAuthenticator;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomTokenAuthenticator;

/**
* An adapter for {@link CustomApiKeyAuthenticator} that implements the {@link Authenticator} interface, so the custom API key authenticator
* An adapter for {@link CustomTokenAuthenticator} that implements the {@link Authenticator} interface, so the custom API key authenticator
* can be plugged into the authenticator chain. Module dependencies prevent us from introducing a direct extension point for
* an {@link Authenticator}.
*/
public class PluggableApiKeyAuthenticator implements Authenticator {
private final CustomApiKeyAuthenticator authenticator;
public class PluggableApiKeyAuthenticator extends AbstractPluggableAuthenticator {
private final CustomTokenAuthenticator apiKeyAuthenticator;

public PluggableApiKeyAuthenticator(CustomApiKeyAuthenticator authenticator) {
this.authenticator = authenticator;
public PluggableApiKeyAuthenticator(CustomTokenAuthenticator apiKeyAuthenticator) {
this.apiKeyAuthenticator = apiKeyAuthenticator;
}

@Override
public String name() {
return authenticator.name();
return apiKeyAuthenticator.name();
}

@Override
public AuthenticationToken extractCredentials(Context context) {
return authenticator.extractCredentials(context.getApiKeyString());
public AuthenticationToken extractCredentials(Authenticator.Context context) {
return apiKeyAuthenticator.extractCredentials(context.getApiKeyString());

}

@Override
public void authenticate(Context context, ActionListener<AuthenticationResult<Authentication>> listener) {
final AuthenticationToken authenticationToken = context.getMostRecentAuthenticationToken();
authenticator.authenticate(authenticationToken, ActionListener.wrap(response -> {
if (response.isAuthenticated()) {
listener.onResponse(response);
} else if (response.getStatus() == AuthenticationResult.Status.TERMINATE) {
final Exception ex = response.getException();
if (ex == null) {
listener.onFailure(context.getRequest().authenticationFailed(authenticationToken));
} else {
listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, authenticationToken));
}
} else if (response.getStatus() == AuthenticationResult.Status.CONTINUE) {
listener.onResponse(AuthenticationResult.notHandled());
}
}, ex -> listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, authenticationToken))));
public CustomTokenAuthenticator getAuthenticator() {
return apiKeyAuthenticator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security.authc;

import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.core.security.authc.apikey.CustomTokenAuthenticator;

/**
* An adapter for {@link CustomTokenAuthenticator} that implements the {@link Authenticator} interface, so the custom API key authenticator
* can be plugged into the authenticator chain. Module dependencies prevent us from introducing a direct extension point for
* an {@link Authenticator}.
*/
public class PluggableOAuth2TokenAuthenticator extends AbstractPluggableAuthenticator {

private CustomTokenAuthenticator customOAuth2TokenAuthenticator;

public PluggableOAuth2TokenAuthenticator(CustomTokenAuthenticator authenticator) {
this.customOAuth2TokenAuthenticator = authenticator;
}

@Override
public AuthenticationToken extractCredentials(Context context) {
return customOAuth2TokenAuthenticator.extractCredentials(context.getBearerString());
}

@Override
public CustomTokenAuthenticator getAuthenticator() {
return customOAuth2TokenAuthenticator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public class AuthenticatorChainTests extends ESTestCase {
private OAuth2TokenAuthenticator oAuth2TokenAuthenticator;
private ApiKeyAuthenticator apiKeyAuthenticator;
private PluggableApiKeyAuthenticator pluggableApiKeyAuthenticator;
private PluggableOAuth2TokenAuthenticator pluggableOAuth2TokenAuthenticator;
private RealmsAuthenticator realmsAuthenticator;
private Authentication authentication;
private User fallbackUser;
Expand Down Expand Up @@ -104,6 +105,7 @@ public void init() {
anonymousUser,
authenticationContextSerializer,
serviceAccountAuthenticator,
pluggableOAuth2TokenAuthenticator,
oAuth2TokenAuthenticator,
pluggableApiKeyAuthenticator,
apiKeyAuthenticator,
Expand Down