Skip to content

Commit 090e1c2

Browse files
committed
Reuse SPNEGO token until expiry and reset on expiration
Signed-off-by: raccoonback <[email protected]>
1 parent cfa42a3 commit 090e1c2

File tree

4 files changed

+70
-11
lines changed

4 files changed

+70
-11
lines changed

reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/client/spnego/Application.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public static void main(String[] args) {
2929

3030
SpnegoAuthenticator authenticator = new JaasAuthenticator("KerberosLogin"); // <4>
3131
HttpClient client = HttpClient.create()
32-
.spnego(SpnegoAuthProvider.create(authenticator)); // <5>
32+
.spnego(SpnegoAuthProvider.create(authenticator, 401)); // <5>
3333

3434
client.get()
3535
.uri("http://protected.example.com/")

reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,15 @@ public Context currentContext() {
446446
@Override
447447
public void onStateChange(Connection connection, State newState) {
448448
if (newState == HttpClientState.RESPONSE_RECEIVED) {
449+
HttpClientOperations ops = connection.as(HttpClientOperations.class);
450+
if (ops != null && handler.spnegoAuthProvider != null) {
451+
int statusCode = ops.status().code();
452+
HttpHeaders headers = ops.responseHeaders();
453+
if (handler.spnegoAuthProvider.isUnauthorized(statusCode, headers)) {
454+
handler.spnegoAuthProvider.invalidateCache();
455+
}
456+
}
457+
449458
sink.success(connection);
450459
return;
451460
}

reactor-netty-http/src/main/java/reactor/netty/http/client/SpnegoAuthProvider.java

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static reactor.core.scheduler.Schedulers.boundedElastic;
1919

2020
import io.netty.handler.codec.http.HttpHeaderNames;
21+
import io.netty.handler.codec.http.HttpHeaders;
2122
import java.net.InetSocketAddress;
2223
import java.security.PrivilegedAction;
2324
import java.util.Base64;
@@ -49,28 +50,35 @@
4950
*/
5051
public final class SpnegoAuthProvider {
5152

53+
private static final String SPNEGO_HEADER = "Negotiate";
54+
5255
private final SpnegoAuthenticator authenticator;
5356
private final GSSManager gssManager;
57+
private final int unauthorizedStatusCode;
58+
59+
private volatile String verifiedAuthHeader;
5460

5561
/**
5662
* Constructs a new SpnegoAuthProvider with the given authenticator and GSSManager.
5763
*
5864
* @param authenticator the authenticator to use for JAAS login
5965
* @param gssManager the GSSManager to use for SPNEGO token generation
6066
*/
61-
private SpnegoAuthProvider(SpnegoAuthenticator authenticator, GSSManager gssManager) {
67+
private SpnegoAuthProvider(SpnegoAuthenticator authenticator, GSSManager gssManager, int unauthorizedStatusCode) {
6268
this.authenticator = authenticator;
6369
this.gssManager = gssManager;
70+
this.unauthorizedStatusCode = unauthorizedStatusCode;
6471
}
6572

6673
/**
6774
* Creates a new SPNEGO authentication provider using the default GSSManager instance.
6875
*
6976
* @param authenticator the authenticator to use for JAAS login
77+
* @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
7078
* @return a new SPNEGO authentication provider
7179
*/
72-
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator) {
73-
return create(authenticator, GSSManager.getInstance());
80+
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, int unauthorizedStatusCode) {
81+
return create(authenticator, GSSManager.getInstance(), unauthorizedStatusCode);
7482
}
7583

7684
/**
@@ -81,10 +89,11 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator) {
8189
*
8290
* @param authenticator the authenticator to use for JAAS login
8391
* @param gssManager the GSSManager to use for SPNEGO token generation
92+
* @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
8493
* @return a new SPNEGO authentication provider
8594
*/
86-
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSManager gssManager) {
87-
return new SpnegoAuthProvider(authenticator, gssManager);
95+
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSManager gssManager, int unauthorizedStatusCode) {
96+
return new SpnegoAuthProvider(authenticator, gssManager, unauthorizedStatusCode);
8897
}
8998

9099
/**
@@ -100,24 +109,32 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSMa
100109
* @throws RuntimeException if login or token generation fails
101110
*/
102111
public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
112+
String hostName = address.getHostName();
113+
if (verifiedAuthHeader != null) {
114+
request.header(HttpHeaderNames.AUTHORIZATION, verifiedAuthHeader);
115+
return Mono.empty();
116+
}
117+
103118
return Mono.fromCallable(() -> {
104119
try {
105120
return Subject.doAs(
106121
authenticator.login(),
107122
(PrivilegedAction<byte[]>) () -> {
108123
try {
109-
byte[] token = generateSpnegoToken(address.getHostName());
110-
String authHeader = "Negotiate " + Base64.getEncoder().encodeToString(token);
124+
byte[] token = generateSpnegoToken(hostName);
125+
String authHeader = SPNEGO_HEADER + " " + Base64.getEncoder().encodeToString(token);
126+
127+
verifiedAuthHeader = authHeader;
111128
request.header(HttpHeaderNames.AUTHORIZATION, authHeader);
112129
return token;
113130
}
114-
catch (GSSException e) {
131+
catch (GSSException e) {
115132
throw new RuntimeException("Failed to generate SPNEGO token", e);
116133
}
117134
}
118135
);
119136
}
120-
catch (LoginException e) {
137+
catch (LoginException e) {
121138
throw new RuntimeException("Failed to login with SPNEGO", e);
122139
}
123140
})
@@ -143,4 +160,36 @@ private byte[] generateSpnegoToken(String hostName) throws GSSException {
143160
GSSContext context = gssManager.createContext(serverName, spnegoOid, null, GSSContext.DEFAULT_LIFETIME);
144161
return context.initSecContext(new byte[0], 0, 0);
145162
}
163+
164+
/**
165+
* Invalidates the cached authentication token.
166+
* <p>
167+
* This method should be called when a response indicates that the current token
168+
* is no longer valid (typically after receiving an unauthorized status code).
169+
* The next request will generate a new authentication token.
170+
* </p>
171+
*/
172+
public void invalidateCache() {
173+
this.verifiedAuthHeader = null;
174+
}
175+
176+
/**
177+
* Checks if the response indicates an authentication failure that requires a new token.
178+
* <p>
179+
* This method checks both the status code and the WWW-Authenticate header to determine
180+
* if a new SPNEGO token needs to be generated.
181+
* </p>
182+
*
183+
* @param status the HTTP status code
184+
* @param headers the HTTP response headers
185+
* @return true if the response indicates an authentication failure
186+
*/
187+
public boolean isUnauthorized(int status, HttpHeaders headers) {
188+
if (status != unauthorizedStatusCode) {
189+
return false;
190+
}
191+
192+
String header = headers.get(HttpHeaderNames.WWW_AUTHENTICATE);
193+
return header != null && header.startsWith(SPNEGO_HEADER);
194+
}
146195
}

reactor-netty-http/src/test/java/reactor/netty/http/client/SpnegoAuthProviderTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ void negotiateSpnegoAuthenticationWithHttpClient() throws GSSException {
9191
principals.add(new KerberosPrincipal("test@LOCALHOST"));
9292
return new Subject(true, principals, new HashSet<>(), new HashSet<>());
9393
},
94-
gssManager
94+
gssManager,
95+
401
9596
)
9697
)
9798
.wiretap(true)

0 commit comments

Comments
 (0)