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+ 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}
0 commit comments