From 2739f57ef89e7a4e10401fd597bed82e28fd63bd Mon Sep 17 00:00:00 2001 From: ankitsethi Date: Mon, 18 Aug 2025 23:18:58 -0500 Subject: [PATCH 1/8] initial commit - tests pending potentially --- .../core/security/SecurityExtension.java | 4 +- .../core/security/authc/Authentication.java | 11 +++++ ...tor.java => CustomTokenAuthenticator.java} | 13 +++--- .../xpack/security/Security.java | 38 +++++++++--------- .../authc/AbstractPluggableAuthenticator.java | 35 ++++++++++++++++ .../security/authc/AuthenticationService.java | 18 +++++++-- .../security/authc/AuthenticatorChain.java | 2 + .../authc/PluggableApiKeyAuthenticator.java | 40 ++++++------------- .../PluggableOAuth2TokenAuthenticator.java | 23 +++++++++++ 9 files changed, 128 insertions(+), 56 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/{CustomApiKeyAuthenticator.java => CustomTokenAuthenticator.java} (82%) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index f41b19de95272..eaf74d672a1e9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -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; @@ -129,7 +129,7 @@ default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents return null; } - default CustomApiKeyAuthenticator getCustomApiKeyAuthenticator(SecurityComponents components) { + default List getCustomApiKeyAuthenticator(SecurityComponents components) { return null; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index 20a02139aa17e..f2c9faf112358 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -1376,6 +1376,17 @@ public static Authentication newRealmAuthentication(User user, RealmRef realmRef return authentication; } + public static Authentication newCloudAccessTokenAuthentication(AuthenticationResult 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 authResult, String nodeName) { assert authResult.isAuthenticated() : "cloud API Key authn result must be successful"; final User apiKeyUser = authResult.getValue(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java similarity index 82% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java index 4f5d05e720715..f410fd0d708ea 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomApiKeyAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java @@ -19,24 +19,27 @@ * The implementation is wrapped by a core `Authenticator` class and included in the authenticator chain _before_ the * default API key authenticator. */ -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> 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; } 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 a82200aadac2d..bf56c46075039 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 @@ -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; @@ -1068,9 +1068,9 @@ Collection createComponents( operatorPrivilegesService.set(OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); } - final CustomApiKeyAuthenticator customApiKeyAuthenticator = createCustomApiKeyAuthenticator(extensionComponents); + final Collection customTokenAuthenticator = createCustomApiKeyAuthenticator(extensionComponents); - components.add(customApiKeyAuthenticator); + components.add(customTokenAuthenticator); authcService.set( new AuthenticationService( @@ -1084,7 +1084,7 @@ Collection createComponents( apiKeyService, serviceAccountService, operatorPrivilegesService.get(), - customApiKeyAuthenticator, + customTokenAuthenticator, telemetryProvider.getMeterRegistry() ) ); @@ -1220,11 +1220,11 @@ Collection createComponents( return components; } - private CustomApiKeyAuthenticator createCustomApiKeyAuthenticator(SecurityExtension.SecurityComponents extensionComponents) { - final Map customApiKeyAuthenticatorByExtension = new HashMap<>(); + private List createCustomApiKeyAuthenticator(SecurityExtension.SecurityComponents extensionComponents) { + final Map> customApiKeyAuthenticatorByExtension = new HashMap<>(); for (final SecurityExtension extension : securityExtensions) { - final CustomApiKeyAuthenticator customApiKeyAuthenticator = extension.getCustomApiKeyAuthenticator(extensionComponents); - if (customApiKeyAuthenticator != null) { + final List customTokenAuthenticator = extension.getCustomApiKeyAuthenticator(extensionComponents); + if (customTokenAuthenticator != null) { if (false == isInternalExtension(extension)) { throw new IllegalStateException( "The [" @@ -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( @@ -1251,14 +1251,16 @@ private CustomApiKeyAuthenticator createCustomApiKeyAuthenticator(SecurityExtens } else { final var authenticatorByExtensionEntry = customApiKeyAuthenticatorByExtension.entrySet().iterator().next(); - final CustomApiKeyAuthenticator customApiKeyAuthenticator = authenticatorByExtensionEntry.getValue(); + final List 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; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java new file mode 100644 index 0000000000000..335f823d42100 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java @@ -0,0 +1,35 @@ +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> 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(); +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index a9c513a605fe8..12ee0a0f017c4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -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; @@ -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; @@ -93,7 +94,7 @@ public AuthenticationService( ApiKeyService apiKeyService, ServiceAccountService serviceAccountService, OperatorPrivilegesService operatorPrivilegesService, - CustomApiKeyAuthenticator customApiKeyAuthenticator, + Collection customTokenAuthenticators, MeterRegistry meterRegistry ) { this.realms = realms; @@ -110,14 +111,25 @@ public AuthenticationService( } final String nodeName = Node.NODE_NAME_SETTING.get(settings); + CustomTokenAuthenticator oauth2Authenticator = customTokenAuthenticators + .stream() + .filter(t -> t.name().contains("oauth2") || t instanceof CustomTokenAuthenticator.Noop) + .findAny() + .orElseThrow(); + CustomTokenAuthenticator apiKeyAuthenticator = customTokenAuthenticators + .stream() + .filter(t -> t.name().contains("api key") || t instanceof CustomTokenAuthenticator.Noop) + .findAny() + .orElseThrow(); 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) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java index f3532dc4c6270..c00571c10baf8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java @@ -53,6 +53,7 @@ class AuthenticatorChain { AnonymousUser anonymousUser, AuthenticationContextSerializer authenticationSerializer, ServiceAccountAuthenticator serviceAccountAuthenticator, + PluggableOAuth2TokenAuthenticator pluggableOAuth2TokenAuthenticator, OAuth2TokenAuthenticator oAuth2TokenAuthenticator, PluggableApiKeyAuthenticator pluggableApiKeyAuthenticator, ApiKeyAuthenticator apiKeyAuthenticator, @@ -67,6 +68,7 @@ class AuthenticatorChain { this.realmsAuthenticator = realmsAuthenticator; this.allAuthenticators = List.of( serviceAccountAuthenticator, + pluggableOAuth2TokenAuthenticator, oAuth2TokenAuthenticator, pluggableApiKeyAuthenticator, apiKeyAuthenticator, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java index 0637efbc5e89a..e8d60e49f1915 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java @@ -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> 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; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java new file mode 100644 index 0000000000000..14e1b3622c4f5 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java @@ -0,0 +1,23 @@ +package org.elasticsearch.xpack.security.authc; + +import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomTokenAuthenticator; + +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; + } +} From 832f4699ef857d98e83e7331c95ed151572dc7d7 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 19 Aug 2025 04:32:45 +0000 Subject: [PATCH 2/8] [CI] Auto commit changes from spotless --- .../xpack/core/security/authc/Authentication.java | 2 +- .../xpack/security/authc/AuthenticationService.java | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index f2c9faf112358..e6c8ddaadb5d8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -1379,7 +1379,7 @@ public static Authentication newRealmAuthentication(User user, RealmRef realmRef public static Authentication newCloudAccessTokenAuthentication(AuthenticationResult authResult, String nodeName) { assert authResult.isAuthenticated() : "cloud token authn result must be successful"; final User user = authResult.getValue(); - //#TODO is this right? + // #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()), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 12ee0a0f017c4..62c80a76116b8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -111,13 +111,11 @@ public AuthenticationService( } final String nodeName = Node.NODE_NAME_SETTING.get(settings); - CustomTokenAuthenticator oauth2Authenticator = customTokenAuthenticators - .stream() + CustomTokenAuthenticator oauth2Authenticator = customTokenAuthenticators.stream() .filter(t -> t.name().contains("oauth2") || t instanceof CustomTokenAuthenticator.Noop) .findAny() .orElseThrow(); - CustomTokenAuthenticator apiKeyAuthenticator = customTokenAuthenticators - .stream() + CustomTokenAuthenticator apiKeyAuthenticator = customTokenAuthenticators.stream() .filter(t -> t.name().contains("api key") || t instanceof CustomTokenAuthenticator.Noop) .findAny() .orElseThrow(); From 6a8fbb2a9ab80112a0b6a8a25fdbb8d0072cb755 Mon Sep 17 00:00:00 2001 From: ankitsethi Date: Wed, 20 Aug 2025 14:38:19 -0500 Subject: [PATCH 3/8] fix syntax --- .../xpack/security/authc/AuthenticatorChainTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java index bfd122655768b..da56e7d30806a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java @@ -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; @@ -104,6 +105,7 @@ public void init() { anonymousUser, authenticationContextSerializer, serviceAccountAuthenticator, + pluggableOAuth2TokenAuthenticator, oAuth2TokenAuthenticator, pluggableApiKeyAuthenticator, apiKeyAuthenticator, From 118705fa0122b5e6a0d68f785550e8313ec8c5d8 Mon Sep 17 00:00:00 2001 From: ankitsethi Date: Wed, 20 Aug 2025 15:23:25 -0500 Subject: [PATCH 4/8] correct javadoc --- .../authc/apikey/CustomTokenAuthenticator.java | 4 ++-- .../authc/AbstractPluggableAuthenticator.java | 7 +++++++ .../authc/PluggableOAuth2TokenAuthenticator.java | 12 ++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java index f410fd0d708ea..ff5ca8e4cda87 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java @@ -15,9 +15,9 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; /** - * An extension point to provide a custom API key authenticator implementation. + * 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 - * default API key authenticator. + * respective "standard" authenticator(s). */ public interface CustomTokenAuthenticator { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java index 335f823d42100..cc080d628e637 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java @@ -1,3 +1,10 @@ +/* + * 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; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java index 14e1b3622c4f5..69c1b9230d01a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java @@ -1,8 +1,20 @@ +/* + * 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; From 113b4bade448982ddc7a94b07bd36a2ac4d1a8c9 Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Fri, 22 Aug 2025 16:04:21 +0200 Subject: [PATCH 5/8] [PoC] Pluggable authenticator chain --- .../core/security/SecurityExtension.java | 4 +- .../authc/apikey/CustomAuthenticator.java | 32 ++++++++ .../apikey/CustomTokenAuthenticator.java | 54 ------------ .../xpack/security/Security.java | 53 ++++++------ .../authc/AbstractPluggableAuthenticator.java | 42 ---------- .../security/authc/AuthenticationService.java | 16 +--- .../security/authc/AuthenticatorChain.java | 23 +++--- .../authc/PluggableApiKeyAuthenticator.java | 40 --------- .../authc/PluggableAuthenticatorChain.java | 82 +++++++++++++++++++ .../PluggableOAuth2TokenAuthenticator.java | 35 -------- .../authc/AuthenticatorChainTests.java | 29 +------ 11 files changed, 162 insertions(+), 248 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java delete mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java delete mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java index eaf74d672a1e9..449246fbb5c92 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java @@ -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.CustomTokenAuthenticator; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomAuthenticator; 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; @@ -129,7 +129,7 @@ default ServiceAccountTokenStore getServiceAccountTokenStore(SecurityComponents return null; } - default List getCustomApiKeyAuthenticator(SecurityComponents components) { + default List getCustomAuthenticators(SecurityComponents components) { return null; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java new file mode 100644 index 0000000000000..2d873218afb69 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java @@ -0,0 +1,32 @@ +/* + * 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.core.security.authc.apikey; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.Nullable; +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.user.User; + +/** + * 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 CustomAuthenticator { + + boolean supports(AuthenticationToken token); + + @Nullable + AuthenticationToken extractToken(ThreadContext context); + + void authenticate(@Nullable AuthenticationToken token, ActionListener> listener); + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java deleted file mode 100644 index ff5ca8e4cda87..0000000000000 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomTokenAuthenticator.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.core.security.authc.apikey; - -import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.core.Nullable; -import org.elasticsearch.xpack.core.security.authc.Authentication; -import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; -import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; - -/** - * 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 CustomTokenAuthenticator { - - String CLIENT_AUTHENTICATION_HEADER = "X-Client-Authentication"; - - String name(); - - AuthenticationToken extractCredentials(@Nullable SecureString tokenCredentials); - - void authenticate(@Nullable AuthenticationToken authenticationToken, ActionListener> listener); - - /** - * A no-op implementation of {@link CustomTokenAuthenticator} that is effectively skipped in the authenticator chain. - */ - class Noop implements CustomTokenAuthenticator { - @Override - public String name() { - return "noop"; - } - - @Override - public AuthenticationToken extractCredentials(@Nullable SecureString tokenCredentials) { - return null; - } - - @Override - public void authenticate( - @Nullable AuthenticationToken authenticationToken, - ActionListener> listener - ) { - listener.onResponse(AuthenticationResult.notHandled()); - } - } -} 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 3448427ccaccf..7a4a80dcfc412 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 @@ -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.CustomTokenAuthenticator; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomAuthenticator; 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; @@ -1068,9 +1068,7 @@ Collection createComponents( operatorPrivilegesService.set(OperatorPrivileges.NOOP_OPERATOR_PRIVILEGES_SERVICE); } - final Collection customTokenAuthenticator = createCustomApiKeyAuthenticator(extensionComponents); - - components.add(customTokenAuthenticator); + final List customAuthenticators = getCustomAuthenticatorFromExtensions(extensionComponents); authcService.set( new AuthenticationService( @@ -1084,7 +1082,7 @@ Collection createComponents( apiKeyService, serviceAccountService, operatorPrivilegesService.get(), - customTokenAuthenticator, + customAuthenticators, telemetryProvider.getMeterRegistry() ) ); @@ -1220,47 +1218,48 @@ Collection createComponents( return components; } - private List createCustomApiKeyAuthenticator(SecurityExtension.SecurityComponents extensionComponents) { - final Map> customApiKeyAuthenticatorByExtension = new HashMap<>(); - for (final SecurityExtension extension : securityExtensions) { - final List customTokenAuthenticator = extension.getCustomApiKeyAuthenticator(extensionComponents); - if (customTokenAuthenticator != null) { - if (false == isInternalExtension(extension)) { + private List getCustomAuthenticatorFromExtensions(SecurityExtension.SecurityComponents extensionComponents) { + final Map> customAuthenticatorsByExtension = new HashMap<>(); + for (final SecurityExtension securityExtension : securityExtensions) { + final List customAuthenticators = securityExtension.getCustomAuthenticators(extensionComponents); + if (customAuthenticators != null) { + if (false == isInternalExtension(securityExtension)) { throw new IllegalStateException( "The [" - + extension.extensionName() - + "] extension tried to install a custom CustomApiKeyAuthenticator. " + + securityExtension.extensionName() + + "] extension tried to install a " + + CustomAuthenticator.class.getSimpleName() + + ". " + "This functionality is not available to external extensions." ); } - customApiKeyAuthenticatorByExtension.put(extension.extensionName(), customTokenAuthenticator); + customAuthenticatorsByExtension.put(securityExtension.extensionName(), customAuthenticators); } } - if (customApiKeyAuthenticatorByExtension.isEmpty()) { + if (customAuthenticatorsByExtension.isEmpty()) { logger.debug( - "No custom implementation for [{}]. Falling-back to noop implementation.", - CustomTokenAuthenticator.class.getCanonicalName() + "No custom implementations for [{}] provided by security extensions.", + CustomAuthenticator.class.getCanonicalName() ); - return List.of(new CustomTokenAuthenticator.Noop()); - - } else if (customApiKeyAuthenticatorByExtension.size() > 1) { + return List.of(); + } else if (customAuthenticatorsByExtension.size() > 1) { throw new IllegalStateException( - "Multiple extensions tried to install a custom CustomApiKeyAuthenticator: " + customApiKeyAuthenticatorByExtension.keySet() + "Multiple extensions tried to install custom authenticators: " + customAuthenticatorsByExtension.keySet() ); - } else { - final var authenticatorByExtensionEntry = customApiKeyAuthenticatorByExtension.entrySet().iterator().next(); - final List customTokenAuthenticators = authenticatorByExtensionEntry.getValue(); + final var authenticatorByExtensionEntry = customAuthenticatorsByExtension.entrySet().iterator().next(); + final List customAuthenticators = authenticatorByExtensionEntry.getValue(); final String extensionName = authenticatorByExtensionEntry.getKey(); - for (CustomTokenAuthenticator authenticator : customTokenAuthenticators) { + for (CustomAuthenticator authenticator : customAuthenticators) { logger.debug( - "CustomApiKeyAuthenticator implementation [{}] provided by extension [{}]", + "{} implementation [{}] provided by extension [{}]", + CustomAuthenticator.class.getSimpleName(), authenticator.getClass().getCanonicalName(), extensionName ); } - return customTokenAuthenticators; + return customAuthenticators; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java deleted file mode 100644 index cc080d628e637..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AbstractPluggableAuthenticator.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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> 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(); -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index 62c80a76116b8..cb34d4cf9b450 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -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.CustomTokenAuthenticator; +import org.elasticsearch.xpack.core.security.authc.apikey.CustomAuthenticator; 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; @@ -94,7 +94,7 @@ public AuthenticationService( ApiKeyService apiKeyService, ServiceAccountService serviceAccountService, OperatorPrivilegesService operatorPrivilegesService, - Collection customTokenAuthenticators, + List customAuthenticators, MeterRegistry meterRegistry ) { this.realms = realms; @@ -111,23 +111,15 @@ public AuthenticationService( } final String nodeName = Node.NODE_NAME_SETTING.get(settings); - CustomTokenAuthenticator oauth2Authenticator = customTokenAuthenticators.stream() - .filter(t -> t.name().contains("oauth2") || t instanceof CustomTokenAuthenticator.Noop) - .findAny() - .orElseThrow(); - CustomTokenAuthenticator apiKeyAuthenticator = customTokenAuthenticators.stream() - .filter(t -> t.name().contains("api key") || t instanceof CustomTokenAuthenticator.Noop) - .findAny() - .orElseThrow(); + this.authenticatorChain = new AuthenticatorChain( settings, operatorPrivilegesService, anonymousUser, new AuthenticationContextSerializer(), + new PluggableAuthenticatorChain(customAuthenticators), new ServiceAccountAuthenticator(serviceAccountService, nodeName, meterRegistry), - new PluggableOAuth2TokenAuthenticator(oauth2Authenticator), new OAuth2TokenAuthenticator(tokenService, meterRegistry), - new PluggableApiKeyAuthenticator(apiKeyAuthenticator), new ApiKeyAuthenticator(apiKeyService, nodeName, meterRegistry), new RealmsAuthenticator(numInvalidation, lastSuccessfulAuthCache, meterRegistry) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java index c00571c10baf8..a87225c9f7682 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticatorChain.java @@ -26,6 +26,8 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; @@ -52,10 +54,9 @@ class AuthenticatorChain { OperatorPrivilegesService operatorPrivilegesService, AnonymousUser anonymousUser, AuthenticationContextSerializer authenticationSerializer, + PluggableAuthenticatorChain pluggableAuthenticatorChain, ServiceAccountAuthenticator serviceAccountAuthenticator, - PluggableOAuth2TokenAuthenticator pluggableOAuth2TokenAuthenticator, OAuth2TokenAuthenticator oAuth2TokenAuthenticator, - PluggableApiKeyAuthenticator pluggableApiKeyAuthenticator, ApiKeyAuthenticator apiKeyAuthenticator, RealmsAuthenticator realmsAuthenticator ) { @@ -66,14 +67,16 @@ class AuthenticatorChain { this.isAnonymousUserEnabled = AnonymousUser.isAnonymousEnabled(settings); this.authenticationSerializer = authenticationSerializer; this.realmsAuthenticator = realmsAuthenticator; - this.allAuthenticators = List.of( - serviceAccountAuthenticator, - pluggableOAuth2TokenAuthenticator, - oAuth2TokenAuthenticator, - pluggableApiKeyAuthenticator, - apiKeyAuthenticator, - realmsAuthenticator - ); + + List authenticators = new ArrayList<>(); + if (pluggableAuthenticatorChain.hasCustomAuthenticators()) { + authenticators.add(pluggableAuthenticatorChain); + } + authenticators.add(serviceAccountAuthenticator); + authenticators.add(oAuth2TokenAuthenticator); + authenticators.add(apiKeyAuthenticator); + authenticators.add(realmsAuthenticator); + this.allAuthenticators = Collections.unmodifiableList(authenticators); } void authenticate(Authenticator.Context context, ActionListener originalListener) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java deleted file mode 100644 index e8d60e49f1915..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableApiKeyAuthenticator.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 PluggableApiKeyAuthenticator extends AbstractPluggableAuthenticator { - private final CustomTokenAuthenticator apiKeyAuthenticator; - - public PluggableApiKeyAuthenticator(CustomTokenAuthenticator apiKeyAuthenticator) { - this.apiKeyAuthenticator = apiKeyAuthenticator; - } - - @Override - public String name() { - return apiKeyAuthenticator.name(); - } - - @Override - public AuthenticationToken extractCredentials(Authenticator.Context context) { - return apiKeyAuthenticator.extractCredentials(context.getApiKeyString()); - - } - - @Override - public CustomTokenAuthenticator getAuthenticator() { - return apiKeyAuthenticator; - } -} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java new file mode 100644 index 0000000000000..9780b4ae43f93 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableAuthenticatorChain.java @@ -0,0 +1,82 @@ +/* + * 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.CustomAuthenticator; + +import java.util.List; + +public class PluggableAuthenticatorChain implements Authenticator { + + public static final PluggableAuthenticatorChain EMPTY = new PluggableAuthenticatorChain(List.of()); + + private final List customAuthenticators; + + public PluggableAuthenticatorChain(List customAuthenticators) { + this.customAuthenticators = customAuthenticators; + } + + @Override + public String name() { + return "pluggable custom authenticator chain"; + } + + public boolean hasCustomAuthenticators() { + return customAuthenticators.size() > 0; + } + + @Override + public AuthenticationToken extractCredentials(Context context) { + if (false == hasCustomAuthenticators()) { + return null; + } + for (CustomAuthenticator customAuthenticator : customAuthenticators) { + AuthenticationToken token = customAuthenticator.extractToken(context.getThreadContext()); + if (token != null) { + return token; + } + } + return null; + } + + @Override + public void authenticate(Context context, ActionListener> listener) { + if (false == hasCustomAuthenticators()) { + listener.onResponse(AuthenticationResult.notHandled()); + return; + } + AuthenticationToken token = context.getMostRecentAuthenticationToken(); + if (token != null) { + for (CustomAuthenticator customAuthenticator : customAuthenticators) { + if (customAuthenticator.supports(token)) { + customAuthenticator.authenticate(token, 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(token)); + } else { + listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, token)); + } + } else if (response.getStatus() == AuthenticationResult.Status.CONTINUE) { + listener.onResponse(AuthenticationResult.notHandled()); + } + }, ex -> listener.onFailure(context.getRequest().exceptionProcessingRequest(ex, token)))); + return; + } + } + } + listener.onResponse(AuthenticationResult.notHandled()); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java deleted file mode 100644 index 69c1b9230d01a..0000000000000 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/PluggableOAuth2TokenAuthenticator.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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; - } -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java index da56e7d30806a..ce5ca298a9d75 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticatorChainTests.java @@ -67,8 +67,6 @@ public class AuthenticatorChainTests extends ESTestCase { private ServiceAccountAuthenticator serviceAccountAuthenticator; private OAuth2TokenAuthenticator oAuth2TokenAuthenticator; private ApiKeyAuthenticator apiKeyAuthenticator; - private PluggableApiKeyAuthenticator pluggableApiKeyAuthenticator; - private PluggableOAuth2TokenAuthenticator pluggableOAuth2TokenAuthenticator; private RealmsAuthenticator realmsAuthenticator; private Authentication authentication; private User fallbackUser; @@ -93,7 +91,8 @@ public void init() { oAuth2TokenAuthenticator = mock(OAuth2TokenAuthenticator.class); apiKeyAuthenticator = mock(ApiKeyAuthenticator.class); realmsAuthenticator = mock(RealmsAuthenticator.class); - pluggableApiKeyAuthenticator = mock(PluggableApiKeyAuthenticator.class); + PluggableAuthenticatorChain pluggableAuthenticatorChain = PluggableAuthenticatorChain.EMPTY; + when(realms.getActiveRealms()).thenReturn(List.of(mock(Realm.class))); when(realms.getUnlicensedRealms()).thenReturn(List.of()); final User user = new User(randomAlphaOfLength(8)); @@ -104,10 +103,9 @@ public void init() { operatorPrivilegesService, anonymousUser, authenticationContextSerializer, + pluggableAuthenticatorChain, serviceAccountAuthenticator, - pluggableOAuth2TokenAuthenticator, oAuth2TokenAuthenticator, - pluggableApiKeyAuthenticator, apiKeyAuthenticator, realmsAuthenticator ); @@ -222,13 +220,6 @@ public void testAuthenticateWithApiKey() throws IOException { new ApiKeyCredentials(randomAlphaOfLength(20), apiKeySecret, randomFrom(ApiKey.Type.values())) ); doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); - doAnswer(invocationOnMock -> { - @SuppressWarnings("unchecked") - final ActionListener> listener = (ActionListener< - AuthenticationResult>) invocationOnMock.getArguments()[1]; - listener.onResponse(AuthenticationResult.notHandled()); - return null; - }).when(pluggableApiKeyAuthenticator).authenticate(eq(context), any()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); } doAnswer(invocationOnMock -> { @@ -271,13 +262,6 @@ public void testAuthenticateWithRealms() throws IOException { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); - doAnswer(invocationOnMock -> { - @SuppressWarnings("unchecked") - final ActionListener> listener = (ActionListener< - AuthenticationResult>) invocationOnMock.getArguments()[1]; - listener.onResponse(AuthenticationResult.notHandled()); - return null; - }).when(pluggableApiKeyAuthenticator).authenticate(eq(context), any()); } doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") @@ -340,13 +324,6 @@ public void testContextWithDirectWrongTokenFailsAuthn() { doCallRealMethod().when(serviceAccountAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(oAuth2TokenAuthenticator).authenticate(eq(context), anyActionListener()); doCallRealMethod().when(apiKeyAuthenticator).authenticate(eq(context), anyActionListener()); - doAnswer(invocationOnMock -> { - @SuppressWarnings("unchecked") - final ActionListener> listener = (ActionListener< - AuthenticationResult>) invocationOnMock.getArguments()[1]; - listener.onResponse(AuthenticationResult.notHandled()); - return null; - }).when(pluggableApiKeyAuthenticator).authenticate(eq(context), any()); // 1. realms do not consume the token doAnswer(invocationOnMock -> { From 040a9aa9e067bcc4fa04e46e69d43ffc37c913c0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 22 Aug 2025 14:28:47 +0000 Subject: [PATCH 6/8] [CI] Auto commit changes from spotless --- .../xpack/core/security/authc/apikey/CustomAuthenticator.java | 1 - .../xpack/security/authc/AuthenticationService.java | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java index 2d873218afb69..39f5b22357b73 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java @@ -13,7 +13,6 @@ 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.user.User; /** * An extension point to provide a custom token authenticator implementation. For example, a custom API key or a custom OAuth2 token implementation. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java index cb34d4cf9b450..5fb930ceb3324 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/AuthenticationService.java @@ -42,7 +42,6 @@ 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; From b2b640412cbe158fbd440a0bf1c47f1e7e3f1e0a Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 25 Aug 2025 09:37:25 +0200 Subject: [PATCH 7/8] spotless + remove unused method --- .../xpack/core/security/authc/Authentication.java | 11 ----------- .../xpack/security/authc/Authenticator.java | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java index e6c8ddaadb5d8..20a02139aa17e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java @@ -1376,17 +1376,6 @@ public static Authentication newRealmAuthentication(User user, RealmRef realmRef return authentication; } - public static Authentication newCloudAccessTokenAuthentication(AuthenticationResult 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 authResult, String nodeName) { assert authResult.isAuthenticated() : "cloud API Key authn result must be successful"; final User apiKeyUser = authResult.getValue(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Authenticator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Authenticator.java index ed218c3eba58d..6433cfacc77bb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Authenticator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Authenticator.java @@ -37,7 +37,7 @@ public interface Authenticator { /** * Attempt to Extract an {@link AuthenticationToken} from the given {@link Context}. * @param context The context object encapsulating current request and other information relevant for authentication. - * + * @return An {@link AuthenticationToken} if one can be extracted or null if this Authenticator cannot * extract one. */ From c782a2cc37b612b433dc26a3a941816e02420a7c Mon Sep 17 00:00:00 2001 From: Slobodan Adamovic Date: Mon, 25 Aug 2025 09:47:59 +0200 Subject: [PATCH 8/8] fix javadoc line lenght --- .../core/security/authc/apikey/CustomAuthenticator.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java index 39f5b22357b73..5415714bb3114 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/apikey/CustomAuthenticator.java @@ -15,9 +15,9 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationToken; /** - * 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). + * An extension point to provide a custom 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 CustomAuthenticator {