2121import io .netty .handler .codec .http .HttpHeaders ;
2222import java .net .InetSocketAddress ;
2323import java .security .PrivilegedAction ;
24+ import java .util .Arrays ;
2425import java .util .Base64 ;
26+ import java .util .concurrent .atomic .AtomicInteger ;
27+ import java .util .concurrent .atomic .AtomicReference ;
2528import javax .security .auth .Subject ;
2629import javax .security .auth .login .LoginException ;
2730import org .ietf .jgss .GSSContext ;
3033import org .ietf .jgss .GSSName ;
3134import org .ietf .jgss .Oid ;
3235import reactor .core .publisher .Mono ;
36+ import reactor .util .Logger ;
37+ import reactor .util .Loggers ;
3338
3439/**
3540 * Provides SPNEGO authentication for Reactor Netty HttpClient.
5055 */
5156public final class SpnegoAuthProvider {
5257
58+ private static final Logger log = Loggers .getLogger (SpnegoAuthProvider .class );
5359 private static final String SPNEGO_HEADER = "Negotiate" ;
5460 private static final String STR_OID = "1.3.6.1.5.5.2" ;
5561
5662 private final SpnegoAuthenticator authenticator ;
5763 private final GSSManager gssManager ;
5864 private final int unauthorizedStatusCode ;
5965
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 ;
6169
6270 /**
6371 * Constructs a new SpnegoAuthProvider with the given authenticator and GSSManager.
@@ -110,8 +118,9 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSMa
110118 * @throws SpnegoAuthenticationException if login or token generation fails
111119 */
112120 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 );
115124 return Mono .empty ();
116125 }
117126
@@ -124,7 +133,7 @@ public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
124133 byte [] token = generateSpnegoToken (address .getHostName ());
125134 String authHeader = SPNEGO_HEADER + " " + Base64 .getEncoder ().encodeToString (token );
126135
127- verifiedAuthHeader = authHeader ;
136+ verifiedAuthHeader . set ( authHeader ) ;
128137 request .header (HttpHeaderNames .AUTHORIZATION , authHeader );
129138 return token ;
130139 }
@@ -154,27 +163,61 @@ public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
154163 * @throws GSSException if token generation fails
155164 */
156165 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 );
158171 Oid spnegoOid = new Oid (STR_OID ); // SPNEGO OID
159172
160- GSSContext context = gssManager . createContext ( serverName , spnegoOid , null , GSSContext . DEFAULT_LIFETIME ) ;
173+ GSSContext context = null ;
161174 try {
175+ context = gssManager .createContext (serverName , spnegoOid , null , GSSContext .DEFAULT_LIFETIME );
162176 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+ }
165190 }
166191 }
167192
168193 /**
169194 * 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>
175195 */
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 );
178221 }
179222
180223 /**
@@ -194,6 +237,13 @@ public boolean isUnauthorized(int status, HttpHeaders headers) {
194237 }
195238
196239 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 ()));
198248 }
199249}
0 commit comments