如何防止浏览器发送NTLM凭据?

时间:2018-07-20 14:29:24

标签: kerberos ntlm spring-security-kerberos mit-kerberos

我正在一个我们要使用Spring Security Kerberos使用Kerberos身份验证的网站上工作。因此,我们不支持NTLM。当用户发出未经身份验证的请求时,服务器将通过HTTP 401进行响应,其标题为WWW-Authenticate:Negotiate。

问题: 对于某些用户/配置,浏览器将发送NTLM凭据。该服务器不一定在Windows上运行,因此无法处理NTLM凭据。

据我了解,“协商”的意思是“请尽可能向我发送Kerberos,否则请发送NTLM”。是否有其他设置显示“仅向我发送Kerberos”?还是可以通过某种方式告诉浏览器该站点仅支持Kerberos?

作为后续,为什么浏览器没有可用的Kerberos?在这种情况下,它们将登录到同一域。也许他们的凭证已过期?

1 个答案:

答案 0 :(得分:1)

Kerberos和Spnego不应混淆。尽管Spnego通常用于Kerberos身份验证,但Spnego并不总是表示Kerberos,甚至不是Kerberos的首选。

Spnego是一种协议,它允许客户端和服务器协商相互接受的机械类型(如果可用)。

根据协商过程中客户端和服务器请求的子机制,这可以是Kerberos,也可以不是。 协商过程可能需要进行几次握手尝试。

以人类语言为例。如果我按优先顺序说英语,拉丁语和祖鲁语,而您说爱斯基摩语和祖鲁语,那么我们最终会说祖鲁语。

在我目前正在测试的设置中,以Internet Explorer作为客户端,并使用JAAS + GSS作为服务器的自定义Java应用程序服务器,我观察到的行为与您的评论类似:

  1. 浏览器发送未经身份验证的请求
  2. 服务器使用未经授权的HTTP 401进行答复,WWW身份验证:协商标头。
  3. 浏览器要么以协商+ NTLM令牌响应(错误!)。

就我而言,游戏还没有结束,它继续如下:

  1. 服务器使用未经授权的HTTP 401进行回复,WWW进行身份验证:协商+ GSS响应令牌
  2. 浏览器以Negotiate + Spnego NegoTokenTarg封装Kerberos令牌作为响应。
  3. 服务器解开Kerberos令牌;解码并验证客户端;响应使用HTTP 200,WWW-Authenticate:协商+ GSS响应令牌

即我不会阻止浏览器发送NTLM令牌,我的服务器只是继续进行另一轮协商,直到获得Kerberos令牌为止。

作为一个附带问题:Internet Explorer 11在上述第3步中提供的令牌与Spnego不兼容,它既不是NegTokenInit也不是NetTokenTarg,并且127字节长显然太短了,无法打包。 Kerberos令牌。

您正在使用Spring Security Kerberos,但是在注释中您表示对其他库感兴趣,因此下面是我的基于JGSS的Spnego身份验证代码。

为简洁起见,我省去了JAAS设置,但是所有这些都在JAAS Subject.doAs()特权上下文中进行。

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]);
}

请注意,此代码以“原样”显示为例。它正在进行中,存在许多缺陷:

1)getTokenType()使用已解码的令牌,但extractKerberosToken可用于已编码的令牌,两者都应对已解码的令牌使用字节操作。

2)基于长度的令牌拒绝有点太简单了。我计划添加更好的NTLM令牌标识。...

3)我没有真正的GSS上下文循环。如果我不喜欢客户提出的内容,我将拒绝并关闭上下文。 对于客户端进行的以下握手尝试,我打开了一个新的GSS上下文。