stoty commented on code in PR #577: URL: https://github.com/apache/httpcomponents-client/pull/577#discussion_r1814851771
########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java: ########## @@ -95,7 +99,7 @@ public List<AuthScheme> select( Collection<String> authPrefs = challengeType == ChallengeType.TARGET ? config.getTargetPreferredAuthSchemes() : config.getProxyPreferredAuthSchemes(); if (authPrefs == null) { - authPrefs = DEFAULT_SCHEME_PRIORITY; + authPrefs = getSchemePriority(); Review Comment: This is a convenience change, to make it easier to re-add the SpnegoScheme without having to fully duplicate this class. ########## 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 + * <http://www.apache.org/>. + * + */ +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 the 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( Review Comment: When processing a response with mutual a auth token, we don't send a new request, hence we must be able to signal the authentication failiure to the caller, and MalformedChallengeException is not semantically correct for that. We also need additional information, the HttpContext, and the challenged flag to be able to verify the received mutual auth token. The old method did not verify anything, it just stored the token, an deferred processing to the generateAuthResponse() method. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java: ########## @@ -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 { Review Comment: This is corresponds to to the processChallenge() change in AuthScheme2, we need to be able to signal the authentication failure to the caller even when the server considers the authentication complete, and does not challenge us. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java: ########## @@ -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 { Review Comment: This is corresponds to to the processChallenge() change in AuthScheme2, as discussed above. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java: ########## @@ -330,7 +334,7 @@ private boolean needAuthentication( } } - if (targetAuthRequested) { + if (targetAuthRequested || targetMutualAuthRequired) { Review Comment: make sure we call the authenticatior if we expect a challenge, if the server didn't send it. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java: ########## @@ -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); Review Comment: The ..mutualAuthRequired variables are set if we still need a challenge with the mutual auth response. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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 { Review Comment: Tha same pattern, we add the new exceptions because we need to evaluate the challenge early, possibly even when there is no challenge, and throw an exception if we didn't get the correct mutual auth token. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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); Review Comment: The same logic as in the ...Exec classes, only one level lower, we need to know if the server MUST send a response with the mutual auth. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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<String, AuthChallenge> 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) { Review Comment: Same old change, in mutual auth mode we must make sure to push down the non-existent challenge to AuthScheme2, which is going to throw an exception. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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<String, AuthChallenge> 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(); Review Comment: Previously, if there was no challenge, then we could return here indicating that the authentication process has finished. However, if we have mutual auth enabled, but didn't get a challenge, then the authentication is not finished, and must continue processing. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java: ########## @@ -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 { Review Comment: I am bit hazy on Connection oriented Auth schemes, but I think that in reality, there is no valid combination where a connection oriented scheme is used with Spengo, so this is only about conforming to the interface change. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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<String, AuthChallenge> extractChallengeMap(final ChallengeType challengeType, Review Comment: This is just factored out from updateAuthState for readability, it can be reverted if needed. ########## httpclient5/src/main/java/org/apache/hc/client5/http/auth/KerberosConfig.java: ########## @@ -151,11 +159,17 @@ public Builder setRequestDelegCreds(final Option requestDelegCreds) { return this; } + public Builder setRequestMutualAuth(final Option requestMutualAuth) { Review Comment: This the new option to enable mutual auth. Michal suggested changing this API, which is certainly possible, especially now, that we have duplicated the Scheme objects, and require changes in callers anyway. However, this configuration API is indenpendent of the deeper changes. ########## 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 + * <http://www.apache.org/>. + * + */ +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 the 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. + * + * @return true if responses with non 401/407 response codes must be processed by the scheme. + * @since 5.5 + */ + boolean isChallengeExpected(); Review Comment: This is the other half of the mutual auth handling. If mutual auth is requested, then the server MUST send a challenge with the mutual auth token (which is not really a "challenge", as a response is not expected). If this returns true, but the server does not send a challenge, then mutual authentication failed, and an Exception is thrown to the user. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java: ########## @@ -528,7 +531,7 @@ private boolean needAuthentication( } } - if (proxyAuthRequested) { + if (proxyAuthRequested || proxyMutualAuthRequired) { Review Comment: We must call the authenticator if we except a mutual auth response. Again, I don't think this can happen in this specific class. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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<String, AuthChallenge> 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); Review Comment: Extra info for AuthScheme2 ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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); Review Comment: Getting exchangeId lazily for the debug message, because of the refactor above. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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) { Review Comment: This is glue code so that both AuthScheme and AuthScheme2 can be handled, always return false for AuthScheme, which does not support mutual auth. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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)) { Review Comment: same code factored out to the new checkChallenged method, no change in operation. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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) { Review Comment: This is factored out from isChallenged, but does the same. We need a way to check if we are challenged, without doing the state updates that isChallenged() does, this is the non state-changing check part. We call this from isChallenged(), which originally included this code, and also from updateAuthState, where we need to determine if the server sent a challenged, but do not want to change the Auth state. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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); Review Comment: Did the server send a challenge ? ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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<String, AuthChallenge> 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) { Review Comment: AuthChallenge.SUCCESS only indicates that the server hasn't sent a response. If the server should have sent a response, then we must continue processing. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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<String, AuthChallenge> 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; Review Comment: Mutual auth case, throw exception, because the server has sent a 200 or equvalent code, and we will simply return the response to the client otherwise, even if the AuthExchange.STATE is Failure. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java: ########## @@ -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); Review Comment: For old AuthChallenge schemes, this is always false. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: dev-unsubscr...@hc.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@hc.apache.org For additional commands, e-mail: dev-h...@hc.apache.org