1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package org.apache.hc.client5.http.impl.auth;
29
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.LinkedList;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.Queue;
37
38 import org.apache.hc.client5.http.AuthenticationStrategy;
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.AuthenticationException;
43 import org.apache.hc.client5.http.auth.ChallengeType;
44 import org.apache.hc.client5.http.auth.CredentialsProvider;
45 import org.apache.hc.client5.http.auth.MalformedChallengeException;
46 import org.apache.hc.client5.http.protocol.HttpClientContext;
47 import org.apache.hc.core5.annotation.Contract;
48 import org.apache.hc.core5.annotation.Internal;
49 import org.apache.hc.core5.annotation.ThreadingBehavior;
50 import org.apache.hc.core5.http.FormattedHeader;
51 import org.apache.hc.core5.http.Header;
52 import org.apache.hc.core5.http.HttpHeaders;
53 import org.apache.hc.core5.http.HttpHost;
54 import org.apache.hc.core5.http.HttpRequest;
55 import org.apache.hc.core5.http.HttpResponse;
56 import org.apache.hc.core5.http.HttpStatus;
57 import org.apache.hc.core5.http.ParseException;
58 import org.apache.hc.core5.http.message.BasicHeader;
59 import org.apache.hc.core5.http.message.ParserCursor;
60 import org.apache.hc.core5.http.protocol.HttpContext;
61 import org.apache.hc.core5.util.Asserts;
62 import org.apache.hc.core5.util.CharArrayBuffer;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66
67
68
69
70
71
72 @Internal
73 @Contract(threading = ThreadingBehavior.STATELESS)
74 public class AuthenticationHandler {
75
76 private static final Logger LOG = LoggerFactory.getLogger(AuthenticationHandler.class);
77
78 private final AuthChallengeParser parser;
79
80 public AuthenticationHandler() {
81 this.parser = new AuthChallengeParser();
82 }
83
84
85
86
87
88
89
90
91
92
93
94
95
96 public boolean isChallenged(
97 final HttpHost host,
98 final ChallengeType challengeType,
99 final HttpResponse response,
100 final AuthExchange authExchange,
101 final HttpContext context) {
102 final HttpClientContext clientContext = HttpClientContext.cast(context);
103 if (checkChallenged(challengeType, response, clientContext)) {
104 return true;
105 }
106 switch (authExchange.getState()) {
107 case CHALLENGED:
108 case HANDSHAKE:
109 if (LOG.isDebugEnabled()) {
110
111 LOG.debug("{} Server has accepted authentication", clientContext.getExchangeId());
112 }
113 authExchange.setState(AuthExchange.State.SUCCESS);
114 break;
115 case SUCCESS:
116 break;
117 default:
118 authExchange.setState(AuthExchange.State.UNCHALLENGED);
119 }
120 return false;
121 }
122
123
124
125
126
127
128
129
130
131
132
133 private boolean checkChallenged(
134 final ChallengeType challengeType,
135 final HttpResponse response,
136 final HttpClientContext clientContext) {
137 final int challengeCode;
138 switch (challengeType) {
139 case TARGET:
140 challengeCode = HttpStatus.SC_UNAUTHORIZED;
141 break;
142 case PROXY:
143 challengeCode = HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED;
144 break;
145 default:
146 throw new IllegalStateException("Unexpected challenge type: " + challengeType);
147 }
148
149 if (response.getCode() == challengeCode) {
150 if (LOG.isDebugEnabled()) {
151 LOG.debug("{} Authentication required", clientContext.getExchangeId());
152 }
153 return true;
154 }
155 return false;
156 }
157
158
159
160
161
162 public boolean isChallengeExpected(final AuthExchange authExchange) {
163 final AuthScheme authScheme = authExchange.getAuthScheme();
164 return authScheme != null && authScheme.isChallengeExpected();
165 }
166
167 public Map<String, AuthChallenge> extractChallengeMap(
168 final ChallengeType challengeType,
169 final HttpResponse response,
170 final HttpClientContext context) {
171 final Map<String, AuthChallenge> challengeMap = new HashMap<>();
172 final Iterator<Header> headerIterator = response.headerIterator(
173 challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE);
174 while (headerIterator.hasNext()) {
175 final Header header = headerIterator.next();
176 final CharArrayBuffer buffer;
177 final int pos;
178 if (header instanceof FormattedHeader) {
179 buffer = ((FormattedHeader) header).getBuffer();
180 pos = ((FormattedHeader) header).getValuePos();
181 } else {
182 final String s = header.getValue();
183 if (s == null) {
184 continue;
185 }
186 buffer = new CharArrayBuffer(s.length());
187 buffer.append(s);
188 pos = 0;
189 }
190 final ParserCursor cursor = new ParserCursor(pos, buffer.length());
191 final List<AuthChallenge> authChallenges;
192 try {
193 authChallenges = parser.parse(challengeType, buffer, cursor);
194 } catch (final ParseException ex) {
195 if (LOG.isWarnEnabled()) {
196 final HttpClientContext clientContext = HttpClientContext.cast(context);
197 final String exchangeId = clientContext.getExchangeId();
198 LOG.warn("{} Malformed challenge: {}", exchangeId, header.getValue());
199 }
200 continue;
201 }
202 for (final AuthChallenge authChallenge : authChallenges) {
203 final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT);
204 if (!challengeMap.containsKey(schemeName)) {
205 challengeMap.put(schemeName, authChallenge);
206 }
207 }
208 }
209 return challengeMap;
210 }
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231 public boolean handleResponse(
232 final HttpHost host,
233 final ChallengeType challengeType,
234 final HttpResponse response,
235 final AuthenticationStrategy authStrategy,
236 final AuthExchange authExchange,
237 final HttpContext context) throws AuthenticationException, MalformedChallengeException {
238
239 final HttpClientContext clientContext = HttpClientContext.cast(context);
240 final String exchangeId = clientContext.getExchangeId();
241 final boolean challenged = checkChallenged(challengeType, response, clientContext);
242 final boolean isChallengeExpected = isChallengeExpected(authExchange);
243
244 if (LOG.isDebugEnabled()) {
245 LOG.debug("{} {} requested authentication", exchangeId, host.toHostString());
246 }
247
248 final Map<String, AuthChallenge> challengeMap = extractChallengeMap(challengeType, response, clientContext);
249
250 if (challengeMap.isEmpty()) {
251 if (LOG.isDebugEnabled()) {
252 LOG.debug("{} Response contains no valid authentication challenges", exchangeId);
253 }
254 if (!isChallengeExpected) {
255 authExchange.reset();
256 return false;
257 }
258 }
259
260 switch (authExchange.getState()) {
261 case FAILURE:
262 return false;
263 case SUCCESS:
264 if (!isChallengeExpected) {
265 authExchange.reset();
266 break;
267 }
268
269 case CHALLENGED:
270
271 case HANDSHAKE:
272 Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme");
273
274 case UNCHALLENGED:
275 final AuthScheme authScheme = authExchange.getAuthScheme();
276
277
278 if (authScheme != null) {
279 final String schemeName = authScheme.getName();
280 final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
281 if (challenge != null || isChallengeExpected) {
282 if (LOG.isDebugEnabled()) {
283 LOG.debug("{} Processing authentication challenge {}", exchangeId, challenge);
284 }
285 try {
286 authScheme.processChallenge(host, challenged, challenge, clientContext);
287 } catch (final AuthenticationException | MalformedChallengeException ex) {
288 if (LOG.isWarnEnabled()) {
289 LOG.warn("Exception processing challenge {}", exchangeId, ex);
290 }
291 authExchange.reset();
292 authExchange.setState(AuthExchange.State.FAILURE);
293 if (isChallengeExpected) {
294 throw ex;
295 }
296 }
297 if (authScheme.isChallengeComplete()) {
298 if (LOG.isDebugEnabled()) {
299 LOG.debug("{} Authentication failed", exchangeId);
300 }
301 authExchange.reset();
302 authExchange.setState(AuthExchange.State.FAILURE);
303 return false;
304 }
305 if (!challenged) {
306
307
308 authExchange.setState(AuthExchange.State.SUCCESS);
309 return false;
310 } else {
311 authExchange.setState(AuthExchange.State.HANDSHAKE);
312 }
313 return true;
314 }
315 authExchange.reset();
316
317 }
318 }
319
320
321
322
323 final List<AuthScheme> preferredSchemes = authStrategy.select(challengeType, challengeMap, clientContext);
324 final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
325 if (credsProvider == null) {
326 if (LOG.isDebugEnabled()) {
327 LOG.debug("{} Credentials provider not set in the context", exchangeId);
328 }
329 return false;
330 }
331
332 final Queue<AuthScheme> authOptions = new LinkedList<>();
333 if (LOG.isDebugEnabled()) {
334 LOG.debug("{} Selecting authentication options", exchangeId);
335 }
336 for (final AuthScheme authScheme: preferredSchemes) {
337
338
339
340 try {
341 final String schemeName = authScheme.getName();
342 final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
343 authScheme.processChallenge(host, challenged, challenge, clientContext);
344 if (authScheme.isResponseReady(host, credsProvider, clientContext)) {
345 authOptions.add(authScheme);
346 }
347 } catch (final AuthenticationException | MalformedChallengeException ex) {
348 if (LOG.isWarnEnabled()) {
349 LOG.warn("Exception while processing Challange", ex);
350 }
351 }
352 }
353 if (!authOptions.isEmpty()) {
354 if (LOG.isDebugEnabled()) {
355 LOG.debug("{} Selected authentication options: {}", exchangeId, authOptions);
356 }
357 authExchange.reset();
358 authExchange.setState(AuthExchange.State.CHALLENGED);
359 authExchange.setOptions(authOptions);
360 return true;
361 }
362 return false;
363 }
364
365
366
367
368
369
370
371
372
373
374
375 public void addAuthResponse(
376 final HttpHost host,
377 final ChallengeType challengeType,
378 final HttpRequest request,
379 final AuthExchange authExchange,
380 final HttpContext context) {
381 final HttpClientContext clientContext = HttpClientContext.cast(context);
382 final String exchangeId = clientContext.getExchangeId();
383 AuthScheme authScheme = authExchange.getAuthScheme();
384 switch (authExchange.getState()) {
385 case FAILURE:
386 return;
387 case SUCCESS:
388 Asserts.notNull(authScheme, "AuthScheme");
389 if (authScheme.isConnectionBased()) {
390 return;
391 }
392 break;
393 case HANDSHAKE:
394 Asserts.notNull(authScheme, "AuthScheme");
395 break;
396 case CHALLENGED:
397 final Queue<AuthScheme> authOptions = authExchange.getAuthOptions();
398 if (authOptions != null) {
399 while (!authOptions.isEmpty()) {
400 authScheme = authOptions.remove();
401 authExchange.select(authScheme);
402 if (LOG.isDebugEnabled()) {
403 LOG.debug("{} Generating response to an authentication challenge using {} scheme",
404 exchangeId, authScheme.getName());
405 }
406 try {
407 final String authResponse = authScheme.generateAuthResponse(host, request, clientContext);
408 if (authResponse != null) {
409 final Header header = new BasicHeader(
410 challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
411 authResponse);
412 request.addHeader(header);
413 }
414 break;
415 } catch (final AuthenticationException ex) {
416 if (LOG.isWarnEnabled()) {
417 LOG.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
418 }
419 }
420 }
421 return;
422 }
423 Asserts.notNull(authScheme, "AuthScheme");
424 default:
425 }
426
427
428
429 if (authScheme != null) {
430 try {
431 final String authResponse = authScheme.generateAuthResponse(host, request, clientContext);
432 final Header header = new BasicHeader(
433 challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
434 authResponse);
435 request.addHeader(header);
436 } catch (final AuthenticationException ex) {
437 if (LOG.isErrorEnabled()) {
438 LOG.error("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
439 }
440 }
441 }
442 }
443
444 }