21
21
import io .netty .handler .codec .http .HttpHeaders ;
22
22
import java .net .InetSocketAddress ;
23
23
import java .security .PrivilegedAction ;
24
+ import java .util .Arrays ;
24
25
import java .util .Base64 ;
26
+ import java .util .concurrent .atomic .AtomicInteger ;
27
+ import java .util .concurrent .atomic .AtomicReference ;
25
28
import javax .security .auth .Subject ;
26
29
import javax .security .auth .login .LoginException ;
27
30
import org .ietf .jgss .GSSContext ;
30
33
import org .ietf .jgss .GSSName ;
31
34
import org .ietf .jgss .Oid ;
32
35
import reactor .core .publisher .Mono ;
36
+ import reactor .util .Logger ;
37
+ import reactor .util .Loggers ;
33
38
34
39
/**
35
40
* Provides SPNEGO authentication for Reactor Netty HttpClient.
50
55
*/
51
56
public final class SpnegoAuthProvider {
52
57
58
+ private static final Logger log = Loggers .getLogger (SpnegoAuthProvider .class );
53
59
private static final String SPNEGO_HEADER = "Negotiate" ;
54
60
private static final String STR_OID = "1.3.6.1.5.5.2" ;
55
61
56
62
private final SpnegoAuthenticator authenticator ;
57
63
private final GSSManager gssManager ;
58
64
private final int unauthorizedStatusCode ;
59
65
60
- private volatile String verifiedAuthHeader ;
66
+ private final AtomicReference <String > verifiedAuthHeader = new AtomicReference <>();
67
+ private final AtomicInteger retryCount = new AtomicInteger (0 );
68
+ private static final int MAX_RETRY_COUNT = 1 ;
61
69
62
70
/**
63
71
* Constructs a new SpnegoAuthProvider with the given authenticator and GSSManager.
@@ -110,8 +118,9 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSMa
110
118
* @throws SpnegoAuthenticationException if login or token generation fails
111
119
*/
112
120
public Mono <Void > apply (HttpClientRequest request , InetSocketAddress address ) {
113
- if (verifiedAuthHeader != null ) {
114
- request .header (HttpHeaderNames .AUTHORIZATION , verifiedAuthHeader );
121
+ String cachedToken = verifiedAuthHeader .get ();
122
+ if (cachedToken != null ) {
123
+ request .header (HttpHeaderNames .AUTHORIZATION , cachedToken );
115
124
return Mono .empty ();
116
125
}
117
126
@@ -124,7 +133,7 @@ public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
124
133
byte [] token = generateSpnegoToken (address .getHostName ());
125
134
String authHeader = SPNEGO_HEADER + " " + Base64 .getEncoder ().encodeToString (token );
126
135
127
- verifiedAuthHeader = authHeader ;
136
+ verifiedAuthHeader . set ( authHeader ) ;
128
137
request .header (HttpHeaderNames .AUTHORIZATION , authHeader );
129
138
return token ;
130
139
}
@@ -154,27 +163,61 @@ public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
154
163
* @throws GSSException if token generation fails
155
164
*/
156
165
private byte [] generateSpnegoToken (String hostName ) throws GSSException {
157
- GSSName serverName = gssManager .createName ("HTTP/" + hostName , GSSName .NT_HOSTBASED_SERVICE );
166
+ if (hostName == null || hostName .trim ().isEmpty ()) {
167
+ throw new IllegalArgumentException ("Host name cannot be null or empty" );
168
+ }
169
+
170
+ GSSName serverName = gssManager .createName ("HTTP/" + hostName .trim (), GSSName .NT_HOSTBASED_SERVICE );
158
171
Oid spnegoOid = new Oid (STR_OID ); // SPNEGO OID
159
172
160
- GSSContext context = gssManager . createContext ( serverName , spnegoOid , null , GSSContext . DEFAULT_LIFETIME ) ;
173
+ GSSContext context = null ;
161
174
try {
175
+ context = gssManager .createContext (serverName , spnegoOid , null , GSSContext .DEFAULT_LIFETIME );
162
176
return context .initSecContext (new byte [0 ], 0 , 0 );
163
- } finally {
164
- context .dispose ();
177
+ }
178
+ finally {
179
+ if (context != null ) {
180
+ try {
181
+ context .dispose ();
182
+ }
183
+ catch (GSSException e ) {
184
+ // Log but don't propagate disposal errors
185
+ if (log .isDebugEnabled ()) {
186
+ log .debug ("Failed to dispose GSSContext" , e );
187
+ }
188
+ }
189
+ }
165
190
}
166
191
}
167
192
168
193
/**
169
194
* Invalidates the cached authentication token.
170
- * <p>
171
- * This method should be called when a response indicates that the current token
172
- * is no longer valid (typically after receiving an unauthorized status code).
173
- * The next request will generate a new authentication token.
174
- * </p>
175
195
*/
176
- public void invalidateCache () {
177
- this .verifiedAuthHeader = null ;
196
+ public void invalidateTokenHeader () {
197
+ this .verifiedAuthHeader .set (null );
198
+ }
199
+
200
+ /**
201
+ * Checks if SPNEGO authentication retry is allowed.
202
+ *
203
+ * @return true if retry is allowed, false otherwise
204
+ */
205
+ public boolean canRetry () {
206
+ return retryCount .get () < MAX_RETRY_COUNT ;
207
+ }
208
+
209
+ /**
210
+ * Increments the retry count for SPNEGO authentication attempts.
211
+ */
212
+ public void incrementRetryCount () {
213
+ retryCount .incrementAndGet ();
214
+ }
215
+
216
+ /**
217
+ * Resets the retry count for SPNEGO authentication.
218
+ */
219
+ public void resetRetryCount () {
220
+ retryCount .set (0 );
178
221
}
179
222
180
223
/**
@@ -194,6 +237,13 @@ public boolean isUnauthorized(int status, HttpHeaders headers) {
194
237
}
195
238
196
239
String header = headers .get (HttpHeaderNames .WWW_AUTHENTICATE );
197
- return header != null && header .startsWith (SPNEGO_HEADER );
240
+ if (header == null ) {
241
+ return false ;
242
+ }
243
+
244
+ // More robust parsing - handle multiple comma-separated authentication schemes
245
+ return Arrays .stream (header .split ("," ))
246
+ .map (String ::trim )
247
+ .anyMatch (auth -> auth .toLowerCase ().startsWith (SPNEGO_HEADER .toLowerCase ()));
198
248
}
199
249
}
0 commit comments