Search code examples
javakerberosjaasspnegogssapi

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


I am having problems authenticating via SPNEGO from a Web Browser (Internet Explorer 11) to a Web Service offered by a custom Java Application Server.

I can successfully authenticate using SPNEGO to the same Application Server using a custom Java Client Application.

Implementation details of the custom Java Client and Application server can be found below.

I suspect that SPNEGO from a Web Browser is not working because:

a) Is the Token from Internet Explorer a valid SPNEGO Token?

The GSSAPI token provided by the Web Browser is different to that provided by my Java Client, and may not be a valid SPNEGO / Kerberos token. The Java Client provides an Authorization Header starting with “Negotiate YIMMQA...” (OK), whereas the Web Browser provides an Authorization Header starting with “Negotiate oYIMRz...” (Probably NOT OK).

and / or

b) Format of the Server Principal Name

For historical reasons, the Application Server is running using a Service Principal Name that is actually a Microsoft Active Directory User Principal (format = "user@DOMAIN"), whereas I strongly suspect that Web Browser SPNEGO implementations use the requested URL to build a Service Principal Name. Indeed this is exactly what my custom Java Client does when running on Linux against a Linux backend.

Implementation Details:

The Java Application Server runs on Windows Server 2012. The Kerberos / SPNEGO implementation is pure Java JAAS + GSSAPI.

The Java Client runs on Windows (7 / 10), and can be configured to use either Java SSPI (via Waffle), or JAAS + GSSAPI. Both implementations create GSS Tokens accepted by the Server.

The GSS / SPNEGO tokens generated are transported in the headers of Web Service requests (Client) and responses (Server).

The Server is using the Oids "1.3.6.1.5.5.2" (SPNEGO) and "1.2.840.113554.1.2.2" (Kerberos).

Testing using the Custom Java Client (OK):

The Server is able to authenticate the Java Client in a single handshake. The Java Client directly calls the Web Service with an Authorization Header starting with “Negotiate YIMMQA...” After Base64 decoding on the Server the gssapiData 3140 bytes long and the call to acceptSecContext() is successful.

If I convert the gssapiData from this call to a string, and search this for anything human readable, then towards the start I find “EXAMPLE.COM” and “user-DEV”. This looks like the SPN that the Server is using, an Active Directory User Principal (“[email protected]”).

Testing using Internet Explorer 11 (NOT OK):

The first call from the Browser has an empty Authorization Header. My Server prompts with “Negotiate” —> OK.

The second call from the Browser has an Authorization Header starting with “Negotiate YH4GBis...”. Once Base64 decoded, the gssapiData is 128 bytes long. Clearly this does not contain a service ticket.

If I convert the gssapiData to a string, in the middle I find the characters “NTLMSSP”. I guess the Browser is suggesting NTLM. My Server rejects this call.

The third call from the Browser has an Authorization Header starting with “Negotiate oYIMRz...” Once Base64 decoded, the gssapiData is 3147 bytes long (very close to the length of that from the Java Client).

However when my Server does a acceptSecContext() on this, It throws the error. “GSS Exception: Defective Token detected (Mechanism level: GSS Header did not find the right tag). —> NOT OK.

This suggests to me that the token is not valid, or I am using the wrong Oids to read it.

If I convert the gssapiData from this call to a string, then towards the start I find “HTTP” and “APPSERVER.example.com”. This looks like a Kerberos Service Principal Name (SPN) built using the URL as a basis. —> This suggests to me that my Application Server should be running with an SPN in a format something like “HTTP/APPSERVER.example.com” or “HTTP/[email protected]” (the second being the format my Linux / FreeIPA configuration uses).

As a side note: On the Windows platform that is the focus of this question I do not have the rights to create / change SPNs or aliases to the same, or to try different Web Browsers. On my Linux development environment I do, which may provide additional input . . .


Solution

  • Quick Answer

    Two fixes were required:

    1) Internet Explorer (IE) builds the Service Principal Name (SPN) based on the URL. e.g. https://appserver.example.com/foo results in the SPN "HTTP/APPSERVER.example.com".

    Therefore proper Service Principal Names had to be setup in Active Directory in the above format as aliases to the User Principal Name (UPN) used by the Application Server.

    and

    2) The Token from Internet Explorer is a valid SPNEGO token, but is not accepted by GSS API on the Server.

    However with some simple string manipulation of the incoming token, a Kerbeos token can be extracted that GSS will accept and successfully authenticate.

    Longer Answer

    1) Service Principal Names ...

    After posting this issue here, we set up 2 SPNs HTTP/APPSERVER.example.com and HTTP/APPSERVER as aliases for the Server's UPN [email protected].

    The Server continues to run using the UPN [email protected]. (my initial assumption that the server had to run with one of the new SPNs was wrong.)

    My Java client is now able to use the new SPNs to acquire Kerberos Service Tickets and to create tokens that are successfully authenticated by my Server.

    However tokens from Internet Explorer continue to be be rejected.

    2) The Tokens from Internet Explorer vs my Java Client...

    The token from my Java Client starts like this:

    Negotiate YIIMdwYGKwYBBQUCoI....

    Base64 decoded, and represented as Hex bytes:

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

    of which 06 06 2B 06 01 05 05 02 is the SPNEGO OID 1.3.6.1.5.5.2 .

    The token from Internet Explorer starts like this:

    Negotiate oYIMPjCCDDqgAwoBAaKCDDEEggwtYIIMKQYJKoZIhvcSAQIC

    Base64 decoded, and represented as Hex bytes:

    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….

    This is a Spnego NegTokenTarg as it starts with "A1". However the java class sun.security.jgss.GSSHeader will reject any GSS token that does not start with "60".

    Examining the IE NegTokenTarg byte by byte shows that after the first 21 bytes I have a series of bytes very close to that of the token from my app:

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

    of which 06 09 2A 86 48 86 F7 12 01 02 02 is the Kerberos OID 1.2.840.113554.1.2.2

    If I extract this token by discarding the first 21 bytes of the original token (or provide gssContext.acceptSecContext(gssapiData, offset, gssapiData.length) with an offset of 21), then GSS API is able to read the new token, and extract the user principal and thus authenticate the request from Internet Explorer.

    The code example below uses string manipulation of the base64 endcoded authorization string to achieve the same:

    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.
    

    In conclusion I think we have either

    a) A Java GSS API weakness: GSS does not directly accept a SPNEGO token that is a NegTokenTarg.

    or

    b) The interplay between the Internet Explorer and my Server, resulting in IE sending a NegTokenTarg, which is not expected by GSS API.

    The IE - Server interplay is:

    1) Request from IE (without Negotiate)

    2) Reject from my Server, with Negotiate

    3) 2nd Request from IE, with Negotiate Header + Token that looks like NTLM,not Kerberos. --> This may be the route cause of the problem.

    4) Reject from my Server, with Negotiate + SPNEGO token

    5) 3rd Request from IE, with Negotiate Header + SPNEGO NegTokenTarg

    Background Info:

    My Application Server uses Java JAAS + GSS for the Kerberos / Spnego functionality. My custom client can use either Java JAAS + GSS, or Microsoft SSPI + Waffle.

    I found this Microsoft document very helpful to understand the format of a SPNEGO token.

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

    and this blog to understand how to “handle” negative decimal bytes. (The numbers -128 to -1 translate as 128 to 255).

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

    As the corporate standard browser of my target end-users is Internet Explorer, with no realistic option to use something “better”, while googling I came across the SPN handling code of Chromium and Firefox. Links are below. The Chromium code has extensive comments and links to Knowledge Base articles and SME Blogs.

    Chromiumn Code

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

    FireFox Code

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