Skip to content
Closed
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
2 changes: 1 addition & 1 deletion sdk/core/azure-core-amqp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>qpid-proton-j-extensions</artifactId>
<version>1.2.4</version> <!-- {x-version-update;com.microsoft.azure:qpid-proton-j-extensions;external_dependency} -->
<version>1.2.6-SNAPSHOT</version> <!-- {x-version-update;com.microsoft.azure:qpid-proton-j-extensions;external_dependency} -->
</dependency>
<dependency>
<groupId>org.apache.qpid</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.amqp;

import com.azure.core.amqp.implementation.ChallengeResponseAccessHelper;

import java.util.List;
import java.util.Map;

/**
* A contract to authenticate a proxy server to tunnel a websocket connection to an AMQP broker.
*/
public interface ProxyAuthenticator {
/**
* Authenticate a proxy server to tunnel a websocket connection to an AMQP broker.
* <p>
* This method is called when the proxy server replies to the CONNECT with 407 (Proxy Authentication Required)
* challenge. The proxy server's challenge response includes a 'Proxy-Authenticate' header indicating
* the authentication scheme(s) that the proxy supports. The implementation of this method should
* <ul>
* <li>enumerate the schemes using {@link ChallengeResponse#getAuthenticationSchemes()}) and choose the most
* secure scheme the client supports,</li>
* <li>identify the credential for the chosen scheme, </li>
* <li>compute and return authorization value.The RFC7325 defines authorization format as a value that starts
* with the selected scheme, followed by a space and the base64 encoded credentials for the scheme.</li>
* </ul>
* The returned authorization value will be sent to the proxy server in 'Proxy-Authorization' header to complete
* the authentication.
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407">407 Proxy Authentication Required</a>
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate">Proxy-Authenticate</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-4.4">RFC7235</a>
*
* @param response the challenge response from the proxy server.
* @return the authorization value to send to the proxy server using 'Proxy-Authorization' header.
*/
String authenticate(ChallengeResponse response);

/**
* Represents the 407 challenge response from the proxy server.
*/
final class ChallengeResponse {
static {
ChallengeResponseAccessHelper.setAccessor(ChallengeResponse::new);
}
private static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
private final Map<String, List<String>> headers;

/**
* Creates the ChallengeResponse.
*
* @param headers the response headers
*/
ChallengeResponse(Map<String, List<String>> headers) {
this.headers = headers;
}

/**
* Gets the authentication schemes supported by the proxy server.
*
* @return the authentication schemes supported by the proxy server.
*/
public List<String> getAuthenticationSchemes() {
return headers.get(PROXY_AUTHENTICATE);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public class ProxyOptions implements AutoCloseable {
private final PasswordAuthentication credentials;
private final Proxy proxyAddress;
private final ProxyAuthenticationType authentication;

private final ProxyAuthenticator proxyAuthenticator;
/**
* Gets the system defaults for proxy configuration and authentication.
*/
Expand All @@ -91,6 +91,7 @@ private ProxyOptions() {
this.credentials = null;
this.proxyAddress = null;
this.authentication = ProxyAuthenticationType.NONE;
this.proxyAuthenticator = null;
}

/**
Expand All @@ -110,6 +111,7 @@ private ProxyOptions() {
public ProxyOptions(ProxyAuthenticationType authentication, Proxy proxyAddress, String username, String password) {
this.authentication = Objects.requireNonNull(authentication, "'authentication' cannot be null.");
this.proxyAddress = proxyAddress;
this.proxyAuthenticator = null;

if (username != null && password != null) {
this.credentials = new PasswordAuthentication(username, password.toCharArray());
Expand All @@ -119,6 +121,21 @@ public ProxyOptions(ProxyAuthenticationType authentication, Proxy proxyAddress,
}
}

/**
* Creates a proxy configuration that uses the {@code proxyAddress} and authenticates with provided
* {@code authenticator}.
*
* @param authenticator the proxy authenticator to use.
* @param proxyAddress Proxy to use.
* @throws NullPointerException if {@code proxyAddress} or {@code proxyAuthenticator} is {@code null}.
*/
public ProxyOptions(ProxyAuthenticator authenticator, Proxy proxyAddress) {
this.proxyAuthenticator = Objects.requireNonNull(authenticator, "'authenticator' cannot be null.");
this.proxyAddress = Objects.requireNonNull(proxyAddress, "'proxyAddress' cannot be null.");
this.authentication = null;
this.credentials = null;
}

/**
* Attempts to load a proxy from the configuration.
*
Expand Down Expand Up @@ -162,6 +179,19 @@ public ProxyAuthenticationType getAuthentication() {
return this.authentication;
}

/**
* Gets the proxy authenticator to set up the web socket connection to the AMQP broker via a proxy.
* <p>
* The authenticator is responsible for selecting one of the authorization schemes that the proxy presents, identify
* the credentials for the scheme it selects then compute and return the authorization value to be sent through
* the 'Proxy-Authorization' Header.
* </p>
* @return the proxy authenticator.
*/
public ProxyAuthenticator getAuthenticator() {
return this.proxyAuthenticator;
}

/**
* Gets the proxy address.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.amqp.implementation;

import com.azure.core.amqp.ProxyAuthenticator;

import java.util.List;
import java.util.Map;

/**
* Access helper to provide access to the package-private constructor of {@link ProxyAuthenticator.ChallengeResponse}.
*/
public final class ChallengeResponseAccessHelper {
private static ChallengeResponseAccessor accessor;

/**
* The {@link ProxyAuthenticator.ChallengeResponse} accessor contract.
*/
public interface ChallengeResponseAccessor {
/**
* Creates a new instance of {@link ProxyAuthenticator.ChallengeResponse} using its package-private constructor.
*
* @param headers the proxy challenge response headers.
* @return the created instance.
*/
ProxyAuthenticator.ChallengeResponse internalCreate(Map<String, List<String>> headers);
}

/**
* Sets the accessor.
*
* @param accessor the accessor.
*/
public static void setAccessor(ChallengeResponseAccessor accessor) {
ChallengeResponseAccessHelper.accessor = accessor;
}

/**
* Creates a new instance of {@link ProxyAuthenticator.ChallengeResponse} using its package-private constructor.
*
* @param headers the proxy challenge response headers.
* @return the created instance.
* @throws RuntimeException if the accessor lookup fails.
*/
public static ProxyAuthenticator.ChallengeResponse internalCreate(Map<String, List<String>> headers) {
if (accessor == null) {
try {
Class.forName(ProxyAuthenticator.ChallengeResponse.class.getName(), true,
ChallengeResponseAccessHelper.class.getClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
assert accessor != null;
return accessor.internalCreate(headers);
}

/**
* Access helper shouldn't have an accessible constructor.
*/
private ChallengeResponseAccessHelper() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
package com.azure.core.amqp.implementation.handler;

import com.azure.core.amqp.ProxyAuthenticationType;
import com.azure.core.amqp.ProxyAuthenticator;
import com.azure.core.amqp.ProxyOptions;
import com.azure.core.amqp.implementation.AmqpErrorCode;
import com.azure.core.amqp.implementation.AmqpMetricsProvider;
import com.azure.core.amqp.implementation.ChallengeResponseAccessHelper;
import com.azure.core.amqp.implementation.ConnectionOptions;
import com.azure.core.util.CoreUtils;
import com.microsoft.azure.proton.transport.proxy.ProxyHandler;
Expand Down Expand Up @@ -217,6 +219,13 @@ protected void addTransportLayers(final Event event, final TransportInternal tra
}

private com.microsoft.azure.proton.transport.proxy.ProxyConfiguration getProtonConfiguration() {
final ProxyAuthenticator authenticator = proxyOptions.getAuthenticator();
if (authenticator != null) {
final ProtonJExtensionsProxyAuthenticator protonAuthenticator
= new ProtonJExtensionsProxyAuthenticator(authenticator);
return new com.microsoft.azure.proton.transport.proxy.ProxyConfiguration(protonAuthenticator,
proxyOptions.getProxyAddress());
}
final com.microsoft.azure.proton.transport.proxy.ProxyAuthenticationType type
= getProtonAuthType(proxyOptions.getAuthentication());
final String username
Expand Down Expand Up @@ -268,4 +277,19 @@ private static boolean isProxyAddressLegal(final List<Proxy> proxies) {
&& proxies.get(0).address() != null
&& proxies.get(0).address() instanceof InetSocketAddress;
}

private static final class ProtonJExtensionsProxyAuthenticator
implements com.microsoft.azure.proton.transport.proxy.ProxyAuthenticator {
private final com.azure.core.amqp.ProxyAuthenticator authenticator;

private ProtonJExtensionsProxyAuthenticator(com.azure.core.amqp.ProxyAuthenticator authenticator) {
this.authenticator = Objects.requireNonNull(authenticator, "'authenticator' cannot be null.");
}

@Override
public String
authenticate(com.microsoft.azure.proton.transport.proxy.ProxyAuthenticator.ChallengeResponse response) {
return authenticator.authenticate(ChallengeResponseAccessHelper.internalCreate(response.getHeaders()));
}
}
}
2 changes: 1 addition & 1 deletion sdk/eventhubs/azure-messaging-eventhubs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-core-amqp</artifactId>
<version>2.9.3</version> <!-- {x-version-update;com.azure:azure-core-amqp;dependency} -->
<version>2.10.0-beta.1</version> <!-- {x-version-update;com.azure:azure-core-amqp;dependency} -->
</dependency>

<!-- Test dependencies -->
Expand Down