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.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
69
70
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
93
94
95
96
97
98
99
100
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 final String exchangeId = clientContext.getExchangeId();
122
123 if (response.getCode() == challengeCode) {
124 if (log.isDebugEnabled()) {
125 log.debug("{} Authentication required", exchangeId);
126 }
127 if (authExchange.getState() == AuthExchange.State.SUCCESS) {
128 clearCache(host, clientContext);
129 }
130 return true;
131 }
132 switch (authExchange.getState()) {
133 case CHALLENGED:
134 case HANDSHAKE:
135 if (log.isDebugEnabled()) {
136 log.debug("{} Authentication succeeded", exchangeId);
137 }
138 authExchange.setState(AuthExchange.State.SUCCESS);
139 updateCache(host, authExchange.getAuthScheme(), clientContext);
140 break;
141 case SUCCESS:
142 break;
143 default:
144 authExchange.setState(AuthExchange.State.UNCHALLENGED);
145 }
146 return false;
147 }
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162 public boolean updateAuthState(
163 final HttpHost host,
164 final ChallengeType challengeType,
165 final HttpResponse response,
166 final AuthenticationStrategy authStrategy,
167 final AuthExchange authExchange,
168 final HttpContext context) {
169
170 final HttpClientContext clientContext = HttpClientContext.adapt(context);
171 final String exchangeId = clientContext.getExchangeId();
172
173 if (log.isDebugEnabled()) {
174 log.debug("{} {} requested authentication", exchangeId, host.toHostString());
175 }
176
177 final Header[] headers = response.getHeaders(
178 challengeType == ChallengeType.PROXY ? HttpHeaders.PROXY_AUTHENTICATE : HttpHeaders.WWW_AUTHENTICATE);
179 final Map<String, AuthChallenge> challengeMap = new HashMap<>();
180 for (final Header header: headers) {
181 final CharArrayBuffer buffer;
182 final int pos;
183 if (header instanceof FormattedHeader) {
184 buffer = ((FormattedHeader) header).getBuffer();
185 pos = ((FormattedHeader) header).getValuePos();
186 } else {
187 final String s = header.getValue();
188 if (s == null) {
189 continue;
190 }
191 buffer = new CharArrayBuffer(s.length());
192 buffer.append(s);
193 pos = 0;
194 }
195 final ParserCursor cursor = new ParserCursor(pos, buffer.length());
196 final List<AuthChallenge> authChallenges;
197 try {
198 authChallenges = parser.parse(challengeType, buffer, cursor);
199 } catch (final ParseException ex) {
200 if (log.isWarnEnabled()) {
201 log.warn("{} Malformed challenge: {}", exchangeId, header.getValue());
202 }
203 continue;
204 }
205 for (final AuthChallenge authChallenge: authChallenges) {
206 final String schemeName = authChallenge.getSchemeName().toLowerCase(Locale.ROOT);
207 if (!challengeMap.containsKey(schemeName)) {
208 challengeMap.put(schemeName, authChallenge);
209 }
210 }
211 }
212 if (challengeMap.isEmpty()) {
213 if (log.isDebugEnabled()) {
214 log.debug("{} Response contains no valid authentication challenges", exchangeId);
215 }
216 clearCache(host, clientContext);
217 authExchange.reset();
218 return false;
219 }
220
221 switch (authExchange.getState()) {
222 case FAILURE:
223 return false;
224 case SUCCESS:
225 authExchange.reset();
226 break;
227 case CHALLENGED:
228 case HANDSHAKE:
229 Asserts.notNull(authExchange.getAuthScheme(), "AuthScheme");
230 case UNCHALLENGED:
231 final AuthScheme authScheme = authExchange.getAuthScheme();
232 if (authScheme != null) {
233 final String schemeName = authScheme.getName();
234 final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
235 if (challenge != null) {
236 if (log.isDebugEnabled()) {
237 log.debug("{} Authorization challenge processed", exchangeId);
238 }
239 try {
240 authScheme.processChallenge(challenge, context);
241 } catch (final MalformedChallengeException ex) {
242 if (log.isWarnEnabled()) {
243 log.warn("{} {}", exchangeId, ex.getMessage());
244 }
245 clearCache(host, clientContext);
246 authExchange.reset();
247 return false;
248 }
249 if (authScheme.isChallengeComplete()) {
250 if (log.isDebugEnabled()) {
251 log.debug("{} Authentication failed", exchangeId);
252 }
253 clearCache(host, clientContext);
254 authExchange.reset();
255 authExchange.setState(AuthExchange.State.FAILURE);
256 return false;
257 }
258 authExchange.setState(AuthExchange.State.HANDSHAKE);
259 return true;
260 }
261 authExchange.reset();
262
263 }
264 }
265
266 final List<AuthScheme> preferredSchemes = authStrategy.select(challengeType, challengeMap, context);
267 final CredentialsProvider credsProvider = clientContext.getCredentialsProvider();
268 if (credsProvider == null) {
269 if (log.isDebugEnabled()) {
270 log.debug("{} Credentials provider not set in the context", exchangeId);
271 }
272 return false;
273 }
274
275 final Queue<AuthScheme> authOptions = new LinkedList<>();
276 if (log.isDebugEnabled()) {
277 log.debug("{} Selecting authentication options", exchangeId);
278 }
279 for (final AuthScheme authScheme: preferredSchemes) {
280 try {
281 final String schemeName = authScheme.getName();
282 final AuthChallenge challenge = challengeMap.get(schemeName.toLowerCase(Locale.ROOT));
283 authScheme.processChallenge(challenge, context);
284 if (authScheme.isResponseReady(host, credsProvider, context)) {
285 authOptions.add(authScheme);
286 }
287 } catch (final AuthenticationException | MalformedChallengeException ex) {
288 if (log.isWarnEnabled()) {
289 log.warn(ex.getMessage());
290 }
291 }
292 }
293 if (!authOptions.isEmpty()) {
294 if (log.isDebugEnabled()) {
295 log.debug("{} Selected authentication options: {}", exchangeId, authOptions);
296 }
297 authExchange.reset();
298 authExchange.setState(AuthExchange.State.CHALLENGED);
299 authExchange.setOptions(authOptions);
300 return true;
301 }
302 return false;
303 }
304
305
306
307
308
309
310
311
312
313
314
315 public void addAuthResponse(
316 final HttpHost host,
317 final ChallengeType challengeType,
318 final HttpRequest request,
319 final AuthExchange authExchange,
320 final HttpContext context) {
321 final HttpClientContext clientContext = HttpClientContext.adapt(context);
322 final String exchangeId = clientContext.getExchangeId();
323 AuthScheme authScheme = authExchange.getAuthScheme();
324 switch (authExchange.getState()) {
325 case FAILURE:
326 return;
327 case SUCCESS:
328 Asserts.notNull(authScheme, "AuthScheme");
329 if (authScheme.isConnectionBased()) {
330 return;
331 }
332 break;
333 case HANDSHAKE:
334 Asserts.notNull(authScheme, "AuthScheme");
335 break;
336 case CHALLENGED:
337 final Queue<AuthScheme> authOptions = authExchange.getAuthOptions();
338 if (authOptions != null) {
339 while (!authOptions.isEmpty()) {
340 authScheme = authOptions.remove();
341 authExchange.select(authScheme);
342 if (log.isDebugEnabled()) {
343 log.debug("{} Generating response to an authentication challenge using {} scheme",
344 exchangeId, authScheme.getName());
345 }
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 break;
353 } catch (final AuthenticationException ex) {
354 if (log.isWarnEnabled()) {
355 log.warn("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
356 }
357 }
358 }
359 return;
360 }
361 Asserts.notNull(authScheme, "AuthScheme");
362 default:
363 }
364 if (authScheme != null) {
365 try {
366 final String authResponse = authScheme.generateAuthResponse(host, request, context);
367 final Header header = new BasicHeader(
368 challengeType == ChallengeType.TARGET ? HttpHeaders.AUTHORIZATION : HttpHeaders.PROXY_AUTHORIZATION,
369 authResponse);
370 request.addHeader(header);
371 } catch (final AuthenticationException ex) {
372 if (log.isErrorEnabled()) {
373 log.error("{} {} authentication error: {}", exchangeId, authScheme, ex.getMessage());
374 }
375 }
376 }
377 }
378
379 private void updateCache(final HttpHost host, final AuthScheme authScheme, final HttpClientContext clientContext) {
380 final boolean cacheable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null;
381 if (cacheable) {
382 AuthCache authCache = clientContext.getAuthCache();
383 if (authCache == null) {
384 authCache = new BasicAuthCache();
385 clientContext.setAuthCache(authCache);
386 }
387 if (log.isDebugEnabled()) {
388 final String exchangeId = clientContext.getExchangeId();
389 log.debug("{} Caching '{}' auth scheme for {}", exchangeId, authScheme.getName(), host);
390 }
391 authCache.put(host, authScheme);
392 }
393 }
394
395 private void clearCache(final HttpHost host, final HttpClientContext clientContext) {
396
397 final AuthCache authCache = clientContext.getAuthCache();
398 if (authCache != null) {
399 if (log.isDebugEnabled()) {
400 final String exchangeId = clientContext.getExchangeId();
401 log.debug("{} Clearing cached auth scheme for {}", exchangeId, host);
402 }
403 authCache.remove(host);
404 }
405 }
406
407 }