Skip to content

Commit 96aa2ba

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

File tree

5 files changed

+73
-14
lines changed

5 files changed

+73
-14
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 operations = connection.as(HttpClientOperations.class);
450+
if (operations != null && handler.spnegoAuthProvider != null) {
451+
int statusCode = operations.status().code();
452+
HttpHeaders headers = operations.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,36 @@
4950
*/
5051
public final class SpnegoAuthProvider {
5152

53+
private static final String SPNEGO_HEADER = "Negotiate";
54+
private static final String STR_OID = "1.3.6.1.5.5.2";
55+
5256
private final SpnegoAuthenticator authenticator;
5357
private final GSSManager gssManager;
58+
private final int unauthorizedStatusCode;
59+
60+
private volatile String verifiedAuthHeader;
5461

5562
/**
5663
* Constructs a new SpnegoAuthProvider with the given authenticator and GSSManager.
5764
*
5865
* @param authenticator the authenticator to use for JAAS login
5966
* @param gssManager the GSSManager to use for SPNEGO token generation
6067
*/
61-
private SpnegoAuthProvider(SpnegoAuthenticator authenticator, GSSManager gssManager) {
68+
private SpnegoAuthProvider(SpnegoAuthenticator authenticator, GSSManager gssManager, int unauthorizedStatusCode) {
6269
this.authenticator = authenticator;
6370
this.gssManager = gssManager;
71+
this.unauthorizedStatusCode = unauthorizedStatusCode;
6472
}
6573

6674
/**
6775
* Creates a new SPNEGO authentication provider using the default GSSManager instance.
6876
*
6977
* @param authenticator the authenticator to use for JAAS login
78+
* @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
7079
* @return a new SPNEGO authentication provider
7180
*/
72-
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator) {
73-
return create(authenticator, GSSManager.getInstance());
81+
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, int unauthorizedStatusCode) {
82+
return create(authenticator, GSSManager.getInstance(), unauthorizedStatusCode);
7483
}
7584

7685
/**
@@ -81,10 +90,11 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator) {
8190
*
8291
* @param authenticator the authenticator to use for JAAS login
8392
* @param gssManager the GSSManager to use for SPNEGO token generation
93+
* @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
8494
* @return a new SPNEGO authentication provider
8595
*/
86-
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSManager gssManager) {
87-
return new SpnegoAuthProvider(authenticator, gssManager);
96+
public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSManager gssManager, int unauthorizedStatusCode) {
97+
return new SpnegoAuthProvider(authenticator, gssManager, unauthorizedStatusCode);
8898
}
8999

90100
/**
@@ -100,24 +110,31 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSMa
100110
* @throws RuntimeException if login or token generation fails
101111
*/
102112
public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
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 {
109124
byte[] token = generateSpnegoToken(address.getHostName());
110-
String authHeader = "Negotiate " + Base64.getEncoder().encodeToString(token);
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
})
@@ -138,9 +155,41 @@ public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
138155
*/
139156
private byte[] generateSpnegoToken(String hostName) throws GSSException {
140157
GSSName serverName = gssManager.createName("HTTP/" + hostName, GSSName.NT_HOSTBASED_SERVICE);
141-
Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); // SPNEGO OID
158+
Oid spnegoOid = new Oid(STR_OID); // SPNEGO OID
142159

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/main/java/reactor/netty/http/client/SpnegoAuthenticator.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
/**
2222
* An abstraction for authentication logic used by SPNEGO providers.
2323
* <p>
24-
* Implementations are responsible for performing a JAAS login and returning a logged-in Subject.
24+
* Implementations are responsible for performing a login and returning a logged-in Subject.
2525
* </p>
2626
*
2727
* @author raccoonback
@@ -30,9 +30,9 @@
3030
public interface SpnegoAuthenticator {
3131

3232
/**
33-
* Performs a JAAS login and returns the authenticated Subject.
33+
* Performs a login and returns the authenticated Subject.
3434
*
35-
* @return the authenticated JAAS Subject
35+
* @return the authenticated Subject
3636
* @throws LoginException if login fails
3737
*/
3838
Subject login() throws LoginException;

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)