Skip to content

Feature: Add Option to Strip Authorization Header on Redirect #2090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -375,6 +375,13 @@ public interface AsyncHttpClientConfig {

int getIoThreadsCount();

/**
* Indicates whether the Authorization header should be stripped during redirects to a different domain.
*
* @return true if the Authorization header should be stripped, false otherwise.
*/
boolean isStripAuthorizationOnRedirect();

enum ResponseBodyPartFactory {

EAGER {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
private final boolean keepEncodingHeader;
private final ProxyServerSelector proxyServerSelector;
private final boolean validateResponseHeaders;
private final boolean stripAuthorizationOnRedirect;

// websockets
private final boolean aggregateWebSocketFrameFragments;
Expand Down Expand Up @@ -219,6 +220,7 @@ private DefaultAsyncHttpClientConfig(// http
boolean validateResponseHeaders,
boolean aggregateWebSocketFrameFragments,
boolean enablewebSocketCompression,
boolean stripAuthorizationOnRedirect,

// timeouts
Duration connectTimeout,
Expand Down Expand Up @@ -307,6 +309,7 @@ private DefaultAsyncHttpClientConfig(// http
this.keepEncodingHeader = keepEncodingHeader;
this.proxyServerSelector = proxyServerSelector;
this.validateResponseHeaders = validateResponseHeaders;
this.stripAuthorizationOnRedirect = stripAuthorizationOnRedirect;

// websocket
this.aggregateWebSocketFrameFragments = aggregateWebSocketFrameFragments;
Expand Down Expand Up @@ -564,6 +567,11 @@ public boolean isValidateResponseHeaders() {
return validateResponseHeaders;
}

@Override
public boolean isStripAuthorizationOnRedirect() {
return stripAuthorizationOnRedirect;
}

// ssl
@Override
public boolean isUseOpenSsl() {
Expand Down Expand Up @@ -800,6 +808,7 @@ public static class Builder {
private boolean useProxySelector = defaultUseProxySelector();
private boolean useProxyProperties = defaultUseProxyProperties();
private boolean validateResponseHeaders = defaultValidateResponseHeaders();
private boolean stripAuthorizationOnRedirect = false; // default value

// websocket
private boolean aggregateWebSocketFrameFragments = defaultAggregateWebSocketFrameFragments();
Expand Down Expand Up @@ -891,6 +900,7 @@ public Builder(AsyncHttpClientConfig config) {
keepEncodingHeader = config.isKeepEncodingHeader();
proxyServerSelector = config.getProxyServerSelector();
validateResponseHeaders = config.isValidateResponseHeaders();
stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect();

// websocket
aggregateWebSocketFrameFragments = config.isAggregateWebSocketFrameFragments();
Expand Down Expand Up @@ -1079,6 +1089,11 @@ public Builder setUseProxyProperties(boolean useProxyProperties) {
return this;
}

public Builder setStripAuthorizationOnRedirect(boolean value) {
stripAuthorizationOnRedirect = value;
return this;
}

// websocket
public Builder setAggregateWebSocketFrameFragments(boolean aggregateWebSocketFrameFragments) {
this.aggregateWebSocketFrameFragments = aggregateWebSocketFrameFragments;
Expand Down Expand Up @@ -1444,6 +1459,7 @@ public DefaultAsyncHttpClientConfig build() {
validateResponseHeaders,
aggregateWebSocketFrameFragments,
enablewebSocketCompression,
stripAuthorizationOnRedirect,
connectTimeout,
requestTimeout,
readTimeout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.slf4j.LoggerFactory;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION;
Expand Down Expand Up @@ -73,11 +72,13 @@ public class Redirect30xInterceptor {
private final AsyncHttpClientConfig config;
private final NettyRequestSender requestSender;
private final MaxRedirectException maxRedirectException;
private final boolean stripAuthorizationOnRedirect;

Redirect30xInterceptor(ChannelManager channelManager, AsyncHttpClientConfig config, NettyRequestSender requestSender) {
this.channelManager = channelManager;
this.config = config;
this.requestSender = requestSender;
stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect(); // New flag
maxRedirectException = unknownStackTrace(new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects()),
Redirect30xInterceptor.class, "exitAfterHandlingRedirect");
}
Expand Down Expand Up @@ -127,7 +128,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture<?>
}
}

requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody));
requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody, stripAuthorizationOnRedirect));

// in case of a redirect from HTTP to HTTPS, future
// attributes might change
Expand Down Expand Up @@ -180,7 +181,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture<?>
return false;
}

private static HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody) {
private static HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody, boolean stripAuthorization) {
HttpHeaders headers = request.getHeaders()
.remove(HOST)
.remove(CONTENT_LENGTH);
Expand All @@ -189,7 +190,7 @@ private static HttpHeaders propagatedHeaders(Request request, Realm realm, boole
headers.remove(CONTENT_TYPE);
}

if (realm != null && realm.getScheme() == AuthScheme.NTLM) {
if (stripAuthorization || (realm != null && realm.getScheme() == AuthScheme.NTLM)) {
headers.remove(AUTHORIZATION)
.remove(PROXY_AUTHORIZATION);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.asynchttpclient;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class DefaultAsyncHttpClientConfigTest {
@Test
void testStripAuthorizationOnRedirect_DefaultIsFalse() {
DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build();
assertFalse(config.isStripAuthorizationOnRedirect(), "Default should be false");
}

@Test
void testStripAuthorizationOnRedirect_SetTrue() {
DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
.setStripAuthorizationOnRedirect(true)
.build();
assertTrue(config.isStripAuthorizationOnRedirect(), "Should be true when set");
}

@Test
void testStripAuthorizationOnRedirect_SetFalse() {
DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
.setStripAuthorizationOnRedirect(false)
.build();
assertFalse(config.isStripAuthorizationOnRedirect(), "Should be false when set to false");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package org.asynchttpclient;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

public class StripAuthorizationOnRedirectHttpTest {
private static HttpServer server;
private static int port;
private static volatile String lastAuthHeader;

@BeforeAll
public static void startServer() throws Exception {
server = HttpServer.create(new InetSocketAddress(0), 0);
port = server.getAddress().getPort();
server.createContext("/redirect", new RedirectHandler());
server.createContext("/final", new FinalHandler());
server.start();
}

@AfterAll
public static void stopServer() {
server.stop(0);
}

static class RedirectHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
String auth = exchange.getRequestHeaders().getFirst("Authorization");
lastAuthHeader = auth;
exchange.getResponseHeaders().add("Location", "http://localhost:" + port + "/final");
try {
exchange.sendResponseHeaders(302, -1);
} catch (Exception ignored) {
}
exchange.close();
}
}

static class FinalHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) {
String auth = exchange.getRequestHeaders().getFirst("Authorization");
lastAuthHeader = auth;
try {
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
} catch (Exception ignored) {
}
exchange.close();
}
}

@Test
void testAuthHeaderPropagatedByDefault() throws Exception {
DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
.setFollowRedirect(true)
.build();
try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) {
lastAuthHeader = null;
client.prepareGet("http://localhost:" + port + "/redirect")
.setHeader("Authorization", "Bearer testtoken")
.execute()
.get(5, TimeUnit.SECONDS);
// By default, Authorization header is propagated to /final
assertEquals("Bearer testtoken", lastAuthHeader, "Authorization header should be present on redirect by default");
}
}

@Test
void testAuthHeaderStrippedWhenEnabled() throws Exception {
DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
.setFollowRedirect(true)
.setStripAuthorizationOnRedirect(true)
.build();
try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) {
lastAuthHeader = null;
client.prepareGet("http://localhost:" + port + "/redirect")
.setHeader("Authorization", "Bearer testtoken")
.execute()
.get(5, TimeUnit.SECONDS);
// When enabled, Authorization header should be stripped on /final
assertNull(lastAuthHeader, "Authorization header should be stripped on redirect when enabled");
}
}
}