Skip to content
Open
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
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Netty NIO HTTP Client",
"contributor": "",
"description": "Add support for Kerberos (SPNEGO) proxy authentication via the new `proxyAuthScheme` option on the Netty client's `ProxyConfiguration`. Setting `ProxyAuthScheme.NEGOTIATE` authenticates proxy CONNECT tunnels using the Kerberos ticket cache in the environment; a valid ticket-granting ticket must already exist (for example via `kinit`), and no password or keytab is read. `ProxyAuthScheme.BASIC` may also be set to select Basic authentication explicitly. See [#7033](https://github.com/aws/aws-sdk-java-v2/issues/7033)."
}
18 changes: 18 additions & 0 deletions bom-internal/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,24 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-simplekdc</artifactId>
<version>${kerby.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-client</artifactId>
<version>${kerby.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-core</artifactId>
<version>${kerby.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.http;

import software.amazon.awssdk.annotations.SdkPublicApi;

/**
* Supported auth schemes for authentication with a proxy.
*/
@SdkPublicApi
public enum ProxyAuthScheme {
/**
* Basic authentication, as defined by <a href="https://datatracker.ietf.org/doc/html/rfc7617">RFC 7617</a>. Requires a
* username and password.
*/
BASIC("Basic"),

/**
* Kerberos authentication, using SPNEGO as defined by
* <a href="https://datatracker.ietf.org/doc/html/rfc4559">RFC 4559</a>.
* <p>
* Credentials are read from the environment Kerberos ticket cache. The client never prompts for a password and never reads a
* keytab, so the environment must already hold a valid ticket-granting ticket, typically obtained by running
* {@code kinit} and verifiable with {@code klist}. The cache location follows the usual Kerberos conventions, including
* the {@code KRB5CCNAME} environment variable. Any username and password configured on the proxy are ignored.
*/
NEGOTIATE("Negotiate"),
;

private final String value;

ProxyAuthScheme(String value) {
this.value = value;
}

public String value() {
return value;
}
}
15 changes: 15 additions & 0 deletions http-clients/netty-nio-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,21 @@
<artifactId>jetty-util</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-simplekdc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.kerby</groupId>
<artifactId>kerb-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import java.util.HashSet;
import java.util.Set;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.http.ProxyAuthScheme;
import software.amazon.awssdk.http.nio.netty.internal.utils.NettyClientLogger;
import software.amazon.awssdk.utils.ProxyConfigProvider;
import software.amazon.awssdk.utils.ProxyEnvironmentSetting;
import software.amazon.awssdk.utils.ProxySystemSetting;
Expand All @@ -34,13 +36,16 @@
*/
@SdkPublicApi
public final class ProxyConfiguration implements ToCopyableBuilder<ProxyConfiguration.Builder, ProxyConfiguration> {
private static final NettyClientLogger log = NettyClientLogger.getLogger(ProxyConfiguration.class);

private final Boolean useSystemPropertyValues;
private final Boolean useEnvironmentVariablesValues;
private final String scheme;
private final String host;
private final int port;
private final String username;
private final String password;
private final ProxyAuthScheme proxyAuthScheme;
private final Set<String> nonProxyHosts;

private ProxyConfiguration(BuilderImpl builder) {
Expand All @@ -56,7 +61,32 @@ private ProxyConfiguration(BuilderImpl builder) {
this.port = resolvePort(builder, proxyConfigProvider);
this.username = resolveUserName(builder, proxyConfigProvider);
this.password = resolvePassword(builder, proxyConfigProvider);
this.proxyAuthScheme = builder.proxyAuthScheme;
this.nonProxyHosts = resolveNonProxyHosts(builder, proxyConfigProvider);
validateProxyAuthConfig(proxyAuthScheme, username, password);
warnOnIgnoredCredentials(builder);
}

private static void validateProxyAuthConfig(ProxyAuthScheme proxyAuthScheme, String username, String password) {
if (proxyAuthScheme == ProxyAuthScheme.BASIC
&& (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))) {
throw new IllegalArgumentException("username and password must be configured when using BASIC proxy auth");
}
}

/**
* NEGOTIATE reads its credentials from the Kerberos ticket cache, so a username and password are dead configuration. Warn
* rather than fail, and only when they were set directly on this builder: values resolved from system properties or
* environment variables may not be under the caller's control, and warning about those would be noise.
*/
private static void warnOnIgnoredCredentials(BuilderImpl builder) {
if (builder.proxyAuthScheme == ProxyAuthScheme.NEGOTIATE
&& (builder.username != null || builder.password != null)) {
log.warn(null, () -> "A proxy username and/or password was configured alongside the "
+ ProxyAuthScheme.NEGOTIATE + " proxy auth scheme, and will be ignored. " +
ProxyAuthScheme.NEGOTIATE + " authenticates using the Kerberos ticket cache. Configure "
+ ProxyAuthScheme.BASIC + " to authenticate with a username and password instead.");
}
}

private static Set<String> resolveNonProxyHosts(BuilderImpl builder, ProxyConfigProvider proxyConfigProvider) {
Expand Down Expand Up @@ -151,6 +181,13 @@ public Set<String> nonProxyHosts() {
return Collections.unmodifiableSet(nonProxyHosts != null ? nonProxyHosts : Collections.emptySet());
}

/**
* @return The auth scheme to use to authenticate with the proxy.
*/
public ProxyAuthScheme proxyAuthScheme() {
return proxyAuthScheme;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down Expand Up @@ -183,6 +220,10 @@ public boolean equals(Object o) {
return false;
}

if (proxyAuthScheme != null ? !proxyAuthScheme.equals(that.proxyAuthScheme) : that.proxyAuthScheme != null) {
return false;
}

return nonProxyHosts.equals(that.nonProxyHosts);

}
Expand All @@ -195,6 +236,7 @@ public int hashCode() {
result = 31 * result + nonProxyHosts.hashCode();
result = 31 * result + (username != null ? username.hashCode() : 0);
result = 31 * result + (password != null ? password.hashCode() : 0);
result = 31 * result + (proxyAuthScheme != null ? proxyAuthScheme.hashCode() : 0);
return result;
}

Expand Down Expand Up @@ -247,6 +289,25 @@ public interface Builder extends CopyableBuilder<Builder, ProxyConfiguration> {
*/
Builder nonProxyHosts(Set<String> nonProxyHosts);

/**
* Configure the auth scheme to use to authenticate with the proxy.
* <p>
* If unset and {@link #username(String)} and {@link #password(String)} are set, the client will
* assume {@link ProxyAuthScheme#BASIC} auth.
* <p>
* If set to {@link ProxyAuthScheme#BASIC}, {@link #username(String)} and {@link #password(String)} must also be
* configured (directly, or resolved from system properties or environment variables), otherwise
* {@link Builder#build()} throws {@link IllegalArgumentException}.
* <p>
* If set to {@link ProxyAuthScheme#NEGOTIATE}, credentials come from the Kerberos ticket cache rather than from this
* configuration, and any configured username and password are ignored. See {@link ProxyAuthScheme#NEGOTIATE} for the
* environment it requires and for how a missing or expired ticket surfaces.
*
* @param proxyAuthScheme The auth scheme.
* @return This object for method chaining.
*/
Builder proxyAuthScheme(ProxyAuthScheme proxyAuthScheme);

/**
* Set the username used to authenticate with the proxy username.
*
Expand Down Expand Up @@ -305,6 +366,7 @@ private static final class BuilderImpl implements Builder {
private String scheme = "http";
private String host;
private int port = 0;
private ProxyAuthScheme proxyAuthScheme;
private String username;
private String password;
private Set<String> nonProxyHosts;
Expand All @@ -322,6 +384,7 @@ private BuilderImpl(ProxyConfiguration proxyConfiguration) {
this.port = proxyConfiguration.port;
this.nonProxyHosts = proxyConfiguration.nonProxyHosts != null ?
new HashSet<>(proxyConfiguration.nonProxyHosts) : null;
this.proxyAuthScheme = proxyConfiguration.proxyAuthScheme;
this.username = proxyConfiguration.username;
this.password = proxyConfiguration.password;
}
Expand Down Expand Up @@ -354,6 +417,12 @@ public Builder nonProxyHosts(Set<String> nonProxyHosts) {
return this;
}

@Override
public Builder proxyAuthScheme(ProxyAuthScheme proxyAuthScheme) {
this.proxyAuthScheme = proxyAuthScheme;
return this;
}

@Override
public Builder username(String username) {
this.username = username;
Expand Down
Loading
Loading