Search code examples
wcfkerberosspnego

Supporting SSO for a REST API under Windows without using SPNEGO


We have a Windows based desktop application which connects to a WCF REST service. This service supports Basic auth (over HTTPS), Bearer token, and SPNEGO. SPNEGO is what the desktop application uses so that the users do not have enter in credentials when running the application.

We are looking for ways around using SPNEGO. For instance, is there a way that the Windows client can get some kind of signed token from the system or domain controller using the logged in credentials, and then pass that token to custom endpoint on our service which could use that token to lookup the user and then send back an application bearer token?

A couple of the reasons we want to depreciate SPNEGO:

  1. WinHTTP can't be forced to use Kerberos, so every call is actually several calls
  2. SPNEGO cannot run behind AWS Application Load Balancer, it has to be an NLB
  3. We would rather have more visibility into authentication failures. Right now, when SPNEGO fails, there is a lot of event log digging to track down the issue
  4. WCF support for SPNEGO is kind of "magic" in that you can't step through and see what exactly is going on.

Solution

  • We are looking for ways around using SPNEGO. For instance, is there a way that the Windows client can get some kind of signed token from the system or domain controller using the logged in credentials, and then pass that token to custom endpoint on our service which could use that token to lookup the user and then send back an application bearer token?

    The only kind of "signed token" that Active Directory natively supports is Kerberos. It might not necessarily be used as HTTP SPNEGO – e.g. you could directly call SSPI InitSecurityContext(), then ship the Kerberos ticket as part of HTTP request header or even payload – but it's unavoidably going to begin with Kerberos.

    Other than that, your plan is reasonable; it's not unheard of to have SPNEGO only enabled for the single "exchange for a bearer token" endpoint, which could be provided by a completely separate service if needed.

    In fact, I believe Windows already has such a service for web-apps – that's ADFS, which allows you to exchange an HTTP SPNEGO request for a signed SAML2.0 assertion. Although SAML2.0 is maybe overly complicated for your use case, but the fundamental idea is there. (Yes, I suppose ADFS is part of Active Directory, so the answer to your question would be "yes, it's called ADFS". Though I've also set up SimpleSAMLphp for my private use to do the same, and several other SAML2 and OAuth2 IdPs such as Keycloak can do this.)

    You can use HTTP SPNEGO behind load balancers, as long as all backend services share the same service key – e.g. with a Linux-based web server or anything else that uses keytabs instead of Windows 'native' SSPI, you could issue the service keytab once and distribute it across all of your web servers. (With Windows clients it works very much like TLS, where the service principal name only depends on the URL.

    And if you begin to use SSPI or GSS-API directly, you're in full control of the service principal that Kerberos will be told to expect (again, much like with TLS libraries). So I'd say you could avoid most of your problems by directly going to SSPI (or the .NET API that wraps it) and having it issue the Kerberos tokens, then sending them in the 'WWW-Authenticate: Negotiate' header; that's really all it takes to implement the client side of Kerberos authentication for HTTP.

    import spnego
    ctx = spnego.client(hostname="foo.example.com",
                        service="HTTP",
                        protocol="negotiate")
    token = b64encode(ctx.step())
    resp = requests.get("https://foo.example.com/auth",
                        headers={"WWW-Authenticate": f"Negotiate {token}"})