Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to this project are documented in this file.

## 3.2.00

### Fixed

- **A client no longer stays stuck on a failing authentication scheme** (#212). With an ordered
fallback list such as `(KERBEROS, NTLM)` or `(NTLM, BASIC)`, a client that had moved past a
scheme never went back to it: once the last scheme failed too (Basic disabled on the host, NTLM
blocked by policy), every later request failed until the client was recreated, even after a
transient failure of the first scheme (an unreachable KDC, a host restarting) was over. Once
every scheme has failed, the next request now starts over from the first one.

## 3.1.00

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
/**
* Tries several {@link AuthScheme}s in the caller's order, using the first whose handshake succeeds
* (e.g. {@code [KERBEROS, NTLM]}: attempt Kerberos, fall back to NTLM). Once a scheme authenticates
* it becomes the active one for the rest of the connection; on reconnect the fallback runs again.
* it becomes the active one for the rest of the connection; on reconnect the fallback runs again
* from that scheme. Once every scheme has failed, the next request starts over from the first one.
* <p>
* Fallback triggers on a failed handshake — the common case being Kerberos unavailable (no TGT,
* no reachable KDC, unconfigured realm), which fails client-side before any SOAP is sent.
Expand Down Expand Up @@ -68,6 +69,9 @@ public String authenticate(final HttpTransport transport) throws Exception {
}
}
}
// Every remaining candidate failed: rewind so the next request starts over from the first
// scheme instead of staying on the last one for good (issue #212).
startIndex = 0;
throw new IllegalStateException(
"All requested authentication schemes failed"
+ (lastFailure == null ? "" : " (last: " + lastFailure.getMessage() + ")"),
Expand Down Expand Up @@ -108,6 +112,10 @@ public boolean advance() {
startIndex++;
return true;
}
// The last candidate was rejected too: rewind so the next request starts over from the first
// scheme instead of staying on the last one for good (issue #212).
active = null;
startIndex = 0;
return false;
}

Expand Down
4 changes: 3 additions & 1 deletion src/site/markdown/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ When `authentication(...)` is not called, **NTLM** is used.

Several schemes form an **ordered fallback list**: each is tried in the given order until one
succeeds. `authentication(KERBEROS, NTLM)` attempts Kerberos first and falls back to NTLM — for
example when the KDC is unreachable or the clock skew is too large.
example when the KDC is unreachable or the clock skew is too large. The scheme that succeeded is
retried first when the connection is re-established; once every scheme has failed, the next request
starts over from the first one.

## User name and domain

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,44 @@ void allSchemesFailingThrows() {
assertThrows(IllegalStateException.class, () -> fallback.authenticate(dummyTransport()));
}

@Test
void rejectionOfEverySchemeRewindsToTheFirstOne() throws Exception {
// Issue #212: [NTLM, Basic] where the server rejects both (e.g. a transient logon failure,
// then Basic disabled on the host). The next request must start over from NTLM, not stay on
// Basic for the rest of the client's life.
final FakeScheme ntlm = new FakeScheme("ntlm", NEVER);
final FakeScheme basic = new FakeScheme("basic", NEVER, "Basic dXNlcjpwYXNz");
final FallbackAuthScheme fallback = new FallbackAuthScheme(List.of(ntlm, basic));
final HttpTransport transport = dummyTransport();

assertEquals("Negotiate ntlm", fallback.authenticate(transport));
assertTrue(fallback.advance()); // 401 on ntlm -> basic
assertEquals("Negotiate basic", fallback.authenticate(transport));
fallback.reset();
assertFalse(fallback.advance()); // 401 on basic too -> nothing left, rewound
assertFalse(fallback.isAuthenticated());

assertEquals("Negotiate ntlm", fallback.authenticate(transport)); // starts over from ntlm
assertEquals(2, ntlm.authenticateCalls);
assertEquals(1, basic.authenticateCalls);
}

@Test
void clientSideFailureOfEverySchemeRewindsToTheFirstOne() throws Exception {
// Issue #212, the other path: the last scheme fails client-side inside authenticate(), so
// advance() is never called. The next handshake must still start over from the first scheme.
final FakeScheme kerberos = new FakeScheme("kerberos", NEVER);
final FakeScheme ntlm = new FakeScheme("ntlm", ALWAYS);
final FallbackAuthScheme fallback = new FallbackAuthScheme(List.of(kerberos, ntlm));
final HttpTransport transport = dummyTransport();

assertEquals("Negotiate kerberos", fallback.authenticate(transport));
assertTrue(fallback.advance()); // 401 on kerberos -> ntlm
assertThrows(IllegalStateException.class, () -> fallback.authenticate(transport)); // ntlm fails
assertEquals("Negotiate kerberos", fallback.authenticate(transport)); // rewound to kerberos
assertEquals(2, kerberos.authenticateCalls);
}

@Test
void requestAuthorizationForwardsToTheActiveStatelessScheme() throws Exception {
// A stateless candidate (e.g. Basic) repeats its Authorization header on EVERY request, so
Expand Down
Loading