diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java index bce1873ea9..1df164a8c6 100644 --- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java +++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/async/StandardTestClientBuilder.java @@ -33,6 +33,7 @@ import org.apache.hc.client5.http.HttpRequestRetryStrategy; import org.apache.hc.client5.http.UserTokenHandler; import org.apache.hc.client5.http.auth.AuthSchemeFactory; +import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; @@ -159,6 +160,12 @@ public TestAsyncClientBuilder setDefaultAuthSchemeRegistry(final Lookup + *

+ * This interface cannot correctly handle some authentication methods, like SPNEGO. + * See {@link AuthScheme2} for a more capable interface. + *

* * @since 4.0 */ @@ -128,6 +132,9 @@ void processChallenge( * successfully or unsuccessfully), that is, all the required authorization * challenges have been processed in their entirety. * + * Note that due to some assumptions made about the control flow by the authentication code + * returning true will immediately cause the authentication process to fail. + * * @return {@code true} if the authentication process has been completed, * {@code false} otherwise. * diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java new file mode 100644 index 0000000000..12845d3b74 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/AuthScheme2.java @@ -0,0 +1,102 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ +package org.apache.hc.client5.http.auth; + +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * This is an improved version of the {@link AuthScheme} interface, amended to be able to handle + * a conversation involving multiple challenge-response transactions and adding the ability to check + * the results of a final token sent together with the successful HTTP request as required by + * RFC 4559 and RFC 7546. + * + * @since 5.5 + */ +public interface AuthScheme2 extends AuthScheme { + + /** + * Processes the given auth challenge. Some authentication schemes may involve multiple + * challenge-response exchanges. Such schemes must be able to maintain internal state + * when dealing with sequential challenges. + * + * The {@link AuthScheme} interface implicitly assumes that that the token passed here is + * simply stored in this method, and the actual authentication takes place in + * {@link org.apache.hc.client5.http.auth.AuthScheme#generateAuthResponse(HttpHost, HttpRequest, HttpContext) generateAuthResponse } + * and/or {@link org.apache.hc.client5.http.auth.AuthScheme#isResponseReady(HttpHost, HttpRequest, HttpContext) generateAuthResponse }, + * as only those methods receive the HttpHost, and only those can throw an + * AuthenticationException. + * + * This new methods signature makes it possible to process the token and throw an + * AuthenticationException immediately even when no response is sent (i.e. processing an SPNEGO or SCRAM mutual + * authentication response) + * + * When {@link isChallengeExpected} returns true, but no challenge was sent, then this method must + * be called with a null {@link AuthChallenge} so that the Scheme can handle this situation. + * + * @param host HTTP host + * @param authChallenge the auth challenge or null if no challenge was received + * @param context HTTP context + * @param challenged true if the response was unauthorised (401/407) + * @throws AuthenticationException in case the authentication process is unsuccessful. + * @since 5.5 + */ + void processChallenge( + HttpHost host, + AuthChallenge authChallenge, + HttpContext context, + boolean challenged) throws AuthenticationException; + + /** + * The old processChallenge signature is unfit for use in AuthScheme2. + * If the old signature is sufficient for a scheme, then it should implement {@link AuthScheme} + * instead AuthScheme2. + */ + @Override + default void processChallenge( + AuthChallenge authChallenge, + HttpContext context) throws MalformedChallengeException { + throw new UnsupportedOperationException("on AuthScheme2 implementations only the four " + + "argument processChallenge method can be called"); + } + + /** + * Indicates that the even authorized (i.e. not 401 or 407) responses must be processed + * by this Scheme. + * + * The original AuthScheme interface only processes unauthorised responses. + * This method indicates that non unauthorised responses are expected to contain challenges + * and must be processed by the Scheme. + * This is required to implement the SPENGO RFC and Kerberos mutual authentication, and SCRAM. + * + * @return true if responses with non 401/407 response codes must be processed by the scheme. + * @since 5.5 + */ + boolean isChallengeExpected(); + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java index 0440a1322f..9409d6cfa4 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java @@ -72,6 +72,10 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy { StandardAuthScheme.DIGEST, StandardAuthScheme.BASIC)); + protected List getSchemePriority() { + return DEFAULT_SCHEME_PRIORITY; + } + @Override public List select( final ChallengeType challengeType, @@ -95,7 +99,7 @@ public List select( Collection authPrefs = challengeType == ChallengeType.TARGET ? config.getTargetPreferredAuthSchemes() : config.getProxyPreferredAuthSchemes(); if (authPrefs == null) { - authPrefs = DEFAULT_SCHEME_PRIORITY; + authPrefs = getSchemePriority(); } if (LOG.isDebugEnabled()) { LOG.debug("{} Authentication schemes in the order of preference: {}", exchangeId, authPrefs); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java index 33802920fa..a97afef70d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java @@ -43,7 +43,9 @@ import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.AsyncExecRuntime; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; @@ -515,10 +517,11 @@ private boolean needAuthentication( final AuthExchange proxyAuthExchange, final HttpHost proxy, final HttpResponse response, - final HttpClientContext context) { + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -528,7 +531,7 @@ private boolean needAuthentication( } } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java index 907b23e46b..579607a6d9 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java @@ -38,7 +38,9 @@ import org.apache.hc.client5.http.async.AsyncExecChainHandler; import org.apache.hc.client5.http.async.AsyncExecRuntime; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; import org.apache.hc.client5.http.impl.RequestSupport; @@ -305,11 +307,12 @@ private boolean needAuthentication( final HttpHost target, final String pathPrefix, final HttpResponse response, - final HttpClientContext context) { + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean targetAuthRequested = authenticator.isChallenged( target, ChallengeType.TARGET, response, targetAuthExchange, context); + final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange); if (authCacheKeeper != null) { if (targetAuthRequested) { @@ -321,6 +324,7 @@ private boolean needAuthentication( final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -330,7 +334,7 @@ private boolean needAuthentication( } } - if (targetAuthRequested) { + if (targetAuthRequested || targetMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); @@ -340,7 +344,7 @@ private boolean needAuthentication( return updated; } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java index cd9f7ce723..9b4553cd37 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java @@ -38,12 +38,14 @@ import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthScheme2; import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.FormattedHeader; import org.apache.hc.core5.http.Header; @@ -69,6 +71,7 @@ * * @since 4.3 */ +@Internal @Contract(threading = ThreadingBehavior.STATELESS) public final class HttpAuthenticator { @@ -81,12 +84,13 @@ public HttpAuthenticator() { } /** - * Determines whether the given response represents an authentication challenge. + * Determines whether the given response represents an authentication challenge, and updates + * the autheExchange status. * * @param host the hostname of the opposite endpoint. * @param challengeType the challenge type (target or proxy). * @param response the response message head. - * @param authExchange the current authentication exchange state. + * @param authExchange the current authentication exchange state. Gets updated. * @param context the current execution context. * @return {@code true} if the response message represents an authentication challenge, * {@code false} otherwise. @@ -97,32 +101,17 @@ public boolean isChallenged( final HttpResponse response, final AuthExchange authExchange, final HttpContext context) { - final int challengeCode; - switch (challengeType) { - case TARGET: - challengeCode = HttpStatus.SC_UNAUTHORIZED; - break; - case PROXY: - challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED; - break; - default: - throw new IllegalStateException("Unexpected challenge type: " + challengeType); - } - - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); - - if (response.getCode() == challengeCode) { - if (LOG.isDebugEnabled()) { - LOG.debug("{} Authentication required", exchangeId); - } + if (checkChallenged(challengeType, response, context)) { return true; } switch (authExchange.getState()) { case CHALLENGED: case HANDSHAKE: if (LOG.isDebugEnabled()) { - LOG.debug("{} Authentication succeeded", exchangeId); + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + // The mutual auth may still fail + LOG.debug("{} Server has accepted authorization", exchangeId); } authExchange.setState(AuthExchange.State.SUCCESS); break; @@ -135,37 +124,64 @@ public boolean isChallenged( } /** - * Updates the {@link AuthExchange} state based on the challenge presented in the response message - * using the given {@link AuthenticationStrategy}. + * Determines whether the given response represents an authentication challenge, without + * changing the AuthExchange state. * - * @param host the hostname of the opposite endpoint. * @param challengeType the challenge type (target or proxy). * @param response the response message head. - * @param authStrategy the authentication strategy. - * @param authExchange the current authentication exchange state. * @param context the current execution context. - * @return {@code true} if the authentication state has been updated, - * {@code false} if unchanged. + * @return {@code true} if the response message represents an authentication challenge, + * {@code false} otherwise. */ - public boolean updateAuthState( - final HttpHost host, - final ChallengeType challengeType, - final HttpResponse response, - final AuthenticationStrategy authStrategy, - final AuthExchange authExchange, - final HttpContext context) { + private boolean checkChallenged(final ChallengeType challengeType, final HttpResponse response, final HttpContext context) { + final int challengeCode; + switch (challengeType) { + case TARGET: + challengeCode = HttpStatus.SC_UNAUTHORIZED; + break; + case PROXY: + challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED; + break; + default: + throw new IllegalStateException("Unexpected challenge type: " + challengeType); + } - final HttpClientContext clientContext = HttpClientContext.cast(context); - final String exchangeId = clientContext.getExchangeId(); + if (response.getCode() == challengeCode) { + if (LOG.isDebugEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Authentication required", exchangeId); + } + return true; + } + return false; + } - if (LOG.isDebugEnabled()) { - LOG.debug("{} {} requested authentication", exchangeId, host.toHostString()); + /** + * Determines if the scheme requires an auth challenge for responses that do not + * have challenge HTTP code. (i.e whether it needs a mutual authentication token) + * + * @param authExchange + * @return true is authExchange's scheme is AuthScheme2, which currently expects + * a WWW-Authenticate header even for authorized HTTP responses + */ + public boolean isChallengeExpected(final AuthExchange authExchange) { + final AuthScheme authScheme = authExchange.getAuthScheme(); + if (authScheme != null && authScheme instanceof AuthScheme2) { + return ((AuthScheme2)authScheme).isChallengeExpected(); + } else { + return false; } + } - final Header[] headers = response.getHeaders( - challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE); + public Map extractChallengeMap(final ChallengeType challengeType, + final HttpResponse response, final HttpClientContext context) { + final Header[] headers = + response.getHeaders( + challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE + : HttpHeaders.WWW_AUTHENTICATE); final Map challengeMap = new HashMap<>(); - for (final Header header: headers) { + for (final Header header : headers) { final CharArrayBuffer buffer; final int pos; if (header instanceof FormattedHeader) { @@ -186,52 +202,109 @@ public boolean updateAuthState( authChallenges = parser.parse(challengeType, buffer, cursor); } catch (final ParseException ex) { if (LOG.isWarnEnabled()) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); LOG.warn("{} Malformed challenge: {}", exchangeId, header.getValue()); } continue; } - for (final AuthChallenge authChallenge: authChallenges) { + for (final AuthChallenge authChallenge : authChallenges) { final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT); if (!challengeMap.containsKey(schemeName)) { challengeMap.put(schemeName, authChallenge); } } } + return challengeMap; + } + + /** + * Updates the {@link AuthExchange} state based on the challenge presented in the response message + * using the given {@link AuthenticationStrategy}. + * + * @param host the hostname of the opposite endpoint. + * @param challengeType the challenge type (target or proxy). + * @param response the response message head. + * @param authStrategy the authentication strategy. + * @param authExchange the current authentication exchange state. + * @param context the current execution context. + * @return {@code true} if the request needs-to be re-sent, + * {@code false} if the authentication is complete (successful or not). + * + * @throws AuthenticationException if the AuthScheme throws one. In most cases this indicates a + * client side problem, as final server error responses are simply returned. + * @throws MalformedChallengeException if the AuthScheme throws one. In most cases this indicates a + * client side problem, as final server error responses are simply returned. + */ + public boolean updateAuthState( + final HttpHost host, + final ChallengeType challengeType, + final HttpResponse response, + final AuthenticationStrategy authStrategy, + final AuthExchange authExchange, + final HttpContext context) throws AuthenticationException, MalformedChallengeException { + + final HttpClientContext clientContext = HttpClientContext.cast(context); + final String exchangeId = clientContext.getExchangeId(); + final boolean challenged = checkChallenged(challengeType, response, context); + final boolean isChallengeExpected = isChallengeExpected(authExchange); + + if (LOG.isDebugEnabled()) { + LOG.debug("{} {} requested authentication", exchangeId, host.toHostString()); + } + + final Map challengeMap = extractChallengeMap(challengeType, response, clientContext); + if (challengeMap.isEmpty()) { if (LOG.isDebugEnabled()) { LOG.debug("{} Response contains no valid authentication challenges", exchangeId); } - authExchange.reset(); - return false; + if (!isChallengeExpected) { + authExchange.reset(); + return false; + } } switch (authExchange.getState()) { case FAILURE: return false; case SUCCESS: - authExchange.reset(); - break; + if (!isChallengeExpected) { + authExchange.reset(); + break; + } + // otherwise fall through case CHALLENGED: + // fall through case HANDSHAKE: Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme"); + // fall through case UNCHALLENGED: final AuthScheme authScheme = authExchange.getAuthScheme(); + // AuthScheme is only set if we have already sent an auth response, either + // because we have received a challenge for it, or preemptively. if (authScheme != null) { final String schemeName = authScheme.getName(); final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); - if (challenge != null) { + if (challenge != null || isChallengeExpected) { if (LOG.isDebugEnabled()) { - LOG.debug("{} Authorization challenge processed", exchangeId); + LOG.debug("{} Processing authorization challenge {}", exchangeId, challenge); } try { - authScheme.processChallenge(challenge, context); - } catch (final MalformedChallengeException ex) { + if (authScheme instanceof AuthScheme2) { + ((AuthScheme2)authScheme).processChallenge(host, challenge, context, challenged); + } else { + authScheme.processChallenge(challenge, context); + } + } catch (final AuthenticationException | MalformedChallengeException ex) { if (LOG.isWarnEnabled()) { - LOG.warn("{} {}", exchangeId, ex.getMessage()); + LOG.warn("Exception processing Challange {}", exchangeId, ex); } authExchange.reset(); authExchange.setState(AuthExchange.State.FAILURE); - return false; + if (isChallengeExpected) { + throw ex; + } } if (authScheme.isChallengeComplete()) { if (LOG.isDebugEnabled()) { @@ -241,7 +314,14 @@ public boolean updateAuthState( authExchange.setState(AuthExchange.State.FAILURE); return false; } - authExchange.setState(AuthExchange.State.HANDSHAKE); + if (!challenged) { + // There are no more challanges sent after the 200 message, + // and if we get here, then the mutual auth phase has succeeded. + authExchange.setState(AuthExchange.State.SUCCESS); + return false; + } else { + authExchange.setState(AuthExchange.State.HANDSHAKE); + } return true; } authExchange.reset(); @@ -249,6 +329,9 @@ public boolean updateAuthState( } } + // We reach this if we fell through above because the authScheme has not yet been set, or if + // we receive a 401/407 response for an unexpected scheme. Normally this processes the first + // 401/407 response final List preferredSchemes = authStrategy.select(challengeType, challengeMap, context); final CredentialsProvider credsProvider = clientContext.getCredentialsProvider(); if (credsProvider == null) { @@ -263,16 +346,23 @@ public boolean updateAuthState( LOG.debug("{} Selecting authentication options", exchangeId); } for (final AuthScheme authScheme: preferredSchemes) { + // We only respond to the the first successfully processed challenge. However, the + // original AuthScheme API does not really process the challenge at this point, so we need + // to process/store each challenge here anyway. try { final String schemeName = authScheme.getName(); final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT)); - authScheme.processChallenge(challenge, context); + if (authScheme instanceof AuthScheme2) { + ((AuthScheme2)authScheme).processChallenge(host, challenge, context, challenged); + } else { + authScheme.processChallenge(challenge, context); + } if (authScheme.isResponseReady(host, credsProvider, context)) { authOptions.add(authScheme); } } catch (final AuthenticationException | MalformedChallengeException ex) { if (LOG.isWarnEnabled()) { - LOG.warn(ex.getMessage()); + LOG.warn("Exception while processing Challange", ex); } } } @@ -331,10 +421,12 @@ public void addAuthResponse( } try { final String authResponse = authScheme.generateAuthResponse(host, request, context); - final Header header = new BasicHeader( - challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, - authResponse); - request.addHeader(header); + if (authResponse != null) { + final Header header = new BasicHeader( + challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION, + authResponse); + request.addHeader(header); + } break; } catch (final AuthenticationException ex) { if (LOG.isWarnEnabled()) { @@ -347,6 +439,9 @@ public void addAuthResponse( Asserts.notNull(authScheme, "AuthScheme"); default: } + // This is the SUCCESS and HANDSHAKE states, same as the initial response. + // This only happens if the handshake requires multiple requests, which is + // unlikely in practice. if (authScheme != null) { try { final String authResponse = authScheme.generateAuthResponse(host, request, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java index 482d6be154..dd36d335a7 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java @@ -253,6 +253,7 @@ private ClassicHttpResponse createTunnelToTarget( if (config.isAuthenticationEnabled()) { final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -262,7 +263,7 @@ private ClassicHttpResponse createTunnelToTarget( } } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java index bfebce0eaf..976c2da5e4 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java @@ -34,7 +34,9 @@ import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.classic.ExecRuntime; @@ -189,6 +191,7 @@ public ClassicHttpResponse execute( authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context); } + // This is where the actual network communication happens (eventually) final ClassicHttpResponse response = chain.proceed(request, scope); if (Method.TRACE.isSame(request.getMethod())) { @@ -218,6 +221,8 @@ public ClassicHttpResponse execute( EntityUtils.consume(responseEntity); } else { execRuntime.disconnectEndpoint(); + // We don't have any connection based AuthScheme2 implementations. + // If one existed, we'd have think about how to handle it if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS && proxyAuthExchange.isConnectionBased()) { if (LOG.isDebugEnabled()) { @@ -265,11 +270,12 @@ private boolean needAuthentication( final HttpHost target, final String pathPrefix, final HttpResponse response, - final HttpClientContext context) { - final RequestConfig config = context.getRequestConfigOrDefault(); + final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { + final RequestConfig config = context.getRequestConfigOrDefault(); if (config.isAuthenticationEnabled()) { final boolean targetAuthRequested = authenticator.isChallenged( target, ChallengeType.TARGET, response, targetAuthExchange, context); + final boolean targetMutualAuthRequired = authenticator.isChallengeExpected(targetAuthExchange); if (authCacheKeeper != null) { if (targetAuthRequested) { @@ -281,6 +287,7 @@ private boolean needAuthentication( final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + final boolean proxyMutualAuthRequired = authenticator.isChallengeExpected(proxyAuthExchange); if (authCacheKeeper != null) { if (proxyAuthRequested) { @@ -290,7 +297,7 @@ private boolean needAuthentication( } } - if (targetAuthRequested) { + if (targetAuthRequested || targetMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); @@ -300,7 +307,7 @@ private boolean needAuthentication( return updated; } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java index a4657a26ab..0f5347495a 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProxyClient.java @@ -175,7 +175,8 @@ public Socket tunnel( if (status < 200) { throw new HttpException("Unexpected response to CONNECT request: " + response); } - if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context)) { + if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, this.proxyAuthExchange, context) + || authenticator.isChallengeExpected(proxyAuthExchange)) { if (this.authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, this.proxyAuthStrategy, this.proxyAuthExchange, context)) { // Retry request diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java index 9107e88016..61df88722c 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java @@ -38,6 +38,7 @@ import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.auth.StandardAuthScheme; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; @@ -150,7 +151,7 @@ void testAuthenticationNotRequestedSuccess2() { } @Test - void testAuthentication() { + void testAuthentication() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -179,7 +180,7 @@ void testAuthentication() { } @Test - void testAuthenticationCredentialsForBasic() { + void testAuthenticationCredentialsForBasic() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); @@ -205,7 +206,7 @@ void testAuthenticationCredentialsForBasic() { } @Test - void testAuthenticationNoChallenges() { + void testAuthenticationNoChallenges() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); @@ -216,7 +217,7 @@ void testAuthenticationNoChallenges() { } @Test - void testAuthenticationNoSupportedChallenges() { + void testAuthenticationNoSupportedChallenges() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "This realm=\"test\"")); @@ -229,7 +230,7 @@ void testAuthenticationNoSupportedChallenges() { } @Test - void testAuthenticationNoCredentials() { + void testAuthenticationNoCredentials() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -242,7 +243,7 @@ void testAuthenticationNoCredentials() { } @Test - void testAuthenticationFailed() { + void testAuthenticationFailed() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -260,7 +261,7 @@ void testAuthenticationFailed() { } @Test - void testAuthenticationFailedPreviously() { + void testAuthenticationFailedPreviously() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -277,7 +278,7 @@ void testAuthenticationFailedPreviously() { } @Test - void testAuthenticationFailure() { + void testAuthenticationFailure() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -295,7 +296,7 @@ void testAuthenticationFailure() { } @Test - void testAuthenticationHandshaking() { + void testAuthenticationHandshaking() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"test\"")); @@ -314,7 +315,7 @@ void testAuthenticationHandshaking() { } @Test - void testAuthenticationNoMatchingChallenge() { + void testAuthenticationNoMatchingChallenge() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.DIGEST + " realm=\"realm1\", nonce=\"1234\"")); @@ -342,7 +343,7 @@ void testAuthenticationNoMatchingChallenge() { } @Test - void testAuthenticationException() { + void testAuthenticationException() throws AuthenticationException, MalformedChallengeException { final HttpHost host = new HttpHost("somehost", 80); final HttpResponse response = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED, "UNAUTHORIZED"); response.addHeader(new BasicHeader(HttpHeaders.WWW_AUTHENTICATE, "blah blah blah")); diff --git a/pom.xml b/pom.xml index 9cc5cda643..69460bc5e5 100644 --- a/pom.xml +++ b/pom.xml @@ -270,6 +270,8 @@ com.github.siom79.japicmp japicmp-maven-plugin + + true ${project.groupId}