I’m working on a site where we want to use Kerberos authentication using Spring Security Kerberos. So, we don’t support NTLM. When the user makes an unauthenticated request, the server will reply with an HTTP 401 with header WWW-Authenticate: Negotiate.
The problem: For some users/configurations, the browser will send NTLM credentials. The server is not necessarily running on Windows so it can’t handle the NTLM credentials.
As I understand, “Negotiate” means “please send me Kerberos if possible, or else send NTLM”. Is there a different setting that says “only send me Kerberos”? Or is there some way to tell the browsers the site only supports Kerberos?
As a follow-up, why would the browser not have Kerberos available? In this case they are logged in to the same domain. Maybe their credentials have expired?
To disable NTLM, use the Group Policy setting Network Security: Restrict NTLM. If necessary, you can create an exception list to allow specific servers to use NTLM authentication. At a minimum, you want to disable NTLMv1 because it is a glaring security hole in your environment.
To find applications that use NTLMv1, enable Logon Success Auditing on the domain controller, and then look for Success auditing Event 4624, which contains information about the version of NTLM.
In the administration interface, go to Configuration > Domains and User Login. Go to the Authentication Options tab. (Optional) Check the option Always require users to be authenticated when accessing web pages. Check Enable automatic authentication using NTLM.
Kerberos and Spnego should not be confused. Though Spnego is often used for Kerberos authentication, Spnego does not always mean Kerberos, or even a preference for Kerberos.
Spnego is a protocol that allows client and server to negotiate a mutually acceptable mech type (if available).
That may or may not be Kerberos depending on the sub-mechanisms requested by the client and server during the negotiation process. The Negotiation process may take several handshake attempts.
Using human languages as an example. If I speak English, Latin and Zulu, in that order of preference, and you speak Eskimau and Zulu, then we will end up speaking Zulu.
In the setup that I am currently testing, with Internet Explorer as a client, and a custom Java Application Server using JAAS + GSS as the Server I observe similar behavour to that in your comment:
In my case the game does not end there, it continues as follows:
i.e. I don't prevent the browser sending an NTLM token, my Server just continues negotiation for another round until it gets a Kerberos Token.
As a side issue: the token provided by Internet Explorer 11 at step 3. above is not properly Spnego compliant, it is neither a NegTokenInit, nor a NetTokenTarg, and at 127 bytes long is clearly much too short to be or wrap a Kerberos token.
You are using Spring Security Kerberos, but in a comment you indicate an interest in other libraries, so below is my JGSS based Spnego authentication code.
For brevity I leave out the JAAS setup, but all this takes place in a JAAS Subject.doAs() privileged context.
public static final String NEGOTIATE = "Negotiate ";
public static final String AUTHORIZATION = "Authorization";
public static final String WWWAUTHENTICATE = "WWW-Authenticate";
public static final int HTTP_OK = 200;
public static final int HTTP_GOAWAY = 401; //Unauthorized
public static final String SPNEGOOID = "1.3.6.1.5.5.2";
public static final String KRB5OID = "1.2.840.113554.1.2.2";
public void spnegoAuthenticate(Request req, Response resp, Service http) {
GSSContext gssContext = null;
String kerberosUser = null;
String auth =req.headers("Authorization");
if ( auth != null && auth.startsWith(NEGOTIATE )) {
//smells like an SPNEGO request, so get the token from the http headers
String authBody = auth.substring(NEGOTIATE.length());
int offset =0;
// As GSS cannot directly process Spnego NegTokenInit and NegTokenTarg, preprocess and extract native Kerberos token.
authBody = preProcessToken(authBody);
try {
byte gssapiData[] = Base64.getDecoder().decode(authBody);
gssContext = initGSSContext(SPNEGOOID, KRB5OID);
byte token[] = gssContext.acceptSecContext(gssapiData, offset, gssapiData.length);
if (gssapiData.length > 128) {
//extract the Kerberos User. The Execute/Login service will compare this with the user in the message body.
kerberosUser = gssContext.getSrcName().toString();
resp.status(HTTP_OK);
} else {
//Is too short to be a kerberos token (or to wrap one), so don't try and extract the user.
//This could be a first pass from an SPNEGO enabled Web-browser. Maybe NTLM?
resp.status(HTTP_GOAWAY);
}
String responseToken = Base64.getEncoder().encodeToString(token);
if (responseToken != null && responseToken.length() > 0) {
resp.header(WWWAUTHENTICATE, NEGOTIATE + responseToken);
}
} catch (GSSException e) {
// Something went wrong fishing the token from the http headers
http.halt(401, "Go Away! This is a privileged route, and you ain't privileged!"+"\r\n");
} finally {
try {
gssContext.dispose();
} catch (GSSException e) {
//error handling here
}
}
} else {
//This is either not a SPNEGO request, or is the first pass without token
resp.header(WWWAUTHENTICATE, NEGOTIATE.trim()); //set header to suggest negotiation
http.halt(HTTP_GOAWAY, "Go Away! This is a privileged route, and you ain't privileged! Only come back when you are."+"\r\n");
}
}
private String preProcessToken(String authBody) {
String tag = getTokenType(authBody);
if (tag.equals("60")) {
// is a standard "application constructed" token. Kerberos tokens seem to start with "YI.."
} else if (tag.equals("A0")) {
// is a Spnego NegTokenInit, starting with "oA.." to "oP.."
authBody=extractKerberosToken(authBody);
} else if (tag.equals("A1")) {
// is a Spnego NegTokenTarg, starting with "oQ.." to "oZ.."
authBody=extractKerberosToken(authBody);
} else {
// some other unexpected token.
// TODO: generate error
}
return authBody;
}
private String extractKerberosToken(String authBody) {
return authBody.substring(authBody.indexOf("YI", 2));
}
private String getTokenType(String authBody) {
return String.format("%02X", Base64.getDecoder().decode(authBody.substring(0,2))[0]);
}
Note this code is presented "as-is", as an example. It is work-in-progress and has a number of flaws:
1) getTokenType() uses the decoded token, but extractKerberosToken works on the encoded token, both should use byte operations on the decoded token.
2) Token rejection based on length is a little too simple. I plan to add better NTLM token identification....
3) I don't have a true GSS context loop. If I don't like what the client presents, I reject and close the context. For any following handshake attempts from the client I open a new GSS context.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With