18
18
import static reactor .core .scheduler .Schedulers .boundedElastic ;
19
19
20
20
import io .netty .handler .codec .http .HttpHeaderNames ;
21
+ import io .netty .handler .codec .http .HttpHeaders ;
21
22
import java .net .InetSocketAddress ;
22
23
import java .security .PrivilegedAction ;
23
24
import java .util .Base64 ;
49
50
*/
50
51
public final class SpnegoAuthProvider {
51
52
53
+ private static final String SPNEGO_HEADER = "Negotiate" ;
54
+ private static final String STR_OID = "1.3.6.1.5.5.2" ;
55
+
52
56
private final SpnegoAuthenticator authenticator ;
53
57
private final GSSManager gssManager ;
58
+ private final int unauthorizedStatusCode ;
59
+
60
+ private volatile String verifiedAuthHeader ;
54
61
55
62
/**
56
63
* Constructs a new SpnegoAuthProvider with the given authenticator and GSSManager.
57
64
*
58
65
* @param authenticator the authenticator to use for JAAS login
59
66
* @param gssManager the GSSManager to use for SPNEGO token generation
60
67
*/
61
- private SpnegoAuthProvider (SpnegoAuthenticator authenticator , GSSManager gssManager ) {
68
+ private SpnegoAuthProvider (SpnegoAuthenticator authenticator , GSSManager gssManager , int unauthorizedStatusCode ) {
62
69
this .authenticator = authenticator ;
63
70
this .gssManager = gssManager ;
71
+ this .unauthorizedStatusCode = unauthorizedStatusCode ;
64
72
}
65
73
66
74
/**
67
75
* Creates a new SPNEGO authentication provider using the default GSSManager instance.
68
76
*
69
77
* @param authenticator the authenticator to use for JAAS login
78
+ * @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
70
79
* @return a new SPNEGO authentication provider
71
80
*/
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 );
74
83
}
75
84
76
85
/**
@@ -81,10 +90,11 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator) {
81
90
*
82
91
* @param authenticator the authenticator to use for JAAS login
83
92
* @param gssManager the GSSManager to use for SPNEGO token generation
93
+ * @param unauthorizedStatusCode the HTTP status code that indicates authentication failure
84
94
* @return a new SPNEGO authentication provider
85
95
*/
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 );
88
98
}
89
99
90
100
/**
@@ -100,24 +110,31 @@ public static SpnegoAuthProvider create(SpnegoAuthenticator authenticator, GSSMa
100
110
* @throws RuntimeException if login or token generation fails
101
111
*/
102
112
public Mono <Void > apply (HttpClientRequest request , InetSocketAddress address ) {
113
+ if (verifiedAuthHeader != null ) {
114
+ request .header (HttpHeaderNames .AUTHORIZATION , verifiedAuthHeader );
115
+ return Mono .empty ();
116
+ }
117
+
103
118
return Mono .fromCallable (() -> {
104
119
try {
105
120
return Subject .doAs (
106
121
authenticator .login (),
107
122
(PrivilegedAction <byte []>) () -> {
108
123
try {
109
124
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 ;
111
128
request .header (HttpHeaderNames .AUTHORIZATION , authHeader );
112
129
return token ;
113
130
}
114
- catch (GSSException e ) {
131
+ catch (GSSException e ) {
115
132
throw new RuntimeException ("Failed to generate SPNEGO token" , e );
116
133
}
117
134
}
118
135
);
119
136
}
120
- catch (LoginException e ) {
137
+ catch (LoginException e ) {
121
138
throw new RuntimeException ("Failed to login with SPNEGO" , e );
122
139
}
123
140
})
@@ -138,9 +155,41 @@ public Mono<Void> apply(HttpClientRequest request, InetSocketAddress address) {
138
155
*/
139
156
private byte [] generateSpnegoToken (String hostName ) throws GSSException {
140
157
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
142
159
143
160
GSSContext context = gssManager .createContext (serverName , spnegoOid , null , GSSContext .DEFAULT_LIFETIME );
144
161
return context .initSecContext (new byte [0 ], 0 , 0 );
145
162
}
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
+ }
146
195
}
0 commit comments