Skip to content

Commit 1e1b79f

Browse files
committed
apply discussed fixes
set isConnectionBased() to false fix logic in needsAuthentication()
1 parent 3eda19f commit 1e1b79f

File tree

8 files changed

+85
-91
lines changed

8 files changed

+85
-91
lines changed

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestSpnegoScheme.java

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ public AuthScheme create(final HttpContext context) {
291291
* Tests that the client will stop connecting to the server if
292292
* the server still keep asking for a valid ticket.
293293
*/
294+
//@Disabled
294295
@Test
295296
void testDontTryToAuthenticateEndlessly() throws Exception {
296297
configureServer(t -> {
@@ -310,42 +311,16 @@ void testDontTryToAuthenticateEndlessly() throws Exception {
310311
final HttpHost target = startServer();
311312
final String s = "/path";
312313
final HttpGet httpget = new HttpGet(s);
313-
client().execute(target, httpget, response -> {
314-
EntityUtils.consume(response.getEntity());
315-
Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
316-
return null;
317-
});
318-
}
319-
320-
/**
321-
* Javadoc specifies that {@link GSSContext#initSecContext(byte[], int, int)} can return null
322-
* if no token is generated. Client should be able to deal with this response.
323-
*/
324-
@Test
325-
void testNoTokenGeneratedError() throws Exception {
326-
configureServer(t -> {
327-
t.register("*", new PleaseNegotiateService());
328-
});
329-
330-
final AuthSchemeFactory nsf = new TestAuthSchemeFactory(new NegotiateSchemeWithMockGssManager());
331-
final Registry<AuthSchemeFactory> authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
332-
.register(StandardAuthScheme.SPNEGO, nsf)
333-
.build();
334-
configureClient(t -> {
335-
t.setTargetAuthenticationStrategy(spnegoAuthenticationStrategy);
336-
t.setDefaultAuthSchemeRegistry(authSchemeRegistry);
337-
t.setDefaultCredentialsProvider(jaasCredentialsProvider);
338-
});
339-
340-
final HttpHost target = startServer();
341-
final String s = "/path";
342-
final HttpGet httpget = new HttpGet(s);
343-
client().execute(target, httpget, response -> {
344-
EntityUtils.consume(response.getEntity());
345-
Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
346-
return null;
347-
});
348-
314+
try {
315+
client().execute(target, httpget, response -> {
316+
EntityUtils.consume(response.getEntity());
317+
Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
318+
return null;
319+
});
320+
Assertions.fail();
321+
} catch (final IllegalStateException e) {
322+
// Expected
323+
}
349324
}
350325

351326
/**

httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssConfig.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
/**
3434
* Immutable class encapsulating GSS configuration options for the new mutual auth capable
35-
* SpnegoScheme.
35+
* for the new {@link SpnegoScheme}.
3636
*
3737
* Unlike the deprecated {@link KerberosConfig}, this class uses explicit defaults, and
3838
* primitive booleans.
@@ -49,14 +49,14 @@ public class GssConfig implements Cloneable {
4949

5050
public static final GssConfig DEFAULT = new Builder().build();
5151
public static final GssConfig LEGACY =
52-
new Builder().setIgnoreMissingToken(true).setRequireMutualAuth(false).build();
52+
new Builder().setIgnoreIncompleteSecurityContext(true).setRequireMutualAuth(false).build();
5353

5454
private final boolean addPort;
5555
private final boolean useCanonicalHostname;
5656
private final boolean requestMutualAuth;
5757
private final boolean requireMutualAuth;
5858
private final boolean requestDelegCreds;
59-
private final boolean ignoreMissingToken;
59+
private final boolean ignoreIncompleteSecurityContext;
6060

6161
/**
6262
* Intended for CDI compatibility
@@ -71,14 +71,14 @@ protected GssConfig() {
7171
final boolean requestMutualAuth,
7272
final boolean requireMutualAuth,
7373
final boolean requestDelegCreds,
74-
final boolean ignoreMissingToken) {
74+
final boolean ignoreIncompleteSecurityContext) {
7575
super();
7676
this.addPort = addPort;
7777
this.useCanonicalHostname = useCanonicalHostname;
7878
this.requestMutualAuth = requestMutualAuth;
7979
this.requireMutualAuth = requireMutualAuth;
8080
this.requestDelegCreds = requestDelegCreds;
81-
this.ignoreMissingToken = ignoreMissingToken;
81+
this.ignoreIncompleteSecurityContext = ignoreIncompleteSecurityContext;
8282
}
8383

8484
public boolean isAddPort() {
@@ -101,8 +101,8 @@ public boolean isRequireMutualAuth() {
101101
return requireMutualAuth;
102102
}
103103

104-
public boolean isIgnoreMissingToken() {
105-
return ignoreMissingToken;
104+
public boolean isIgnoreIncompleteSecurityContext() {
105+
return ignoreIncompleteSecurityContext;
106106
}
107107

108108
@Override
@@ -119,7 +119,7 @@ public String toString() {
119119
builder.append(", requestDelegCreds=").append(requestDelegCreds);
120120
builder.append(", requestMutualAuth=").append(requestMutualAuth);
121121
builder.append(", requireMutualAuth=").append(requireMutualAuth);
122-
builder.append(", ignoreMissingToken=").append(ignoreMissingToken);
122+
builder.append(", ignoreIncompleteSecurityContext=").append(ignoreIncompleteSecurityContext);
123123
builder.append("]");
124124
return builder.toString();
125125
}
@@ -135,7 +135,7 @@ public static GssConfig.Builder copy(final GssConfig config) {
135135
.setRequestDelegCreds(config.isRequestDelegCreds())
136136
.setRequireMutualAuth(config.isRequireMutualAuth())
137137
.setRequestMutualAuth(config.isRequestMutualAuth())
138-
.setIgnoreMissingToken(config.isIgnoreMissingToken());
138+
.setIgnoreIncompleteSecurityContext(config.isIgnoreIncompleteSecurityContext());
139139
}
140140

141141
public static class Builder {
@@ -145,7 +145,7 @@ public static class Builder {
145145
private boolean requestMutualAuth = true;
146146
private boolean requireMutualAuth = true;
147147
private boolean requestDelegCreds = false;
148-
private boolean ignoreMissingToken = false;
148+
private boolean ignoreIncompleteSecurityContext = false;
149149

150150

151151
Builder() {
@@ -177,14 +177,14 @@ public Builder setRequestDelegCreds(final boolean requuestDelegCreds) {
177177
return this;
178178
}
179179

180-
public Builder setIgnoreMissingToken(final boolean ignoreMissingToken) {
181-
this.ignoreMissingToken = ignoreMissingToken;
180+
public Builder setIgnoreIncompleteSecurityContext(final boolean ignoreIncompleteSecurityContext) {
181+
this.ignoreIncompleteSecurityContext = ignoreIncompleteSecurityContext;
182182
return this;
183183
}
184184

185185
public GssConfig build() {
186-
if (requireMutualAuth && ignoreMissingToken) {
187-
throw new IllegalArgumentException("If requireMutualAuth is set then ignoreMissingToken must not be set");
186+
if (requireMutualAuth && ignoreIncompleteSecurityContext) {
187+
throw new IllegalArgumentException("If requireMutualAuth is set then ignoreIncompleteSecurityContext must not be set");
188188
}
189189
if (requireMutualAuth && !requestMutualAuth) {
190190
throw new IllegalArgumentException("If requireMutualAuth is set then requestMutualAuth must also be set");
@@ -195,7 +195,7 @@ public GssConfig build() {
195195
requestMutualAuth,
196196
requireMutualAuth,
197197
requestDelegCreds,
198-
ignoreMissingToken
198+
ignoreIncompleteSecurityContext
199199
);
200200
}
201201

httpclient5/src/main/java/org/apache/hc/client5/http/auth/gss/GssCredentials.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public GSSCredential getGSSCredential() {
6363

6464
@Override
6565
public Principal getUserPrincipal() {
66+
// TODO Can we extract this somehow ?
6667
return null;
6768
}
6869

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,26 +334,25 @@ private boolean needAuthentication(
334334
}
335335
}
336336

337+
boolean targetNeedsAuth = false;
338+
boolean proxyNeedsAuth = false;
337339
if (targetAuthRequested || targetMutualAuthRequired) {
338-
final boolean updated = authenticator.handleResponse(target, ChallengeType.TARGET, response,
340+
targetNeedsAuth = authenticator.handleResponse(target, ChallengeType.TARGET, response,
339341
targetAuthStrategy, targetAuthExchange, context);
340342

341343
if (authCacheKeeper != null) {
342344
authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context);
343345
}
344-
345-
return updated;
346346
}
347347
if (proxyAuthRequested || proxyMutualAuthRequired) {
348-
final boolean updated = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
348+
proxyNeedsAuth = authenticator.handleResponse(proxy, ChallengeType.PROXY, response,
349349
proxyAuthStrategy, proxyAuthExchange, context);
350350

351351
if (authCacheKeeper != null) {
352352
authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context);
353353
}
354-
355-
return updated;
356354
}
355+
return targetNeedsAuth || proxyNeedsAuth;
357356
}
358357
return false;
359358
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthenticationHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ public boolean handleResponse(
363363

364364
/**
365365
* Generates a response to the authentication challenge based on the actual {@link AuthExchange} state
366-
* and adds it to the given {@link HttpRequest} message .
366+
* and adds it to the given {@link HttpRequest} message.
367367
*
368368
* @param host the hostname of the opposite endpoint.
369369
* @param challengeType the challenge type (target or proxy).

httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/gss/GssSchemeBase.java

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,15 +77,16 @@ enum State {
7777
}
7878

7979
private static final Logger LOG = LoggerFactory.getLogger(GssSchemeBase.class);
80-
private static final String KERBEROS_SCHEME = "HTTP";
80+
private static final String PEER_SERVICE_NAME = "HTTP";
8181

8282
// The GSS spec does not specify how long the conversation can be. This should be plenty.
8383
// Realistically, we get one initial token, then one maybe one more for mutual authentication.
84+
// TODO In the future this might need to be configurable with the upcoming IAKerb support
8485
private static final int MAX_GSS_CHALLENGES = 3;
8586
private final GssConfig config;
8687
private final DnsResolver dnsResolver;
8788
private final boolean requireMutualAuth;
88-
private final boolean ignoreMissingToken;
89+
private final boolean ignoreIncompleteSecurityContext;
8990
private int challengesLeft = MAX_GSS_CHALLENGES;
9091

9192
/** Authentication process state */
@@ -100,10 +101,24 @@ enum State {
100101
this.config = config != null ? config : GssConfig.DEFAULT;
101102
this.dnsResolver = dnsResolver != null ? dnsResolver : SystemDefaultDnsResolver.INSTANCE;
102103
this.requireMutualAuth = config.isRequireMutualAuth();
103-
this.ignoreMissingToken = config.isIgnoreMissingToken();
104+
this.ignoreIncompleteSecurityContext = config.isIgnoreIncompleteSecurityContext();
104105
this.state = State.UNINITIATED;
105106
}
106107

108+
private void dispose() {
109+
// remove sensitive information from memory
110+
// cleaning up the credential is the caller's job
111+
try {
112+
if (gssContext != null) {
113+
gssContext.dispose();
114+
}
115+
} catch (final Exception e) {
116+
if (LOG.isWarnEnabled()) {
117+
LOG.warn("Exception caught while calling gssContext.dispose()", e);
118+
}
119+
}
120+
}
121+
107122
GssSchemeBase(final GssConfig config) {
108123
this(config, SystemDefaultDnsResolver.INSTANCE);
109124
}
@@ -136,24 +151,28 @@ public void processChallenge(
136151
) throws AuthenticationException {
137152

138153
if (challengesLeft-- <= 0 ) {
139-
if (LOG.isDebugEnabled()) {
154+
if (LOG.isWarnEnabled()) {
140155
final HttpClientContext clientContext = HttpClientContext.cast(context);
141156
final String exchangeId = clientContext.getExchangeId();
142-
LOG.debug("{} GSS error: too many challenges received. Infinite loop ?", exchangeId);
157+
LOG.warn("{} GSS error: too many challenges received. Infinite loop ?", exchangeId);
143158
}
144-
// TODO: Should we throw an exception ? There is a test for this behaviour.
145159
state = State.FAILED;
146160
return;
147161
}
148162

149-
final byte[] challengeToken = Base64.decodeBase64(authChallenge == null ? null : authChallenge.getValue());
163+
final byte[] challengeToken = (authChallenge == null) ? null : Base64.decodeBase64(authChallenge.getValue());
150164

151165
final String gssHostname;
152166
String hostname = host.getHostName();
153167
if (config.isUseCanonicalHostname()) {
154168
try {
155169
hostname = dnsResolver.resolveCanonicalHostname(host.getHostName());
156170
} catch (final UnknownHostException ignore) {
171+
if (LOG.isWarnEnabled()) {
172+
final HttpClientContext clientContext = HttpClientContext.cast(context);
173+
final String exchangeId = clientContext.getExchangeId();
174+
LOG.warn("{} Could not canonicalize hostname {}, using as is.", exchangeId, host.getHostName());
175+
}
157176
}
158177
}
159178
if (config.isAddPort()) {
@@ -171,42 +190,42 @@ public void processChallenge(
171190
switch (state) {
172191
case UNINITIATED:
173192
setGssCredential(HttpClientContext.cast(context).getCredentialsProvider(), host, context);
174-
queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname);
175-
if (challengeToken != null) {
193+
if (challengeToken == null) {
194+
queuedToken = generateToken(challengeToken, PEER_SERVICE_NAME, gssHostname);
195+
state = State.TOKEN_READY;
196+
} else {
176197
if (LOG.isDebugEnabled()) {
177198
final HttpClientContext clientContext = HttpClientContext.cast(context);
178199
final String exchangeId = clientContext.getExchangeId();
179200
LOG.debug("{} Internal GSS error: token received when none was sent yet: {}", exchangeId, challengeToken);
180201
}
181-
// TODO Should we fail ? That would break existing tests that send a token
182-
// in the first response, which is against the RFC.
202+
state = State.FAILED;
183203
}
184-
state = State.TOKEN_READY;
185204
break;
186205
case TOKEN_SENT:
187206
if (challengeToken == null) {
188-
if (!challenged && ignoreMissingToken) {
207+
if (!challenged && ignoreIncompleteSecurityContext) {
189208
// Got a Non 401/407 code without a challenge. Old non RFC compliant server.
190-
if (LOG.isDebugEnabled()) {
209+
if (LOG.isWarnEnabled()) {
191210
final HttpClientContext clientContext = HttpClientContext.cast(context);
192211
final String exchangeId = clientContext.getExchangeId();
193-
LOG.debug("{} GSS Context is not established, but continuing because GssConfig.ignoreMissingToken is true.", exchangeId);
212+
LOG.warn("{} GSS Context is not established, but continuing because GssConfig.ignoreIncompleteSecurityContext is true.", exchangeId);
194213
}
195214
state = State.SUCCEEDED;
196215
break;
197216
} else {
198217
if (LOG.isDebugEnabled()) {
199218
final HttpClientContext clientContext = HttpClientContext.cast(context);
200219
final String exchangeId = clientContext.getExchangeId();
201-
LOG.debug("{} Did not receive required challenge and GssConfig.ignoreMissingToken is false.",
220+
LOG.debug("{} Did not receive required challenge.",
202221
exchangeId);
203222
}
204223
state = State.FAILED;
205224
throw new AuthenticationException(
206-
"Did not receive required challenge and GssConfig.ignoreMissingToken is false.");
225+
"Did not receive required challenge.");
207226
}
208227
}
209-
queuedToken = generateToken(challengeToken, KERBEROS_SCHEME, gssHostname);
228+
queuedToken = generateToken(challengeToken, PEER_SERVICE_NAME, gssHostname);
210229
if (challenged) {
211230
state = State.TOKEN_READY;
212231
} else if (!gssContext.isEstablished()) {
@@ -237,16 +256,16 @@ public void processChallenge(
237256
LOG.debug("{} GSSContext MutualAuthState is false, but continuing because GssConfig.requireMutualAuth is false.",
238257
exchangeId);
239258
}
240-
state = State.FAILED;
259+
state = State.SUCCEEDED;
241260
}
242261
} else {
243262
state = State.SUCCEEDED;
244263
}
245264
break;
246265
default:
266+
final State prevState = state;
247267
state = State.FAILED;
248-
throw new IllegalStateException("Illegal state: " + state);
249-
268+
throw new IllegalStateException("Illegal state: " + prevState);
250269
}
251270
} catch (final GSSException gsse) {
252271
state = State.FAILED;
@@ -264,6 +283,10 @@ public void processChallenge(
264283
}
265284
// other error
266285
throw new AuthenticationException(gsse.getMessage(), gsse);
286+
} finally {
287+
if ((state == State.FAILED || state == State.SUCCEEDED) && gssContext != null) {
288+
dispose();
289+
}
267290
}
268291
}
269292

@@ -336,9 +359,6 @@ public boolean isResponseReady(
336359
protected void setGssCredential(final CredentialsProvider credentialsProvider,
337360
final HttpHost host,
338361
final HttpContext context) {
339-
if (this.gssCredential != null) {
340-
return;
341-
}
342362
final Credentials credentials =
343363
credentialsProvider.getCredentials(new AuthScope(host, null, getName()), context);
344364
if (credentials instanceof org.apache.hc.client5.http.auth.gss.GssCredentials) {

0 commit comments

Comments
 (0)