Skip to content

Commit 30a337b

Browse files
committed
Support GSSCredential-based SPNEGO authentication
Signed-off-by: raccoonback <kosb15@naver.com>
1 parent a77c0a5 commit 30a337b

File tree

8 files changed

+431
-182
lines changed

8 files changed

+431
-182
lines changed

docs/modules/ROOT/pages/http-client.adoc

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -750,24 +750,26 @@ SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) provides secure authe
750750

751751
==== How It Works
752752
SPNEGO authentication follows this HTTP authentication flow:
753-
1. The client sends an HTTP request to a protected resource.
754-
2. The server responds with `401 Unauthorized` and a `WWW-Authenticate: Negotiate` header.
755-
3. The client generates a SPNEGO token based on its Kerberos ticket, and resends the request with an `Authorization: Negotiate <base64-encoded-token>` header.
756-
4. The server validates the token and, if authentication is successful, returns 200 OK.
757753

758-
If further negotiation is required, the server may return another 401 with additional data in the WWW-Authenticate header.
754+
. The client sends an HTTP request to a protected resource.
755+
. The server responds with `401 Unauthorized` and a `WWW-Authenticate: Negotiate` header.
756+
. The client generates a SPNEGO token based on its Kerberos ticket, and resends the request with an `Authorization: Negotiate <base64-encoded-token>` header.
757+
. The server validates the token and, if authentication is successful, returns 200 OK.
759758

760-
{examples-link}/spnego/Application.java
759+
If further negotiation is required, the server may return another 401 with additional data in the `WWW-Authenticate` header.
760+
761+
==== JAAS-based Authenticator
762+
{examples-link}/spnego/jaas/Application.java
761763
----
762-
include::{examples-dir}/spnego/Application.java[lines=18..39]
764+
include::{examples-dir}/spnego/jaas/Application.java[lines=18..45]
763765
----
764766
<1> Configures the `jaas.conf`. A JAAS configuration file in Java for integrating with authentication backends such as Kerberos.
765767
<2> Configures the `krb5.conf`. krb5.conf is a Kerberos client configuration file used to define how the client locates and communicates with the Kerberos Key Distribution Center (KDC) for authentication.
766768
<3> Configures the SPNEGO jaas.conf. A JVM system property that enables detailed debug logging for Kerberos operations in Java.
767769
<4> `JaasAuthenticator` performs Kerberos authentication using a JAAS configuration (jaas.conf).
768-
<5> `SpnegoAuthProvider` generates a SPNEGO token from the Kerberos ticket and automatically adds the `Authorization: Negotiate ...` header to HTTP requests. If the server responds with `401 Unauthorized` and includes `WWW-Authenticate: Negotiate`, the client will automatically reauthenticate and retry the request once.
770+
<5> `SpnegoAuthProvider.Builder` supports the following configuration methods. Please refer to <<spnegoauthprovider-config>>.
771+
<6> `SpnegoAuthProvider` generates a SPNEGO token from the Kerberos ticket. It automatically adds the `Authorization: Negotiate ...` header to HTTP requests. If the server responds with `401 Unauthorized` and includes `WWW-Authenticate: Negotiate`, the client will automatically reauthenticate and retry the request once.
769772

770-
==== Environment Configuration
771773
===== Example JAAS Configuration
772774
Specify the path to your JAAS configuration file using the `java.security.auth.login.config` system property.
773775

@@ -809,6 +811,75 @@ Specify Kerberos realm and KDC information using the `java.security.krb5.conf` s
809811
-Djava.security.krb5.conf=/path/to/krb5.conf
810812
----
811813

814+
==== GSSCredential-based Authenticator
815+
For scenarios where you already have a `GSSCredential` available or want to avoid JAAS configuration, you can use `GssCredentialAuthenticator`:
816+
817+
{examples-link}/spnego/gsscredential/Application.java
818+
----
819+
include::{examples-dir}/spnego/gsscredential/Application.java[lines=18..46]
820+
----
821+
<1> Obtain the `GSSCredential` through other means.
822+
<2> Configure the GSSCredential-based authenticator for SPNEGO authentication.
823+
824+
This approach is useful when:
825+
- You want to reuse existing credentials
826+
- You need more control over credential management
827+
- JAAS configuration is not available or preferred
828+
829+
==== Custom Authenticator Implementation
830+
For advanced scenarios where the provided authenticators don't meet your specific requirements, you can implement the `SpnegoAuthenticator` interface directly:
831+
832+
----
833+
import org.ietf.jgss.GSSContext;
834+
import reactor.netty.http.client.SpnegoAuthenticator;
835+
import reactor.netty.http.client.SpnegoAuthProvider;
836+
837+
public class CustomSpnegoAuthenticator implements SpnegoAuthenticator {
838+
839+
@Override
840+
public GSSContext createContext(String serviceName, String remoteHost) throws Exception {
841+
// Your custom authentication logic here
842+
// This method should return a configured GSSContext
843+
// for the specified service and remote host
844+
// serviceName: e.g., "HTTP", "LDAP"
845+
// remoteHost: target server hostname
846+
847+
return null; // Replace with actual GSSContext creation logic
848+
}
849+
}
850+
851+
// Usage with advanced configuration
852+
HttpClient client = HttpClient.create()
853+
.spnego(
854+
SpnegoAuthProvider.builder(new CustomSpnegoAuthenticator())
855+
.serviceName("HTTP") // Custom service name
856+
.unauthorizedStatusCode(401) // Custom status code
857+
.resolveCanonicalHostname(true) // Use canonical hostname
858+
.build()
859+
);
860+
----
861+
862+
This approach is useful when you need:
863+
- Custom credential acquisition logic
864+
- Integration with third-party authentication systems
865+
- Special handling for token caching or refresh
866+
- Environment-specific authentication flows
867+
868+
[[spnegoauthprovider-config]]
869+
==== SpnegoAuthProvider Configuration Options
870+
The `SpnegoAuthProvider.Builder` supports the following configuration Options:
871+
872+
[width="100%",options="header"]
873+
|=======
874+
| Method | Default | Description | Example
875+
| `serviceName(String)` | "HTTP" | Service name for constructing service principal names (serviceName/hostname) | "HTTP", "LDAP"
876+
| `unauthorizedStatusCode(int)` | 401 | HTTP status code that triggers authentication retry | 401, 407
877+
| `resolveCanonicalHostname(boolean)` | false | Whether to use canonical hostname resolution via reverse DNS lookup | true for FQDN requirements
878+
|=======
879+
812880
==== Notes
813881
- SPNEGO authentication is fully supported on Java 1.6 and above.
814-
- If authentication fails, check the server logs and client exception messages, and verify your Kerberos environment settings (realm, KDC, ticket, etc.).
882+
- If authentication fails, check the server logs and client exception messages, and verify your Kerberos environment settings (realm, KDC, ticket, etc.).
883+
- `JaasAuthenticator` performs authentication through JAAS login configuration.
884+
- `GssCredentialAuthenticator` uses pre-existing `GSSCredential` objects, bypassing JAAS configuration.
885+
- For custom scenarios, implement the `SpnegoAuthenticator` interface to provide your own authentication logic.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2025 VMware, Inc. or its affiliates, All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package reactor.netty.examples.documentation.http.client.spnego.gsscredential;
17+
18+
import org.ietf.jgss.GSSCredential;
19+
import reactor.netty.http.client.GssCredentialAuthenticator;
20+
import reactor.netty.http.client.HttpClient;
21+
import reactor.netty.http.client.SpnegoAuthProvider;
22+
23+
public class Application {
24+
25+
public static void main(String[] args) {
26+
// Assuming you have obtained a GSSCredential from elsewhere
27+
GSSCredential credential = obtainGSSCredential(); // <1>
28+
29+
HttpClient client = HttpClient.create()
30+
.spnego(
31+
SpnegoAuthProvider.builder(new GssCredentialAuthenticator(credential)) // <2>
32+
.build()
33+
);
34+
35+
client.get()
36+
.uri("http://protected.example.com/")
37+
.responseSingle((res, content) -> content.asString())
38+
.block();
39+
}
40+
41+
private static GSSCredential obtainGSSCredential() {
42+
// Implement your logic to obtain a GSSCredential
43+
// This could involve using a Kerberos library or other means
44+
return null;
45+
}
46+
}
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package reactor.netty.examples.documentation.http.client.spnego;
16+
package reactor.netty.examples.documentation.http.client.spnego.jaas;
1717

1818
import reactor.netty.http.client.HttpClient;
1919
import reactor.netty.http.client.JaasAuthenticator;
@@ -28,8 +28,15 @@ public static void main(String[] args) {
2828
System.setProperty("sun.security.krb5.debug", "true"); // <3>
2929

3030
SpnegoAuthenticator authenticator = new JaasAuthenticator("KerberosLogin"); // <4>
31+
SpnegoAuthProvider provider = SpnegoAuthProvider.builder(authenticator)
32+
.serviceName("HTTP")
33+
.unauthorizedStatusCode(401)
34+
.resolveCanonicalHostname(false)
35+
.build();
3136
HttpClient client = HttpClient.create()
32-
.spnego(SpnegoAuthProvider.create(authenticator, 401)); // <5>
37+
.spnego(
38+
provider // <5>
39+
); // <6>
3340

3441
client.get()
3542
.uri("http://protected.example.com/")
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright (c) 2025 VMware, Inc. or its affiliates, All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package reactor.netty.http.client;
17+
18+
import java.util.Objects;
19+
import org.ietf.jgss.GSSContext;
20+
import org.ietf.jgss.GSSCredential;
21+
import org.ietf.jgss.GSSManager;
22+
import org.ietf.jgss.GSSName;
23+
import org.ietf.jgss.Oid;
24+
25+
/**
26+
* A GSSCredential-based Authenticator implementation for use with SPNEGO providers.
27+
* <p>
28+
* This authenticator uses a pre-existing GSSCredential to create a GSSContext,
29+
* bypassing the need for JAAS login configuration. This is useful when you already
30+
* have obtained Kerberos credentials through other means or want more direct control
31+
* over the authentication process.
32+
* </p>
33+
* <p>
34+
* The GSSCredential should contain valid Kerberos credentials that can be used
35+
* for SPNEGO authentication. The credential's lifetime and validity are managed
36+
* externally to this authenticator.
37+
* </p>
38+
*
39+
* <p>Example usage:</p>
40+
* <pre>
41+
* GSSCredential credential = // ... obtain credential
42+
* GssCredentialAuthenticator authenticator = new GssCredentialAuthenticator(credential);
43+
* SpnegoAuthProvider provider = SpnegoAuthProvider.builder(authenticator).build();
44+
* </pre>
45+
*
46+
* @author raccoonback
47+
* @since 1.3.0
48+
*/
49+
public class GssCredentialAuthenticator implements SpnegoAuthenticator {
50+
51+
private final GSSCredential credential;
52+
53+
/**
54+
* Creates a new GssCredentialAuthenticator with the given GSSCredential.
55+
*
56+
* @param credential the GSSCredential to use for authentication
57+
*/
58+
public GssCredentialAuthenticator(GSSCredential credential) {
59+
Objects.requireNonNull(credential, "GSSCredential cannot be null");
60+
this.credential = credential;
61+
}
62+
63+
/**
64+
* Creates a GSSContext for the specified service and remote host using the provided GSSCredential.
65+
* <p>
66+
* This method uses the pre-existing GSSCredential to create a GSSContext for SPNEGO
67+
* authentication. The service principal name is constructed as serviceName/remoteHost.
68+
* </p>
69+
*
70+
* @param serviceName the service name (e.g., "HTTP", "FTP")
71+
* @param remoteHost the remote host to authenticate with
72+
* @return the created GSSContext configured for SPNEGO authentication
73+
* @throws Exception if context creation fails
74+
*/
75+
@Override
76+
public GSSContext createContext(String serviceName, String remoteHost) throws Exception {
77+
GSSManager manager = GSSManager.getInstance();
78+
GSSName serverName = manager.createName(serviceName + "/" + remoteHost, GSSName.NT_HOSTBASED_SERVICE);
79+
GSSContext context = manager.createContext(
80+
serverName,
81+
new Oid("1.3.6.1.5.5.2"),
82+
credential,
83+
GSSContext.DEFAULT_LIFETIME
84+
);
85+
context.requestMutualAuth(true);
86+
return context;
87+
}
88+
}

reactor-netty-http/src/main/java/reactor/netty/http/client/JaasAuthenticator.java

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,42 +15,78 @@
1515
*/
1616
package reactor.netty.http.client;
1717

18+
import java.security.PrivilegedExceptionAction;
1819
import javax.security.auth.Subject;
1920
import javax.security.auth.login.LoginContext;
20-
import javax.security.auth.login.LoginException;
21+
import org.ietf.jgss.GSSContext;
22+
import org.ietf.jgss.GSSManager;
23+
import org.ietf.jgss.GSSName;
24+
import org.ietf.jgss.Oid;
2125

2226
/**
2327
* A JAAS-based Authenticator implementation for use with SPNEGO providers.
2428
* <p>
25-
* This authenticator performs a JAAS login using the specified context name and returns the authenticated Subject.
29+
* This authenticator performs a JAAS login using the specified context name and creates a GSSContext
30+
* for SPNEGO authentication. It relies on JAAS configuration to obtain Kerberos credentials.
31+
* </p>
32+
* <p>
33+
* The JAAS configuration should define a login context that acquires Kerberos credentials,
34+
* typically using the Krb5LoginModule. The login context name provided to this authenticator
35+
* must match the entry name in the JAAS configuration file.
2636
* </p>
2737
*
38+
* <p>Example usage:</p>
39+
* <pre>
40+
* JaasAuthenticator authenticator = new JaasAuthenticator("KerberosLogin");
41+
* SpnegoAuthProvider provider = SpnegoAuthProvider.builder(authenticator).build();
42+
* </pre>
43+
*
2844
* @author raccoonback
2945
* @since 1.3.0
3046
*/
3147
public class JaasAuthenticator implements SpnegoAuthenticator {
3248

33-
private final String contextName;
49+
private final String loginContext;
3450

3551
/**
3652
* Creates a new JaasAuthenticator with the given context name.
3753
*
38-
* @param contextName the JAAS login context name
54+
* @param loginContext the JAAS login context name
3955
*/
40-
public JaasAuthenticator(String contextName) {
41-
this.contextName = contextName;
56+
public JaasAuthenticator(String loginContext) {
57+
this.loginContext = loginContext;
4258
}
4359

4460
/**
45-
* Performs a JAAS login using the configured context name and returns the authenticated Subject.
61+
* Creates a GSSContext for the specified service and remote host using JAAS authentication.
62+
* <p>
63+
* This method performs a JAAS login, obtains the authenticated Subject, and creates
64+
* a GSSContext within the Subject's security context. The service principal name
65+
* is constructed as serviceName/remoteHost.
66+
* </p>
4667
*
47-
* @return the authenticated JAAS Subject
48-
* @throws LoginException if login fails
68+
* @param serviceName the service name (e.g., "HTTP", "CIFS")
69+
* @param remoteHost the remote host to authenticate with
70+
* @return the created GSSContext configured for SPNEGO authentication
71+
* @throws Exception if JAAS login or context creation fails
4972
*/
5073
@Override
51-
public Subject login() throws LoginException {
52-
LoginContext context = new LoginContext(contextName);
53-
context.login();
54-
return context.getSubject();
74+
public GSSContext createContext(String serviceName, String remoteHost) throws Exception {
75+
LoginContext lc = new LoginContext(loginContext);
76+
lc.login();
77+
Subject subject = lc.getSubject();
78+
79+
return Subject.doAs(subject, (PrivilegedExceptionAction<GSSContext>) () -> {
80+
GSSManager manager = GSSManager.getInstance();
81+
GSSName serverName = manager.createName(serviceName + "/" + remoteHost, GSSName.NT_HOSTBASED_SERVICE);
82+
GSSContext context = manager.createContext(
83+
serverName,
84+
new Oid("1.3.6.1.5.5.2"), // SPNEGO
85+
null,
86+
GSSContext.DEFAULT_LIFETIME
87+
);
88+
context.requestMutualAuth(true);
89+
return context;
90+
});
5591
}
5692
}

0 commit comments

Comments
 (0)