View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.impl.auth;
29  
30  import java.util.HashMap;
31  import java.util.LinkedList;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.Queue;
36  
37  import org.apache.hc.client5.http.AuthenticationStrategy;
38  import org.apache.hc.client5.http.auth.AuthChallenge;
39  import org.apache.hc.client5.http.auth.AuthExchange;
40  import org.apache.hc.client5.http.auth.AuthScheme;
41  import org.apache.hc.client5.http.auth.AuthenticationException;
42  import org.apache.hc.client5.http.auth.ChallengeType;
43  import org.apache.hc.client5.http.auth.CredentialsProvider;
44  import org.apache.hc.client5.http.auth.MalformedChallengeException;
45  import org.apache.hc.client5.http.protocol.HttpClientContext;
46  import org.apache.hc.core5.annotation.Contract;
47  import org.apache.hc.core5.annotation.ThreadingBehavior;
48  import org.apache.hc.core5.http.FormattedHeader;
49  import org.apache.hc.core5.http.Header;
50  import org.apache.hc.core5.http.HttpHeaders;
51  import org.apache.hc.core5.http.HttpHost;
52  import org.apache.hc.core5.http.HttpRequest;
53  import org.apache.hc.core5.http.HttpResponse;
54  import org.apache.hc.core5.http.HttpStatus;
55  import org.apache.hc.core5.http.ParseException;
56  import org.apache.hc.core5.http.message.BasicHeader;
57  import org.apache.hc.core5.http.message.ParserCursor;
58  import org.apache.hc.core5.http.protocol.HttpContext;
59  import org.apache.hc.core5.util.Asserts;
60  import org.apache.hc.core5.util.CharArrayBuffer;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * Utility class that implements commons aspects of the client side HTTP authentication.
66   * <p>
67   * Please note that since version 5.2 this class no longer updated the authentication cache
68   * bound to the execution context.
69   *
70   * @since 4.3
71   */
72  @Contract(threading = ThreadingBehavior.STATELESS)
73  public final class HttpAuthenticator {
74  
75      private static final Logger LOG = LoggerFactory.getLogger(HttpAuthenticator.class);
76  
77      private final AuthChallengeParser parser;
78  
79      public HttpAuthenticator() {
80          this.parser = new AuthChallengeParser();
81      }
82  
83      /**
84       * Determines whether the given response represents an authentication challenge.
85       *
86       * @param host the hostname of the opposite endpoint.
87       * @param challengeType the challenge type (target or proxy).
88       * @param response the response message head.
89       * @param authExchange the current authentication exchange state.
90       * @param context the current execution context.
91       * @return {@code true} if the response message represents an authentication challenge,
92       *   {@code false} otherwise.
93       */
94      public boolean isChallenged(
95              final HttpHost host,
96              final ChallengeType challengeType,
97              final HttpResponse response,
98              final AuthExchange authExchange,
99              final HttpContext context) {
100         final int challengeCode;
101         switch (challengeType) {
102             case TARGET:
103                 challengeCode = HttpStatus.SC_UNAUTHORIZED;
104                 break;
105             case PROXY:
106                 challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED;
107                 break;
108             default:
109                 throw new IllegalStateException("Unexpected challenge type: " + challengeType);
110         }
111 
112         final HttpClientContext clientContext = HttpClientContext.adapt(context);
113         final String exchangeId = clientContext.getExchangeId();
114 
115         if (response.getCode() == challengeCode) {
116             if (LOG.isDebugEnabled()) {
117                 LOG.debug("{} Authentication required", exchangeId);
118             }
119             return true;
120         }
121         switch (authExchange.getState()) {
122         case CHALLENGED:
123         case HANDSHAKE:
124             if (LOG.isDebugEnabled()) {
125                 LOG.debug("{} Authentication succeeded", exchangeId);
126             }
127             authExchange.setState(AuthExchange.State.SUCCESS);
128             break;
129         case SUCCESS:
130             break;
131         default:
132             authExchange.setState(AuthExchange.State.UNCHALLENGED);
133         }
134         return false;
135     }
136 
137     /**
138      * Updates the {@link AuthExchange} state based on the challenge presented in the response message
139      * using the given {@link AuthenticationStrategy}.
140      *
141      * @param host the hostname of the opposite endpoint.
142      * @param challengeType the challenge type (target or proxy).
143      * @param response the response message head.
144      * @param authStrategy the authentication strategy.
145      * @param authExchange the current authentication exchange state.
146      * @param context the current execution context.
147      * @return {@code true} if the authentication state has been updated,
148      *   {@code false} if unchanged.
149      */
150     public boolean updateAuthState(
151             final HttpHost host,
152             final ChallengeType challengeType,
153             final HttpResponse response,
154             final AuthenticationStrategy authStrategy,
155             final AuthExchange authExchange,
156             final HttpContext context) {
157 
158         final HttpClientContext clientContext = HttpClientContext.adapt(context);
159         final String exchangeId = clientContext.getExchangeId();
160 
161         if (LOG.isDebugEnabled()) {
162             LOG.debug("{} {} requested authentication", exchangeId, host.toHostString());
163         }
164 
165         final Header[] headers = response.getHeaders(
166                 challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE);
167         final Map<String, AuthChallenge> challengeMap = new HashMap<>();
168         for (final Header header: headers) {
169             final CharArrayBuffer buffer;
170             final int pos;
171             if (header instanceof FormattedHeader) {
172                 buffer = ((FormattedHeader) header).getBuffer();
173                 pos = ((FormattedHeader) header).getValuePos();
174             } else {
175                 final String s = header.getValue();
176                 if (s == null) {
177                     continue;
178                 }
179                 buffer = new CharArrayBuffer(s.length());
180                 buffer.append(s);
181                 pos = 0;
182             }
183             final ParserCursor cursor = new ParserCursor(pos, buffer.length());
184             final List<AuthChallenge> authChallenges;
185             try {
186                 authChallenges = parser.parse(challengeType, buffer, cursor);
187             } catch (final ParseException ex) {
188                 if (LOG.isWarnEnabled()) {
189                     LOG.warn("{} Malformed challenge: {}", exchangeId, header.getValue());
190                 }
191                 continue;
192             }
193             for (final AuthChallenge authChallenge: authChallenges) {
194                 final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT);
195                 if (!challengeMap.containsKey(schemeName)) {
196                     challengeMap.put(schemeName, authChallenge);
197                 }
198             }
199         }
200         if (challengeMap.isEmpty()) {
201             if (LOG.isDebugEnabled()) {
202                 LOG.debug("{} Response contains no valid authentication challenges", exchangeId);
203             }
204             authExchange.reset();
205             return false;
206         }
207 
208         switch (authExchange.getState()) {
209             case FAILURE:
210                 return false;
211             case SUCCESS:
212                 authExchange.reset();
213                 break;
214             case CHALLENGED:
215             case HANDSHAKE:
216                 Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme");
217             case UNCHALLENGED:
218                 final AuthScheme authScheme = authExchange.getAuthScheme();
219                 if (authScheme != null) {
220                     final String schemeName = authScheme.getName();
221                     final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
222                     if (challenge != null) {
223                         if (LOG.isDebugEnabled()) {
224                             LOG.debug("{} Authorization challenge processed", exchangeId);
225                         }
226                         try {
227                             authScheme.processChallenge(challenge, context);
228                         } catch (final MalformedChallengeException ex) {
229                             if (LOG.isWarnEnabled()) {
230                                 LOG.warn("{} {}", exchangeId, ex.getMessage());
231                             }
232                             authExchange.reset();
233                             authExchange.setState(AuthExchange.State.FAILURE);
234                             return false;
235                         }
236                         if (authScheme.isChallengeComplete()) {
237                             if (LOG.isDebugEnabled()) {
238                                 LOG.debug("{} Authentication failed", exchangeId);
239                             }
240                             authExchange.reset();
241                             authExchange.setState(AuthExchange.State.FAILURE);
242                             return false;
243                         }
244                         authExchange.setState(AuthExchange.State.HANDSHAKE);
245                         return true;
246                     }
247                     authExchange.reset();
248                     // Retry authentication with a different scheme
249                 }
250         }
251 
252         final List<AuthScheme> preferredSchemes = authStrategy.select(challengeType, challengeMap, context);
253         final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
254         if (credsProvider == null) {
255             if (LOG.isDebugEnabled()) {
256                 LOG.debug("{} Credentials provider not set in the context", exchangeId);
257             }
258             return false;
259         }
260 
261         final Queue<AuthScheme> authOptions = new LinkedList<>();
262         if (LOG.isDebugEnabled()) {
263             LOG.debug("{} Selecting authentication options", exchangeId);
264         }
265         for (final AuthScheme authScheme: preferredSchemes) {
266             try {
267                 final String schemeName = authScheme.getName();
268                 final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
269                 authScheme.processChallenge(challenge, context);
270                 if (authScheme.isResponseReady(host, credsProvider, context)) {
271                     authOptions.add(authScheme);
272                 }
273             } catch (final AuthenticationException | MalformedChallengeException ex) {
274                 if (LOG.isWarnEnabled()) {
275                     LOG.warn(ex.getMessage());
276                 }
277             }
278         }
279         if (!authOptions.isEmpty()) {
280             if (LOG.isDebugEnabled()) {
281                 LOG.debug("{} Selected authentication options: {}", exchangeId, authOptions);
282             }
283             authExchange.reset();
284             authExchange.setState(AuthExchange.State.CHALLENGED);
285             authExchange.setOptions(authOptions);
286             return true;
287         }
288         return false;
289     }
290 
291     /**
292      * Generates a response to the authentication challenge based on the actual {@link AuthExchange} state
293      * and adds it to the given {@link HttpRequest} message .
294      *
295      * @param host the hostname of the opposite endpoint.
296      * @param challengeType the challenge type (target or proxy).
297      * @param request the request message head.
298      * @param authExchange the current authentication exchange state.
299      * @param context the current execution context.
300      */
301     public void addAuthResponse(
302             final HttpHost host,
303             final ChallengeType challengeType,
304             final HttpRequest request,
305             final AuthExchange authExchange,
306             final HttpContext context) {
307         final HttpClientContext clientContext = HttpClientContext.adapt(context);
308         final String exchangeId = clientContext.getExchangeId();
309         AuthScheme authScheme = authExchange.getAuthScheme();
310         switch (authExchange.getState()) {
311         case FAILURE:
312             return;
313         case SUCCESS:
314             Asserts.notNull(authScheme, "AuthScheme");
315             if (authScheme.isConnectionBased()) {
316                 return;
317             }
318             break;
319         case HANDSHAKE:
320             Asserts.notNull(authScheme, "AuthScheme");
321             break;
322         case CHALLENGED:
323             final Queue<AuthScheme> authOptions = authExchange.getAuthOptions();
324             if (authOptions != null) {
325                 while (!authOptions.isEmpty()) {
326                     authScheme = authOptions.remove();
327                     authExchange.select(authScheme);
328                     if (LOG.isDebugEnabled()) {
329                         LOG.debug("{} Generating response to an authentication challenge using {} scheme",
330                                 exchangeId, authScheme.getName());
331                     }
332                     try {
333                         final String authResponse = authScheme.generateAuthResponse(host, request, context);
334                         final Header header = new BasicHeader(
335                                 challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
336                                 authResponse);
337                         request.addHeader(header);
338                         break;
339                     } catch (final AuthenticationException ex) {
340                         if (LOG.isWarnEnabled()) {
341                             LOG.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
342                         }
343                     }
344                 }
345                 return;
346             }
347             Asserts.notNull(authScheme, "AuthScheme");
348         default:
349         }
350         if (authScheme != null) {
351             try {
352                 final String authResponse = authScheme.generateAuthResponse(host, request, context);
353                 final Header header = new BasicHeader(
354                         challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
355                         authResponse);
356                 request.addHeader(header);
357             } catch (final AuthenticationException ex) {
358                 if (LOG.isErrorEnabled()) {
359                     LOG.error("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
360                 }
361             }
362         }
363     }
364 
365 }