SPNEGO 身份验证适用于自定义 Java 客户端,但不适用于 Web 浏览器

SPNEGO Authentication Works from a Custom Java Client, but NOT from a Web Browser

我在通过 SPNEGO 从 Web 浏览器 (Internet Explorer 11) 向自定义 Java 应用程序服务器提供的 Web 服务进行身份验证时遇到问题。

我可以使用自定义 Java 客户端应用程序使用 SPNEGO 成功地向同一应用程序服务器进行身份验证。

自定义 Java 客户端和应用程序服务器的实现细节可以在下面找到。

我怀疑 Web 浏览器中的 SPNEGO 无法正常工作,因为:

a) 来自 Internet Explorer 的令牌是否是有效的 SPNEGO 令牌?

Web 浏览器提供的 GSSAPI 令牌与我的 Java 客户端提供的不同,可能不是有效的 SPNEGO / Kerberos 令牌。 Java 客户端提供以“协商 YIMMQA...”开头的授权 Header(确定),而 Web 浏览器提供以“协商 oYIMRz...”开头的授权Header(可能不行)。

和/或

b) 服务器主体名称的格式

由于历史原因,应用服务器 运行 使用的服务主体名称实际上是 Microsoft Active Directory 用户主体(格式 = "user@DOMAIN"),而我强烈怀疑 Web 浏览器 SPNEGO实现使用请求的 URL 来构建服务主体名称。事实上,这正是我的自定义 Java 客户端在 Linux 上针对 Linux 后端 运行 时所做的。

实施细节:

Java 应用程序服务器在 Windows Server 2012 上运行。Kerberos / SPNEGO 实现是纯 Java JAAS + GSSAPI。

Java 客户端在 Windows(7 / 10)上运行,并且可以配置为使用 Java SSPI(通过 Waffle)或 JAAS + GSSAPI。两种实现都会创建服务器接受的 GSS 令牌。

生成的 GSS / SPNEGO 令牌在 Web 服务请求(客户端)和响应(服务器)的 headers 中传输。

服务器正在使用 Oid“1.3.6.1.5.5.2”(SPNEGO) 和“1.2.840.113554.1.2.2”(Kerberos)。

使用自定义 Java 客户端进行测试(正常):

服务器能够在单次握手中验证 Java 客户端。 Java 客户端直接调用 Web 服务,授权 Header 以“协商 YIMMQA...”开头 在服务器上进行 Base64 解码后,gssapiData 长度为 3140 字节,并且对 acceptSecContext() 的调用成功。

如果我将 gssapiData 从此调用转换为字符串,并在其中搜索任何人类可读的内容,然后在开始时我会找到“EXAMPLE.COM”和“user-DEV”。这看起来像服务器正在使用的 SPN,一个 Active Directory 用户主体(“user-Dev@EXAMPLE.COM”)。

使用 Internet Explorer 11 进行测试(不正常):

来自浏览器的第一个调用具有空授权 Header。我的服务器提示“协商” —> 确定。

来自浏览器的第二个调用具有以“协商 YH4GBis...”开头的授权 Header。 Base64 解码后,gssapiData 的长度为 128 字节。显然这不包含服务票据。

如果我将 gssapiData 转换为字符串,我会在中间找到字符“NTLMSSP”。我猜浏览器建议使用 NTLM。我的服务器拒绝此调用。

来自浏览器的第三次调用具有以“协商 oYIMRz...”开头的授权 Header 一旦 Base64 解码,gssapiData 的长度为 3147 字节(非常接近 Java客户)。

然而,当我的服务器对此执行 acceptSecContext() 时,它会抛出错误。 “GSS 异常:检测到有缺陷的令牌(机制级别:GSS Header 未找到正确的标签)。 —> 不行。

这表明令牌无效,或者我使用了错误的 Oid 来读取它。

如果我将此调用中的 gssapiData 转换为字符串,那么在开始时我会找到“HTTP”和“APPSERVER.example.com”。这看起来像是使用 URL 作为基础构建的 Kerberos 服务主体名称 (SPN)。 —> 这表明我的应用程序服务器应该是 运行 格式类似于“HTTP/APPSERVER.example.com”或“HTTP/appserver.example.com 的 SPN @EXAMPLE.COM”(第二个是我的 Linux / FreeIPA 配置使用的格式)。

附带说明:在这个问题的焦点 Windows 平台上,我无权创建/更改相同的 SPN 或别名,或尝试不同的 Web 浏览器。在我的 Linux 开发环境中,我可以提供额外的输入。 . .

快速回答

需要两个修复:

1) Internet Explorer (IE) 基于 URL 构建服务主体名称 (SPN)。例如https://appserver.example.com/foo 结果为 SPN "HTTP/APPSERVER.example.com"。

因此,必须在 Active Directory 中以上述格式设置正确的服务主体名称作为应用程序服务器使用的用户主体名称 (UPN) 的别名。

2) 来自 Internet Explorer 的令牌是有效的 SPNEGO 令牌,但未被服务器上的 GSS API 接受。

然而,通过对传入令牌进行一些简单的字符串操作,可以提取 GSS 将接受并成功进行身份验证的 Kerbeos 令牌。

更长的答案

1) 服务主体名称...

在此处发布此问题后,我们设置了 2 个 SPN HTTP/APPSERVER.example.com 和 HTTP/APPSERVER 作为服务器 UPN user-Dev@EXAMPLE.COM 的别名.

服务器继续 运行 使用 UPN user-Dev@EXAMPLE.COM。 (我最初假设服务器必须 运行 使用新的 SPN 之一是错误的。)

我的 Java 客户端现在可以使用新的 SPN 来获取 Kerberos 服务票证并创建由我的服务器成功验证的令牌。

但是来自 Internet Explorer 的令牌继续被拒绝。

2) 来自 Internet Explorer 的令牌与我的 Java 客户端...

我的 Java 客户端的令牌是这样开始的:

协商 YIIMdwYGKwYBBQUCoI....

Base64 解码,并表示为十六进制字节:

60 82 0C 77 06 06 2B 06 01 05 05 02 A0…………

其中 06 06 2B 06 01 05 05 02 是 SPNEGO OID 1.3.6.1.5.5.2 .

来自 Internet Explorer 的令牌是这样开头的:

协商 oYIMPjCCDDqgAwoBAaKCDDEEggwtYIIMKQYJKoZIhvcSAQIC

Base64 解码,并表示为十六进制字节:

A1 82 0C 3E 30 82 0C 3A A0 03 0A 01 01 A2 82 0C 31 04 82 0C 2D 60 82 0C 29 06 09 2A 86 48 86 F7 12 01 02 02 ....

这是一个 Spnego NegTokenTarg,因为它以 "A1" 开头。但是 java class sun.security.jgss.GSSHeader 将拒绝任何不以“60”开头的 GSS 令牌。

逐字节检查 IE NegTokenTarg 显示,在前 21 个字节之后,我有一系列字节非常接近我的应用程序的令牌:

60 82 0C 29 06 09 2A 86 48 86 F7 12 01 02 02....

其中06 09 2A 86 48 86 F7 12 01 02 02是Kerberos OID 1.2.840.113554.1.2.2

如果我通过丢弃原始令牌的前 21 个字节(或提供偏移量为 21 的 gssContext.acceptSecContext(gssapiData, offset, gssapiData.length) 来提取此令牌,则 GSS API 能够读取新令牌,提取用户主体,从而验证来自 Internet Explorer 的请求。

下面的代码示例使用 base64 编码授权字符串的字符串操作来实现相同的目的:

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());
    if (authBody.startsWith("oY")) {
        // This is a NegTokenTarg from IE, which GSS API does not properly handle.
        // However if we chop of the first (28) chars, we find a Kerberos Token starting with "60 82 0C" that GSS can handle.            
        authBody=authBody.substring(authBody.indexOf("YI", 2));
     }

     try {                 
         byte gssapiData[] = Base64.getDecoder().decode(authBody);               
         gssContext = initGSSContext(MyUtils.SPNEGOOID, MyUtils.KRB5OID);
         byte token[] = gssContext.acceptSecContext(gssapiData, offset, gssapiData.length);


         ..etc.

总而言之,我想我们要么

a) Java GSS API 弱点:GSS 不直接接受作为 NegTokenTarg 的 SPNEGO 令牌。

b) Internet Explorer 和我的服务器之间的相互作用,导致 IE 发送 NegTokenTarg,这不是 GSS 所期望的 API。

IE - 服务器交互是:

1) 来自 IE 的请求(没有协商)

2) 从我的服务器拒绝,协商

3) 来自 IE 的第二个请求,带有协商 Header + 看起来像 NTLM 而不是 Kerberos 的令牌。 --> 这可能是路由问题的原因。

4) 使用协商 + SPNEGO 令牌从我的服务器拒绝

5) 来自 IE 的第三个请求,协商 Header + SPNEGO NegTokenTarg

背景信息:

我的应用程序服务器使用 Java JAAS + GSS 来实现 Kerberos / Spnego 功能。我的自定义客户端可以使用 Java JAAS + GSS 或 Microsoft SSPI + Waffle。

我发现此 Microsoft 文档对于理解 SPNEGO 令牌的格式非常有帮助。

https://msdn.microsoft.com/en-us/library/ms995330.aspx

和这篇博客了解如何“处理”负十进制字节。 (数字 -128 到 -1 转换为 128 到 255)。

http://sketchytech.blogspot.com/2015/11/bytes-for-beginners-representation-of.html

由于我的目标 end-users 的公司标准浏览器是 Internet Explorer,没有使用“更好”的东西的现实选择,在谷歌搜索时我遇到了 Chromium 和 Firefox 的 SPN 处理代码。链接如下。 Chromium 代码有广泛的评论和指向知识库文章和 SME 博客的链接。

Chromiumn 代码

https://cs.chromium.org/chromium/src/net/http/http_auth_handler_negotiate.cc?type=cs&l=142

FireFox 代码

https://dxr.mozilla.org/mozilla-central/source/extensions/auth/nsAuthSSPI.cpp#98