Skip to content

Commit bfdfba6

Browse files
Add handling for secondary X-Client-Authentication header (elastic#140634)
This PR extends `SecondaryAuthenticator` to support passing of `X-Client-Authentication` header as `es-secondary-x-client-authentication`. This allows secondary authentication to work with custom authenticators that require additional `X-Client-Authentication` header along with the standard `Authorization` header.
1 parent 702a322 commit bfdfba6

File tree

4 files changed

+112
-15
lines changed

4 files changed

+112
-15
lines changed

server/src/main/java/org/elasticsearch/common/util/concurrent/ThreadContext.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,7 @@ public void sanitizeHeaders() {
783783
.removeIf(
784784
entry -> entry.getKey().equalsIgnoreCase("authorization")
785785
|| entry.getKey().equalsIgnoreCase("es-secondary-authorization")
786+
|| entry.getKey().equalsIgnoreCase("es-secondary-x-client-authentication")
786787
|| entry.getKey().equalsIgnoreCase("ES-Client-Authentication")
787788
|| entry.getKey().equalsIgnoreCase("X-Client-Authentication")
788789
);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,6 +1676,7 @@ public Collection<RestHeaderDefinition> getRestHeaders() {
16761676
Set<RestHeaderDefinition> headers = new HashSet<>();
16771677
headers.add(new RestHeaderDefinition(UsernamePasswordToken.BASIC_AUTH_HEADER, false));
16781678
headers.add(new RestHeaderDefinition(SecondaryAuthenticator.SECONDARY_AUTH_HEADER_NAME, false));
1679+
headers.add(new RestHeaderDefinition(SecondaryAuthenticator.SECONDARY_X_CLIENT_AUTH_HEADER_NAME, false));
16791680
if (XPackSettings.AUDIT_ENABLED.get(settings)) {
16801681
headers.add(new RestHeaderDefinition(AuditTrail.X_FORWARDED_FOR_HEADER, true));
16811682
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.elasticsearch.xpack.security.audit.AuditTrailService;
2525
import org.elasticsearch.xpack.security.authc.AuthenticationService;
2626

27+
import java.util.Map;
2728
import java.util.function.Consumer;
2829
import java.util.function.Supplier;
2930

@@ -37,6 +38,14 @@ public class SecondaryAuthenticator {
3738
*/
3839
public static final String SECONDARY_AUTH_HEADER_NAME = "es-secondary-authorization";
3940

41+
/**
42+
* Header name for secondary client authentication credentials.
43+
* Used by authenticators that require additional [@code X-Client-Authentication} header along with the Authorization header.
44+
*/
45+
public static final String SECONDARY_X_CLIENT_AUTH_HEADER_NAME = "es-secondary-x-client-authentication";
46+
47+
private static final String X_CLIENT_AUTHENTICATION = "X-Client-Authentication";
48+
4049
private static final Logger logger = LogManager.getLogger(SecondaryAuthenticator.class);
4150
private final SecurityContext securityContext;
4251
private final AuthenticationService authenticationService;
@@ -110,6 +119,8 @@ private void authenticate(Consumer<ActionListener<Authentication>> authenticate,
110119
return;
111120
}
112121

122+
final Map<String, String> additionalHeaders = mapAdditionalSecondaryAuthHeaders(threadContext);
123+
113124
final Supplier<ThreadContext.StoredContext> originalContext = threadContext.newRestorableContext(false);
114125
final ActionListener<Authentication> authenticationListener = new ContextPreservingActionListener<>(
115126
originalContext,
@@ -133,7 +144,34 @@ private void authenticate(Consumer<ActionListener<Authentication>> authenticate,
133144
UsernamePasswordToken.BASIC_AUTH_HEADER
134145
);
135146
threadContext.putHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, header);
147+
148+
if (false == additionalHeaders.isEmpty()) {
149+
threadContext.putHeader(additionalHeaders);
150+
}
151+
136152
authenticate.accept(authenticationListener);
137153
}
138154
}
155+
156+
/**
157+
* Extracts additional secondary authentication headers from the thread context.
158+
* These headers are mapped from their secondary header names to the actual header names
159+
* expected by authenticators.
160+
*
161+
* @param threadContext the thread context to extract headers from
162+
* @return a map of header names to values for additional secondary auth headers, empty if none found
163+
*/
164+
private Map<String, String> mapAdditionalSecondaryAuthHeaders(ThreadContext threadContext) {
165+
final String secondaryXClientAuthHeader = threadContext.getHeader(SECONDARY_X_CLIENT_AUTH_HEADER_NAME);
166+
if (Strings.hasText(secondaryXClientAuthHeader)) {
167+
logger.trace(
168+
"found additional secondary [{}] header, placing it in the [{}] header",
169+
SECONDARY_X_CLIENT_AUTH_HEADER_NAME,
170+
X_CLIENT_AUTHENTICATION
171+
);
172+
return Map.of(X_CLIENT_AUTHENTICATION, secondaryXClientAuthHeader);
173+
}
174+
175+
return Map.of();
176+
}
139177
}

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

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,20 @@
6464
import org.junit.Before;
6565
import org.mockito.Mockito;
6666

67-
import java.nio.charset.StandardCharsets;
6867
import java.time.Clock;
69-
import java.util.Base64;
7068
import java.util.List;
7169
import java.util.Map;
7270
import java.util.Set;
7371
import java.util.concurrent.ExecutionException;
7472
import java.util.concurrent.atomic.AtomicReference;
7573
import java.util.function.Consumer;
7674

75+
import static org.elasticsearch.test.ActionListenerUtils.anyActionListener;
76+
import static org.elasticsearch.test.rest.ESRestTestCase.basicAuthHeaderValue;
7777
import static org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator.SECONDARY_AUTH_HEADER_NAME;
78+
import static org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator.SECONDARY_X_CLIENT_AUTH_HEADER_NAME;
7879
import static org.hamcrest.Matchers.equalTo;
80+
import static org.hamcrest.Matchers.notNullValue;
7981
import static org.hamcrest.Matchers.nullValue;
8082
import static org.mockito.ArgumentMatchers.any;
8183
import static org.mockito.Mockito.doAnswer;
@@ -259,11 +261,7 @@ private SecondaryAuthentication assertAuthenticateWithBasicAuthentication(Consum
259261
final SecureString password = new SecureString(randomAlphaOfLengthBetween(8, 24).toCharArray());
260262
realm.defineUser(user, password);
261263

262-
threadPool.getThreadContext()
263-
.putHeader(
264-
SECONDARY_AUTH_HEADER_NAME,
265-
"Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8))
266-
);
264+
threadPool.getThreadContext().putHeader(SECONDARY_AUTH_HEADER_NAME, basicAuthHeaderValue(user, password));
267265

268266
final PlainActionFuture<SecondaryAuthentication> future = new PlainActionFuture<>();
269267
final AtomicReference<ThreadContext.StoredContext> listenerContext = new AtomicReference<>();
@@ -273,8 +271,8 @@ private SecondaryAuthentication assertAuthenticateWithBasicAuthentication(Consum
273271
}, e -> future.onFailure(e)));
274272

275273
final SecondaryAuthentication secondaryAuthentication = future.result();
276-
assertThat(secondaryAuthentication, Matchers.notNullValue());
277-
assertThat(secondaryAuthentication.getAuthentication(), Matchers.notNullValue());
274+
assertThat(secondaryAuthentication, notNullValue());
275+
assertThat(secondaryAuthentication.getAuthentication(), notNullValue());
278276
assertThat(secondaryAuthentication.getAuthentication().getEffectiveSubject().getUser().principal(), equalTo(user));
279277
assertThat(secondaryAuthentication.getAuthentication().getAuthenticatingSubject().getRealm().getName(), equalTo(realm.name()));
280278

@@ -303,10 +301,7 @@ private void assertAuthenticateWithIncorrectPassword(Consumer<ActionListener<Sec
303301
realm.defineUser(user, password);
304302

305303
threadPool.getThreadContext()
306-
.putHeader(
307-
SECONDARY_AUTH_HEADER_NAME,
308-
"Basic " + Base64.getEncoder().encodeToString((user + ":NOT-" + password).getBytes(StandardCharsets.UTF_8))
309-
);
304+
.putHeader(SECONDARY_AUTH_HEADER_NAME, basicAuthHeaderValue(user, new SecureString("NOT-" + password)));
310305

311306
final PlainActionFuture<SecondaryAuthentication> future = new PlainActionFuture<>();
312307
final AtomicReference<ThreadContext.StoredContext> listenerContext = new AtomicReference<>();
@@ -354,10 +349,72 @@ public void testAuthenticateUsingBearerToken() throws Exception {
354349
authenticator.authenticate(AuthenticateAction.NAME, request, future);
355350

356351
final SecondaryAuthentication secondaryAuthentication = future.result();
357-
assertThat(secondaryAuthentication, Matchers.notNullValue());
358-
assertThat(secondaryAuthentication.getAuthentication(), Matchers.notNullValue());
352+
assertThat(secondaryAuthentication, notNullValue());
353+
assertThat(secondaryAuthentication.getAuthentication(), notNullValue());
359354
assertThat(secondaryAuthentication.getAuthentication().getEffectiveSubject().getUser(), equalTo(user));
360355
assertThat(secondaryAuthentication.getAuthentication().getAuthenticationType(), equalTo(AuthenticationType.TOKEN));
361356
}
362357

358+
public void testSecondaryXClientAuthHeaderIsPlacedInThreadContext() throws Exception {
359+
final String xClientAuthValue = randomAlphaOfLengthBetween(20, 40);
360+
final String capturedHeader = authenticateAndCaptureXClientAuthHeader(xClientAuthValue);
361+
assertThat(capturedHeader, equalTo(xClientAuthValue));
362+
}
363+
364+
public void testSecondaryXClientAuthHeaderIsNotPlacedWhenNotProvided() throws Exception {
365+
final String capturedHeader = authenticateAndCaptureXClientAuthHeader(randomBoolean() ? null : "");
366+
assertThat(capturedHeader, nullValue());
367+
}
368+
369+
private String authenticateAndCaptureXClientAuthHeader(String xClientAuthValue) throws Exception {
370+
final AtomicReference<String> capturedHeader = new AtomicReference<>();
371+
final Authentication authentication = AuthenticationTestHelper.builder()
372+
.user(new User(randomAlphaOfLengthBetween(6, 12)))
373+
.realmRef(new RealmRef("test_realm", "dummy", "node1"))
374+
.build(false);
375+
376+
final AuthenticationService mockAuthService = mock(AuthenticationService.class);
377+
boolean useTransportRequest = randomBoolean();
378+
if (useTransportRequest) {
379+
doAnswer(invocation -> {
380+
capturedHeader.set(threadPool.getThreadContext().getHeader("X-Client-Authentication"));
381+
@SuppressWarnings("unchecked")
382+
ActionListener<Authentication> listener = (ActionListener<Authentication>) invocation.getArguments()[3];
383+
listener.onResponse(authentication);
384+
return null;
385+
}).when(mockAuthService).authenticate(any(String.class), any(TransportRequest.class), any(Boolean.class), anyActionListener());
386+
} else {
387+
doAnswer(invocation -> {
388+
capturedHeader.set(threadPool.getThreadContext().getHeader("X-Client-Authentication"));
389+
@SuppressWarnings("unchecked")
390+
ActionListener<Authentication> listener = (ActionListener<Authentication>) invocation.getArguments()[2];
391+
listener.onResponse(authentication);
392+
return null;
393+
}).when(mockAuthService).authenticate(any(), any(Boolean.class), anyActionListener());
394+
}
395+
396+
final SecondaryAuthenticator mockAuthenticator = new SecondaryAuthenticator(
397+
securityContext,
398+
mockAuthService,
399+
new AuditTrailService(null, null)
400+
);
401+
402+
threadPool.getThreadContext()
403+
.putHeader(SECONDARY_AUTH_HEADER_NAME, basicAuthHeaderValue(randomAlphanumericOfLength(5), randomSecureStringOfLength(5)));
404+
405+
if (xClientAuthValue != null) {
406+
threadPool.getThreadContext().putHeader(SECONDARY_X_CLIENT_AUTH_HEADER_NAME, xClientAuthValue);
407+
}
408+
409+
final PlainActionFuture<SecondaryAuthentication> future = new PlainActionFuture<>();
410+
if (useTransportRequest) {
411+
mockAuthenticator.authenticate(AuthenticateAction.NAME, AuthenticateRequest.INSTANCE, future);
412+
} else {
413+
mockAuthenticator.authenticateAndAttachToContext(new FakeRestRequest(), future);
414+
}
415+
416+
assertThat(future.result(), notNullValue());
417+
return capturedHeader.get();
418+
}
419+
363420
}

0 commit comments

Comments
 (0)