How Windows Single Sign-On Works
Have you ever wondered how Windows does Single Sign-on? It's been a while since I did one of these threads, so lets look at some history.
— Steve Syfuhs (@SteveSyfuhs) November 18, 2020
Twitter warning: Like all good things this is mostly correct, with a few details fuzzier than others for reasons: a) details are hard on twitter; b) details are fudged for greater clarity; c) maybe I'm just dumb.
For SSO in Windows to make sense you need to take a look at how Windows logon first works: What happens when you type your password into Windows.
(And for completeness how it works for Azure AD joined machines: How Windows Azure AD sign in works)
(And of course how Kerberos works: A bit about Kerberos) Whew!
Tl;dr; you log into Windows with a credential and get a long lived ticket and that ticket is used in place of your password for lots of stuff.
But that just gets you to do the desktop. How do you get access to file shares, web sites, and who knows what on your network?
All of this is handled through a protocol named Negotiate, or SPNEGO (Simple and Protected Negotiation). It's a somewhat simple protocol on the wire, but the implementation is somewhat complicated. Here's how it works.
First, you try and access a resource, like a file share. You type in \\myserver\mysuperdupershare\ and Windows knows the \\ means "file share", so it opens up the SMB stack and says "hey SMB go connect to this thing...please". SMB does it's thing and connects.
The server on the far side says "whoaaa, this isn't an open share, papers please." The SMB client stack asks Windows for a ticket to the service, Windows obliges, and SMB hands that ticket to the remote service. The remote service verifies the ticket and allows SMB to continue.
Where does Negotiate fit into all this? Isn't this all Kerberos under the covers? Kinda, sorta.
First we have to understand that SPNEGO isn't an authentication protocol per-se. It's a protocol wrapper, meaning it takes one or more protocols and lets clients and servers negotiate which protocol they want to use (clever name, right?).
The protocol itself is pretty simple. It starts with a request:
I support: Kerberos, NTLM, NegoEx, digest, basic, etc.
Also, here's an optimistic Kerberos ticket: <ticket>
The server looks at the list and says "oh Kerberos, let's do that" and uses the supplied ticket.
But it's negotiable, so the server might respond with "sorry, Kerberos doesn't work, please gimme NTLM". The client redoes the request with NTLM. The server can ACK it (and include any sub-protocol response) or fail it, and we're done. That's it, that's the entire protocol.
That, in my mind, isn't the interesting thing here. The interesting thing is how this is implemented in Windows.
So how does Windows implement this? Through a thing called the Security Support Provider Interface or SSPI for short. SSPI has a fraternal twin called the Generic Security Services Application Programming Interface or GSSAPI. SSPI is Windows, GSSAPI is...everything else.
They're both compatible with one another. This is how Windows can communicate with Linux servers, and how Linux clients can communicate with Windows servers. They both support SPNEGO.
Anyway, SSPI and GSSAPI (SSPI from here on out) is a collection of functions exposed by the platform. There's only a handful of functions.
These functions operate operate in what's often called The Loop (SSPI loop or GSS loop). It starts with a call to AcquireCredentialsHandle. Here you tell it what credentials you want to use. It could be blank, meaning it uses the default creds (more later) or supplied creds.
The call to ACH gives you a handle, and you pass this handle off to InitializeSecurityContext. You tell ISC you want to do "negotiate" or "kerberos" "or NTLM" or whatever is implemented. ISC returns to you a blob of binary goo.
Our friendly SMB stack is doing this whole thing during its connection and now has this goo from ISC. SMB takes this goo and fires it off to the server. The server receives this goo and now starts it's side of the loop. The server calls AcquireCredentialsHandle and gets a handle.
The server then passes the handle to AcceptSecurityContext plus "negotiate" plus the goo from the client. ASC returns more goo and the SMB server takes this goo and returns it to the client.
The client takes this goo and hands it, plus the handle from before, back into InitializeSecurityContext. ISC gives you more goo and SMB must fire that back to the server. The server takes that goo and passes it to AcceptSecurityContext.
This whole process can go on a few times. Hence called the loop. Once both sides determine either side is properly authenticated the server creates an NT Token based on the user information in the ticket and the application can then impersonate that user locally.
That's nice and simple at the 10k foot view. But how does Windows handle this internally?
Way back in the 'how logon works' thread I glossed over the "logon session". You've typed your password and out pops an NT token. This NT token is kernel structure and is (sort of) a handle to your logon session. The logon session is a logical container stored in LSA.
Within the container is things like your group membership (SIDs) as well as your Kerberos ticket cache, plus additional housekeeping. Whenever you need to access this container to reference it through your NT token. Every process you start has reference to this NT token.
So when explorer.exe starts up it has your NT token. When explorer realizes it needs to get a ticket it askes SSPI for one. SSPI now knows about this NT token.
So now the SSPI loop starts. ACH is called with a blank credentials structure. The ACH/ISC/ASC functions are all just mostly just stubs though. Just enough internals to validate parameters and fire off those parameters to LSA.
So ACH is called, the parameters are validated, and a new message is created and fired off to LSA via RPC. LSA receives this message and creates a credential handle tied to the NT token, and therefore the logon session. The handle is returned to the calling application.
So this handle is just a pointer to a structure in your logon session in LSA. You call ISC. ISC validates parameters and fires it off to LSA. LSA sees that you passed "negotiate" and opens the SPNego security package.
Security packages are all about SSO. They provide the internal LSA implementation of an SSO protocol, and these implementations mirror SSPI. SPNego is one of these packages.
But remember SPNego isn't an authentication protocol. It's a wrapper. So all it knows is a bunch of other protocols like Kerberos and NTLM. What's it do? It iterates through each of those protocols packages.
First it asks Kerberos "hey kerbie can you get a ticket to cifs/myshare?" Kerberos checks the logon session cache for a TGT, finds it, and fires a TGS-REQ to the KDC. KDC returns a ticket or says 🤷♂️. If that fails it moves on to NTLM, otherwise it continues with that ticket.
SPNego takes the ticket as well as the list of packages it knows, creates the message and returns it to the calling application. ISC gets this from LSA, hands it to the caller, and the caller fires it off to the server.
The server in the meantime has done ACH on it's side. The server process is running as a logged on user. It has to. All processes have to run as a user. In some cases that user may just be the SYSTEM. In either case a logon session is present.
The server process receives the message and passes the goo to ASC. ASC validates parameters and fires it off the the server LSA. LSA sees it asked for "negotiate" so it finds the SPNego package and hands the goo to the nego package.
The nego package decodes the message and sees it was Kerberos. The package goes and finds the Kerberos package, hands the ticket to the Kerberos package, and tells it to process it.
The Kerberos package receives this goo, does some validation, and goes looking for the decryption key. The decryption key is the logged on session's password (OR the cred passed through ACH earlier).
Once Kerberos decrypts the message it looks for a special structure called the PAC -- the Privilege Attribute Certificate. Silly name, but it's the thing that contains all your group membership info.
The PAC contains a collection of structures such as your logon info (full name, logon server, groups, etc.), as well as other structures like your user or device claims, plus some signatures.
The logon info structure contains group membership SIDs plus metadata like when your password is going to expire, how many logons you've done on a DC, etc.
All of these structures get signed by the KDC using the service key (AKA the service account password), and then that signature is counter-signed using the krbtgt key.
The server receiving the Kerberos ticket validates the PAC signature because it has the key (machine password, service account password, etc.).
The server *may* decide to validate the PAC further by firing the PAC off to a DC via netlogon to ask if it's valid. However, this only occurs under a handful of instances, specifically when it's possible for the account receiving the ticket to be low privileged trying to do EOP.
Anyway, the Kerberos package now has this validated PAC and the group memberships. Kerberos runs all these groups through SID filtering.
You might have thought SID filtering just happens when you're crossing forests. It's more than that -- it's handled wherever a ticket is accepted. However, there are degrees of filtering, sort of a high/medium/low kind of thing. Here it's more like a sanity check.
Anyway, again, the Kerberos package has the filtered SIDs. The package asks LSA to create an NT token, this time an impersonation token. The impersonation token is special. It sorta has a logon session container, but its limited in what it can do. More on this in a second.
So now the Kerberos package has the NT token, and indicates to the nego package that it's done. Nego indicates to ASC, ASC indicates to the server, server indicates to the client, client informs ISC, and the loop is done. The client now communicates with the server as the user.
Back to the server. The server process has that impersonation token. A process can only have a single logon token, but it can have many impersonation tokens. When the server process wants to run as this user it needs to use the impersonation token. How?
Well, remember in Windows a process itself doesn't do anything. It's just a big container, and all processing actually happens on threads. One property of a thread is the thread identity -- an NT token.
When a process wants to run as a specific impersonated identity, it sets the thread identity to that specific impersonation token.
Now the server process is receiving requests from the client, such as "hey server, I want to access folder .\scratch\steve". The server is processing this request on the thread with the impersonated identity and calls for an ACL check to make sure the user can access the folder.
The ACL check function opens the current thread and gets the thread NT token. The ACL function examines the NT token group memberships (SIDs) and checks against what the folder requires.
This goes on for quite a while and eventually the client closes the connection. The server acknowledges this and begins cleaning up resources for the connection.
It locates the impersonation token and closes it. LSA sees this request to close and cleans up any resources it allocated. Now the server process can't impersonate the user anymore.
If the client wants to reconnect it has to start this dance all over again.
Riley as usual trying to follow along.