1818import static reactor .core .scheduler .Schedulers .boundedElastic ;
1919
2020import io .netty .handler .codec .http .HttpHeaderNames ;
21+ import io .netty .handler .codec .http .HttpHeaders ;
2122import java .net .InetSocketAddress ;
2223import java .security .PrivilegedAction ;
2324import java .util .Base64 ;
4950 */
5051public 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}
0 commit comments