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
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
146
147
148
149
150
151
152
153
154
155
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
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
291
292
293
294
295
296
297
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 }