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.AuthCache;
39  import org.apache.hc.client5.http.auth.AuthChallenge;
40  import org.apache.hc.client5.http.auth.AuthExchange;
41  import org.apache.hc.client5.http.auth.AuthScheme;
42  import org.apache.hc.client5.http.auth.AuthStateCacheable;
43  import org.apache.hc.client5.http.auth.AuthenticationException;
44  import org.apache.hc.client5.http.auth.ChallengeType;
45  import org.apache.hc.client5.http.auth.CredentialsProvider;
46  import org.apache.hc.client5.http.auth.MalformedChallengeException;
47  import org.apache.hc.client5.http.protocol.HttpClientContext;
48  import org.apache.hc.core5.annotation.Contract;
49  import org.apache.hc.core5.annotation.Internal;
50  import org.apache.hc.core5.annotation.ThreadingBehavior;
51  import org.apache.hc.core5.http.FormattedHeader;
52  import org.apache.hc.core5.http.Header;
53  import org.apache.hc.core5.http.HttpHeaders;
54  import org.apache.hc.core5.http.HttpHost;
55  import org.apache.hc.core5.http.HttpRequest;
56  import org.apache.hc.core5.http.HttpResponse;
57  import org.apache.hc.core5.http.HttpStatus;
58  import org.apache.hc.core5.http.ParseException;
59  import org.apache.hc.core5.http.message.BasicHeader;
60  import org.apache.hc.core5.http.message.ParserCursor;
61  import org.apache.hc.core5.http.protocol.HttpContext;
62  import org.apache.hc.core5.util.Asserts;
63  import org.apache.hc.core5.util.CharArrayBuffer;
64  import org.slf4j.Logger;
65  import org.slf4j.LoggerFactory;
66  
67  /**
68   * Utility class that implements commons aspects of the client side HTTP authentication.
69   *
70   * @since 4.3
71   */
72  @Contract(threading = ThreadingBehavior.STATELESS)
73  public final class HttpAuthenticator {
74  
75      private static final Logger DEFAULT_LOGGER = LoggerFactory.getLogger(HttpAuthenticator.class);
76  
77      private final Logger log;
78      private final AuthChallengeParser parser;
79  
80      @Internal
81      public HttpAuthenticator(final Logger log) {
82          super();
83          this.log = log != null ? log : DEFAULT_LOGGER;
84          this.parser = new AuthChallengeParser();
85      }
86  
87      public HttpAuthenticator() {
88          this(null);
89      }
90  
91      /**
92       * Determines whether the given response represents an authentication challenge.
93       *
94       * @param host the hostname of the opposite endpoint.
95       * @param challengeType the challenge type (target or proxy).
96       * @param response the response message head.
97       * @param authExchange the current authentication exchange state.
98       * @param context the current execution context.
99       * @return {@code true} if the response message represents an authentication challenge,
100      *   {@code false} otherwise.
101      */
102     public boolean isChallenged(
103             final HttpHost host,
104             final ChallengeType challengeType,
105             final HttpResponse response,
106             final AuthExchange authExchange,
107             final HttpContext context) {
108         final int challengeCode;
109         switch (challengeType) {
110             case TARGET:
111                 challengeCode = HttpStatus.SC_UNAUTHORIZED;
112                 break;
113             case PROXY:
114                 challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED;
115                 break;
116             default:
117                 throw new IllegalStateException("Unexpected challenge type: " + challengeType);
118         }
119 
120         final HttpClientContext clientContext = HttpClientContext.adapt(context);
121 
122         if (response.getCode() == challengeCode) {
123             log.debug("Authentication required");
124             if (authExchange.getState() == AuthExchange.State.SUCCESS) {
125                 clearCache(host, clientContext);
126             }
127             return true;
128         }
129         switch (authExchange.getState()) {
130         case CHALLENGED:
131         case HANDSHAKE:
132             log.debug("Authentication succeeded");
133             authExchange.setState(AuthExchange.State.SUCCESS);
134             updateCache(host, authExchange.getAuthScheme(), clientContext);
135             break;
136         case SUCCESS:
137             break;
138         default:
139             authExchange.setState(AuthExchange.State.UNCHALLENGED);
140         }
141         return false;
142     }
143 
144     /**
145      * Updates the {@link AuthExchange} state based on the challenge presented in the response message
146      * using the given {@link AuthenticationStrategy}.
147      *
148      * @param host the hostname of the opposite endpoint.
149      * @param challengeType the challenge type (target or proxy).
150      * @param response the response message head.
151      * @param authStrategy the authentication strategy.
152      * @param authExchange the current authentication exchange state.
153      * @param context the current execution context.
154      * @return {@code true} if the authentication state has been updated,
155      *   {@code false} if unchanged.
156      */
157     public boolean updateAuthState(
158             final HttpHost host,
159             final ChallengeType challengeType,
160             final HttpResponse response,
161             final AuthenticationStrategy authStrategy,
162             final AuthExchange authExchange,
163             final HttpContext context) {
164 
165         if (log.isDebugEnabled()) {
166             log.debug("{} requested authentication", host.toHostString());
167         }
168 
169         final HttpClientContext clientContext = HttpClientContext.adapt(context);
170 
171         final Header[] headers = response.getHeaders(
172                 challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE);
173         final Map<String, AuthChallenge> challengeMap = new HashMap<>();
174         for (final Header header: headers) {
175             final CharArrayBuffer buffer;
176             final int pos;
177             if (header instanceof FormattedHeader) {
178                 buffer = ((FormattedHeader) header).getBuffer();
179                 pos = ((FormattedHeader) header).getValuePos();
180             } else {
181                 final String s = header.getValue();
182                 if (s == null) {
183                     continue;
184                 }
185                 buffer = new CharArrayBuffer(s.length());
186                 buffer.append(s);
187                 pos = 0;
188             }
189             final ParserCursor cursor = new ParserCursor(pos, buffer.length());
190             final List<AuthChallenge> authChallenges;
191             try {
192                 authChallenges = parser.parse(challengeType, buffer, cursor);
193             } catch (final ParseException ex) {
194                 if (log.isWarnEnabled()) {
195                     log.warn("Malformed challenge: {}", header.getValue());
196                 }
197                 continue;
198             }
199             for (final AuthChallenge authChallenge: authChallenges) {
200                 final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT);
201                 if (!challengeMap.containsKey(schemeName)) {
202                     challengeMap.put(schemeName, authChallenge);
203                 }
204             }
205         }
206         if (challengeMap.isEmpty()) {
207             log.debug("Response contains no valid authentication challenges");
208             clearCache(host, clientContext);
209             authExchange.reset();
210             return false;
211         }
212 
213         switch (authExchange.getState()) {
214             case FAILURE:
215                 return false;
216             case SUCCESS:
217                 authExchange.reset();
218                 break;
219             case CHALLENGED:
220             case HANDSHAKE:
221                 Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme");
222             case UNCHALLENGED:
223                 final AuthScheme authScheme = authExchange.getAuthScheme();
224                 if (authScheme != null) {
225                     final String schemeName = authScheme.getName();
226                     final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
227                     if (challenge != null) {
228                         log.debug("Authorization challenge processed");
229                         try {
230                             authScheme.processChallenge(challenge, context);
231                         } catch (final MalformedChallengeException ex) {
232                             if (log.isWarnEnabled()) {
233                                 log.warn(ex.getMessage());
234                             }
235                             clearCache(host, clientContext);
236                             authExchange.reset();
237                             return false;
238                         }
239                         if (authScheme.isChallengeComplete()) {
240                             log.debug("Authentication failed");
241                             clearCache(host, clientContext);
242                             authExchange.reset();
243                             authExchange.setState(AuthExchange.State.FAILURE);
244                             return false;
245                         }
246                         authExchange.setState(AuthExchange.State.HANDSHAKE);
247                         return true;
248                     }
249                     authExchange.reset();
250                     // Retry authentication with a different scheme
251                 }
252         }
253 
254         final List<AuthScheme> preferredSchemes = authStrategy.select(challengeType, challengeMap, context);
255         final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
256         if (credsProvider == null) {
257             log.debug("Credentials provider not set in the context");
258             return false;
259         }
260 
261         final Queue<AuthScheme> authOptions = new LinkedList<>();
262         log.debug("Selecting authentication options");
263         for (final AuthScheme authScheme: preferredSchemes) {
264             try {
265                 final String schemeName = authScheme.getName();
266                 final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
267                 authScheme.processChallenge(challenge, context);
268                 if (authScheme.isResponseReady(host, credsProvider, context)) {
269                     authOptions.add(authScheme);
270                 }
271             } catch (final AuthenticationException | MalformedChallengeException ex) {
272                 if (log.isWarnEnabled()) {
273                     log.warn(ex.getMessage());
274                 }
275             }
276         }
277         if (!authOptions.isEmpty()) {
278             if (log.isDebugEnabled()) {
279                 log.debug("Selected authentication options: {}", authOptions);
280             }
281             authExchange.reset();
282             authExchange.setState(AuthExchange.State.CHALLENGED);
283             authExchange.setOptions(authOptions);
284             return true;
285         }
286         return false;
287     }
288 
289     /**
290      * Generates a response to the authentication challenge based on the actual {@link AuthExchange} state
291      * and adds it to the given {@link HttpRequest} message .
292      *
293      * @param host the hostname of the opposite endpoint.
294      * @param challengeType the challenge type (target or proxy).
295      * @param request the request message head.
296      * @param authExchange the current authentication exchange state.
297      * @param context the current execution context.
298      */
299     public void addAuthResponse(
300             final HttpHost host,
301             final ChallengeType challengeType,
302             final HttpRequest request,
303             final AuthExchange authExchange,
304             final HttpContext context) {
305         AuthScheme authScheme = authExchange.getAuthScheme();
306         switch (authExchange.getState()) {
307         case FAILURE:
308             return;
309         case SUCCESS:
310             Asserts.notNull(authScheme, "AuthScheme");
311             if (authScheme.isConnectionBased()) {
312                 return;
313             }
314             break;
315         case HANDSHAKE:
316             Asserts.notNull(authScheme, "AuthScheme");
317             break;
318         case CHALLENGED:
319             final Queue<AuthScheme> authOptions = authExchange.getAuthOptions();
320             if (authOptions != null) {
321                 while (!authOptions.isEmpty()) {
322                     authScheme = authOptions.remove();
323                     authExchange.select(authScheme);
324                     if (log.isDebugEnabled()) {
325                         log.debug("Generating response to an authentication challenge using {} scheme", authScheme.getName());
326                     }
327                     try {
328                         final String authResponse = authScheme.generateAuthResponse(host, request, context);
329                         final Header header = new BasicHeader(
330                                 challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
331                                 authResponse);
332                         request.addHeader(header);
333                         break;
334                     } catch (final AuthenticationException ex) {
335                         if (log.isWarnEnabled()) {
336                             log.warn("{} authentication error: {}", authScheme, ex.getMessage());
337                         }
338                     }
339                 }
340                 return;
341             }
342             Asserts.notNull(authScheme, "AuthScheme");
343         default:
344         }
345         if (authScheme != null) {
346             try {
347                 final String authResponse = authScheme.generateAuthResponse(host, request, context);
348                 final Header header = new BasicHeader(
349                         challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
350                         authResponse);
351                 request.addHeader(header);
352             } catch (final AuthenticationException ex) {
353                 if (log.isErrorEnabled()) {
354                     log.error("{} authentication error: {}", authScheme, ex.getMessage());
355                 }
356             }
357         }
358     }
359 
360     private void updateCache(final HttpHost host, final AuthScheme authScheme, final HttpClientContext clientContext) {
361         final boolean cachable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null;
362         if (cachable) {
363             AuthCache authCache = clientContext.getAuthCache();
364             if (authCache == null) {
365                 authCache = new BasicAuthCache();
366                 clientContext.setAuthCache(authCache);
367             }
368             if (log.isDebugEnabled()) {
369                 log.debug("Caching '{}' auth scheme for {}", authScheme.getName(), host);
370             }
371             authCache.put(host, authScheme);
372         }
373     }
374 
375     private void clearCache(final HttpHost host, final HttpClientContext clientContext) {
376 
377         final AuthCache authCache = clientContext.getAuthCache();
378         if (authCache != null) {
379             if (log.isDebugEnabled()) {
380                 log.debug("Clearing cached auth scheme for {}", host);
381             }
382             authCache.remove(host);
383         }
384     }
385 
386 }