Skip to content

Commit 3e730d2

Browse files
committed
AMQP websocket custom proxy authenticator
1 parent 2ade24f commit 3e730d2

File tree

4 files changed

+242
-18
lines changed

4 files changed

+242
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.azure.proton.transport.proxy;
5+
6+
import com.microsoft.azure.proton.transport.proxy.impl.ChallengeResponseAccessHelper;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
/**
12+
* A contract to authenticate a proxy server to tunnel a websocket connection to an AMQP broker.
13+
*/
14+
public interface ProxyAuthenticator {
15+
/**
16+
* Authenticate a proxy server to tunnel a websocket connection to an AMQP broker.
17+
* <p>
18+
* This method is called when the proxy server replies to the CONNECT with 407 (Proxy Authentication Required)
19+
* challenge. The proxy server's challenge response includes a 'Proxy-Authenticate' header indicating
20+
* the authentication scheme(s) that the proxy supports. The implementation of this method should
21+
* <ul>
22+
* <li>enumerate the schemes using {@link ChallengeResponse#getAuthenticationSchemes()}) and choose the most
23+
* secure scheme the client supports,</li>
24+
* <li>identify the credential for the chosen scheme, </li>
25+
* <li>compute and return authorization value.The RFC7325 defines authorization format as a value that starts
26+
* with the selected scheme, followed by a space and the base64 encoded credentials for the scheme.</li>
27+
* </ul>
28+
* The returned authorization value will be sent to the proxy server in 'Proxy-Authorization' header to complete
29+
* the authentication.
30+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407">407 Proxy Authentication Required</a>
31+
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate">Proxy-Authenticate</a>
32+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7235#section-4.4">RFC7235</a>
33+
*
34+
* @param response the challenge response from the proxy server.
35+
* @return the authorization value to send to the proxy server using 'Proxy-Authorization' header.
36+
*/
37+
String authenticate(ChallengeResponse response);
38+
39+
/**
40+
* Represents the 407 challenge response from the proxy server.
41+
*/
42+
final class ChallengeResponse {
43+
static {
44+
ChallengeResponseAccessHelper.setAccessor(ChallengeResponse::new);
45+
}
46+
private static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
47+
private final Map<String, List<String>> headers;
48+
49+
/**
50+
* Creates the ChallengeResponse.
51+
*
52+
* @param headers the response headers
53+
*/
54+
ChallengeResponse(Map<String, List<String>> headers) {
55+
this.headers = headers;
56+
}
57+
58+
/**
59+
* Gets the headers.
60+
*
61+
* @return the headers.
62+
*/
63+
public Map<String, List<String>> getHeaders() {
64+
return headers;
65+
}
66+
67+
/**
68+
* Gets the authentication schemes supported by the proxy server.
69+
*
70+
* @return the authentication schemes supported by the proxy server.
71+
*/
72+
public List<String> getAuthenticationSchemes() {
73+
return headers.get(PROXY_AUTHENTICATE);
74+
}
75+
}
76+
}

src/main/java/com/microsoft/azure/proton/transport/proxy/ProxyConfiguration.java

+30
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class ProxyConfiguration implements AutoCloseable {
2020

2121
private final java.net.Proxy proxyAddress;
2222
private final ProxyAuthenticationType authentication;
23+
private final ProxyAuthenticator authenticator;
2324
private final PasswordAuthentication credentials;
2425

2526
/**
@@ -32,6 +33,7 @@ public class ProxyConfiguration implements AutoCloseable {
3233
*/
3334
private ProxyConfiguration() {
3435
this.authentication = null;
36+
this.authenticator = null;
3537
this.credentials = null;
3638
this.proxyAddress = null;
3739
}
@@ -65,6 +67,21 @@ public ProxyConfiguration(ProxyAuthenticationType authentication, java.net.Proxy
6567

6668
this.credentials = null;
6769
}
70+
this.authenticator = null;
71+
}
72+
73+
/**
74+
* Creates a proxy configuration that uses the {@code proxyAddress} and authenticates with provided {@code authenticator}.
75+
*
76+
* @param authenticator the proxy authenticator to use.
77+
* @param proxyAddress Proxy to use.
78+
* @throws NullPointerException if {@code proxyAddress} or {@code proxyAuthenticator} is {@code null}.
79+
*/
80+
public ProxyConfiguration(ProxyAuthenticator authenticator, java.net.Proxy proxyAddress) {
81+
this.authenticator = Objects.requireNonNull(authenticator, "'authenticator' cannot be null.");
82+
this.proxyAddress = Objects.requireNonNull(proxyAddress, "'proxyAddress' cannot be null.");
83+
this.authentication = null;
84+
this.credentials = null;
6885
}
6986

7087
/**
@@ -97,6 +114,19 @@ public ProxyAuthenticationType authentication() {
97114
return authentication;
98115
}
99116

117+
/**
118+
* Gets the proxy authenticator to set up the web socket connection to the AMQP broker via a proxy.
119+
* <p>
120+
* The authenticator is responsible for selecting one of the authorization schemes that the proxy presents, identify
121+
* the credentials for the scheme it selects then compute and return the authorization value to be sent through
122+
* the 'Proxy-Authorization' Header.
123+
* </p>
124+
* @return the proxy authenticator.
125+
*/
126+
public ProxyAuthenticator getAuthenticator() {
127+
return this.authenticator;
128+
}
129+
100130
/**
101131
* Gets whether the user has defined credentials.
102132
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.azure.proton.transport.proxy.impl;
5+
6+
import java.util.List;
7+
import java.util.Map;
8+
import com.microsoft.azure.proton.transport.proxy.ProxyAuthenticator;
9+
10+
/**
11+
* The accessor helper for {@link ProxyAuthenticator.ChallengeResponse}.
12+
*/
13+
public final class ChallengeResponseAccessHelper {
14+
private static ChallengeResponseAccessor accessor;
15+
16+
/**
17+
* The accessor interface for {@link ProxyAuthenticator.ChallengeResponse} construction.
18+
*/
19+
public interface ChallengeResponseAccessor {
20+
/**
21+
* Create an instance of {@link ProxyAuthenticator.ChallengeResponse} with the provided headers.
22+
*
23+
* @param headers the headers.
24+
* @return the created instance of {@link ProxyAuthenticator.ChallengeResponse}.
25+
*/
26+
ProxyAuthenticator.ChallengeResponse internalCreate(Map<String, List<String>> headers);
27+
}
28+
29+
/**
30+
* Sets the accessor.
31+
*
32+
* @param accessor the accessor.
33+
*/
34+
public static void setAccessor(ChallengeResponseAccessor accessor) {
35+
ChallengeResponseAccessHelper.accessor = accessor;
36+
}
37+
38+
/**
39+
* Creates an instance of {@link ProxyAuthenticator.ChallengeResponse} with the provided headers.
40+
*
41+
* @param headers the headers.
42+
* @return the created instance of {@link ProxyAuthenticator.ChallengeResponse}.
43+
*/
44+
public static ProxyAuthenticator.ChallengeResponse internalCreate(Map<String, List<String>> headers) {
45+
if (accessor == null) {
46+
try {
47+
Class.forName(ProxyAuthenticator.ChallengeResponse.class.getName(), true,
48+
ChallengeResponseAccessHelper.class.getClassLoader());
49+
} catch (ClassNotFoundException e) {
50+
throw new RuntimeException(e);
51+
}
52+
}
53+
assert accessor != null;
54+
return accessor.internalCreate(headers);
55+
}
56+
57+
/**
58+
* Private constructor to prevent instantiation.
59+
*/
60+
private ChallengeResponseAccessHelper() {
61+
}
62+
}

src/main/java/com/microsoft/azure/proton/transport/proxy/impl/ProxyImpl.java

+74-18
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import java.nio.ByteBuffer;
2323
import java.util.ArrayList;
2424
import java.util.Collections;
25+
import java.util.HashMap;
2526
import java.util.HashSet;
2627
import java.util.List;
2728
import java.util.Locale;
2829
import java.util.Map;
30+
import java.util.Objects;
2931
import java.util.Optional;
3032
import java.util.Set;
3133
import java.util.concurrent.atomic.AtomicReference;
@@ -253,12 +255,13 @@ public void process() throws TransportException {
253255
proxyResponse.set(null);
254256

255257
final boolean isSuccess = proxyHandler.validateProxyResponse(connectResponse);
256-
// When connecting to proxy, it does not challenge us for authentication. If the user has specified
257-
// a configuration, and it is not NONE, then we fail due to misconfiguration.
258258
if (isSuccess) {
259-
if (proxyConfiguration == null || proxyConfiguration.authentication() == ProxyAuthenticationType.NONE) {
259+
if (proxyConfiguration == null || proxyConfiguration.authentication() == ProxyAuthenticationType.NONE
260+
|| proxyConfiguration.getAuthenticator() == null) {
260261
proxyState = ProxyState.PN_PROXY_CONNECTED;
261262
} else {
263+
// In response to CONNECT, the proxy didn't challenge us for authentication. Given the user has specified an
264+
// authentication configuration, we fail due to misconfiguration.
262265
if (LOGGER.isErrorEnabled()) {
263266
LOGGER.error("ProxyConfiguration mismatch. User configured: '{}', but authentication is not required",
264267
proxyConfiguration.authentication());
@@ -269,29 +272,47 @@ public void process() throws TransportException {
269272
}
270273

271274
final Map<String, List<String>> headers = connectResponse.getHeaders();
272-
final Set<ProxyAuthenticationType> supportedTypes = getAuthenticationTypes(headers);
273-
274-
// The proxy did not successfully connect, user has specified that they want a particular
275-
// authentication method, but it is not in list of supported authentication methods.
276-
if (proxyConfiguration != null && !supportedTypes.contains(proxyConfiguration.authentication())) {
277-
if (LOGGER.isErrorEnabled()) {
278-
LOGGER.error("Proxy authentication required. User configured: '{}', but supported proxy authentication methods are: {}",
279-
proxyConfiguration.authentication(),
280-
supportedTypes.stream().map(type -> type.toString()).collect(Collectors.joining(",")));
275+
final List<String> challenges = headers.getOrDefault(PROXY_AUTHENTICATE, new ArrayList<>());
276+
final ProxyChallengeProcessor processor;
277+
if (proxyConfiguration != null && proxyConfiguration.getAuthenticator() != null) {
278+
final boolean is407 = connectResponse.getStatus().getStatusCode() == 407;
279+
if (!is407) {
280+
closeTailProxyError(PROXY_CONNECT_FAILED + connectResponse);
281+
break;
282+
}
283+
if (challenges.isEmpty()) {
284+
closeTailProxyError("'407 Proxy Authentication Required' received without " + PROXY_AUTHENTICATE
285+
+ "header or authentication schemes." + connectResponse);
286+
break;
281287
}
282-
closeTailProxyError(PROXY_CONNECT_USER_ERROR + PROXY_CONNECT_FAILED
288+
processor = new DelegatedProxyChallengeProcessor(headers, proxyConfiguration.getAuthenticator());
289+
} else {
290+
final Set<ProxyAuthenticationType> supportedTypes = getAuthenticationTypes(headers);
291+
// The proxy did not successfully connect, user has specified that they want a particular
292+
// authentication method, but it is not in list of supported authentication methods.
293+
if (proxyConfiguration != null && !supportedTypes.contains(proxyConfiguration.authentication())) {
294+
if (LOGGER.isErrorEnabled()) {
295+
LOGGER.error(
296+
"Proxy authentication required. User configured: '{}', but supported proxy authentication methods are: {}",
297+
proxyConfiguration.authentication(),
298+
supportedTypes.stream().map(type -> type.toString()).collect(Collectors.joining(",")));
299+
}
300+
closeTailProxyError(PROXY_CONNECT_USER_ERROR + PROXY_CONNECT_FAILED
283301
+ connectResponse);
284-
break;
285-
}
286-
287-
final List<String> challenges = headers.getOrDefault(PROXY_AUTHENTICATE, new ArrayList<>());
288-
final ProxyChallengeProcessor processor = proxyConfiguration != null
302+
break;
303+
}
304+
processor = proxyConfiguration != null
289305
? getChallengeProcessor(host, challenges, proxyConfiguration.authentication())
290306
: getChallengeProcessor(host, challenges, supportedTypes);
307+
}
291308

292309
if (processor != null) {
293310
proxyState = ProxyState.PN_PROXY_CHALLENGE;
294311
ProxyImpl.this.headers = processor.getHeader();
312+
if (proxyConfiguration != null
313+
&& proxyConfiguration.getAuthenticator() != null && !testAuthorizeHeader(challenges, ProxyImpl.this.headers)) {
314+
closeTailProxyError("User error: ProxyAuthenticator did not provide a valid authorization header.");
315+
}
295316
} else {
296317
LOGGER.warn("Could not get ProxyChallengeProcessor for challenges.");
297318
closeTailProxyError(PROXY_CONNECT_FAILED + String.join(";", challenges));
@@ -540,4 +561,39 @@ private ProxyResponse readProxyResponse(ByteBuffer buffer) {
540561
return proxyResponse.get();
541562
}
542563
}
564+
565+
private static final class DelegatedProxyChallengeProcessor implements ProxyChallengeProcessor {
566+
private final Map<String, List<String>> headers;
567+
private final com.microsoft.azure.proton.transport.proxy.ProxyAuthenticator authenticator;
568+
569+
DelegatedProxyChallengeProcessor(Map<String, List<String>> headers,
570+
com.microsoft.azure.proton.transport.proxy.ProxyAuthenticator authenticator) {
571+
this.headers = Objects.requireNonNull(headers, "'headers' cannot be null.");
572+
this.authenticator = Objects.requireNonNull(authenticator, "'authenticator' cannot be null.");
573+
}
574+
575+
@Override
576+
public Map<String, String> getHeader() {
577+
// the call site ensured that the 'headers' contain 'Proxy-Authenticate' header with the authentication schemes.
578+
final String authorizedHeader = authenticator.authenticate(ChallengeResponseAccessHelper.internalCreate(headers));
579+
final Map<String, String> headers = new HashMap<>(1);
580+
headers.put(Constants.PROXY_AUTHORIZATION, authorizedHeader);
581+
return headers;
582+
}
583+
}
584+
585+
private boolean testAuthorizeHeader(List<String> challenges, Map<String, String> headers) {
586+
assert !challenges.isEmpty();
587+
final String authorizeHeader = headers.get(Constants.PROXY_AUTHORIZATION);
588+
if (authorizeHeader == null || authorizeHeader.trim().isEmpty()) {
589+
return false;
590+
}
591+
final String value = authorizeHeader.toLowerCase(Locale.ROOT);
592+
for (String scheme : challenges) {
593+
if (value.startsWith(scheme.toLowerCase(Locale.ROOT) + " ")) {
594+
return true;
595+
}
596+
}
597+
return false;
598+
}
543599
}

0 commit comments

Comments
 (0)