SQL Server, File and Printer Sharing, Exchange, and a number of other applications are able to authenticate the user based on their Windows identity.
How do they do this? In particular, how can i do this?
As a concrete example, complete the native Windows code inside the following method:
Boolean IsCurrentUserValidForDomain(String domainName)
{
//TODO: Ask Stackoverflow to fill in the code here
}
I can get us started off:
Boolean IsCurrentUserValidForDomain(String domainName)
{
//Get the security token associated with the thread
TOKEN userToken;
// Get the calling thread's access token.
if (!OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, true, out userToken)
{
if (GetLastError != ERROR_NO_TOKEN)
throw new Exception("Could not get current thread security token");
// Retry against process token since no thread token exists.
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, out userToken)
throw new Exception("Could not get current process security token");
}
//We now have the security token of the running user (userToken)
//From this, we can get the SID of the user
PSID sidUser = null;
DWORD cbBuf = 0;
Boolean bsuccess = GetTokenInformation(hToken, TokenUser, null, 0, ref cbBuf);
PTOKEN_USER ptiUser = null;
while ((!bSuccess) && (GetLastError() = ERROR_INSUFFICIENT_BUFFER))
{
ReallocMem(ref ptiUser, cbBuf);
bSuccess = GetTokenInformation(hToken, TokenUser, ptiUser, cbBuf, ref cbBuf);
}
sidUser = ptiUser.User.Sid;
//Now that we have the user's SID, we can get the SID of their domain
PSID sidDomain = null;
GetWindowsAccountDomainSid(sidUser, null, ref cbBuff);
ReallocMem(sidDomain, cbBuff);
GetWindowsAccountDomainSid(sidUser, sidDomain, ref cbBuff);
//We now have
//TOKEN userToken: security token of the running user
//PSID sidUser (S-1-5-21-2154378322-3929449213-1104335884-1006)
//PSID sidDomain (S-1-5-21-2154378322-3929449213-1104335884)
//TODO: ask stackoverflow if anything i've computed so far can help
//answer the question
//TODO: Ask Stackoverflow to fill in the code here
}
Note: everything from here down is "shows research effort". You can stop reading now. I only have it to document my own research effort (some of which can be very useful in other situations). I also want to preëmpt some of the more common, and insecure, approaches (which is a trap i fell into myself). If i can help anyone else avoid the same trap - all the better.
Background
When a user connects to SQL Server, they have an option of using Integrated Authentication:
Integrated security uses the current Windows identity established on the operating system thread to access the SQL Server database
and more from SQL Server:
When a user connects through a Windows user account, SQL Server validates the account name and password using the Windows principal token in the operating system. This means that the user identity is confirmed by Windows. SQL Server does not ask for the password, and does not perform the identity validation.
Result: I can login to SQL Server without having to type a username and password
And if i connect to a remote network share, my own user credentials are used to validate that i am a user on the remote server. I can browse to the remote machine and the connection implicitly used my logged in user account to validate access.
Result: I can connect to a network share without having to type a username and password.
How does SQL Server validate me as a user?
How does File and Printer Sharing validate me as a user?
Lets say i am writing my own database engine, and i want to support "Windows Authentication" how would i do it?
Lets say my SQL database engine is running on a non-domain joined PC, with a local user account. I can get various pieces of information about them
Local user
- GetUsername:
ginger
- GetUsernameEx(NameSamCompatible):
HYDROGEN\Ginger
(hydrogen is the name of the machine) - GetTokenInformation(TokenUser): UserSID =
S-1-5-21-2154378322-3929449213-1104335884-1006
- LookupAccountSid
- username:
Ginger
- domain:
HYDROGEN
- username:
- GetWindowsAccountDomainSid:
S-1-5-21-2154378322-3929449213-1104335884
Lets say my SQL database engine is running on a domain joined PC, with a domain user account (and to make it interesting, the user is from a different domain than the machine-joined domain). I can get various pieces of information about the user:
- GetUsername:
forest
- GetUsernameEx(NameSamCompatible):
CONTOSO\forest
(contoso is the legacy, SAM Compatible, NetBIOS name of the domain. The actual domain name is contoso.com) - GetTokenInformation(TokenUser): UserSID =
S-1-5-21-1708537768-854245398-2146844275-3110
- LookupAccountSid
- username:
forest
- domain:
CONTOSO
- username:
- GetWindowsAccountDomainSid:
S-1-5-21-1708537768-854245398-2146844275
Is there enough information in here to correctly implement Windows Authentication?
How does SQL Server do it? How does Explorer do it? How does Internet Explorer along with IIS do it?
Aren't there some buzzwords, like Ticket-Granting-Ticket i have to include?
Bad Ideas
I had some bad, insecure, ideas. I figured why not just take name returned from GetUsernameEx
CONTOSO\forest
and split it into two parts:
Username: forest
Domain: CONTOSO
That way i know that the user really is the user forest from the CONTOSO domain. I know it really is contoso\forest because Windows validated their credentials at login.
Except no. Because the user on their standalone, non-domain joined, notebook, can change the name of their workgroup from HYDROGEN
to CONTOSO
. Now when i read their username:
CONTOSO\forest
I will believe they are:
forest of the CONTOSO domain
when in reality they are:
forest of a standalone machine
Ok, so use the SID
Since i cannot trust the "domain name" returned by various Windows functions, then i could use the user's SID:
domain user contoso\forest:
S-1-5-21-1708537768-854245398-2146844275-3110
There's no way a user on a standalone PC can fake that, right? Right? :(
Yes, they can:
local user hydrogen\ginger:
S-1-5-21-1708537768-854245398-2146844275-3110
You see where i'm going with this? I'm trying to invent a way to perform authentication - and failing. Meanwhile Windows and SQL Server teams both already solved this problem twenty years ago. Dave Cutler designed this system in 1994, and knew exactly what i should be doing.
I just don't know what that something is.
User SIDs are not used for authentication
In researching this, i discovered some interesting concepts. Such as the SID of a domain is the machine SID of the first machine to become the domain's controller. I also discovered that users in that domain are a suffix of the domain SID:
Machine SIDs and Domain SIDs
| Machine SID for computer DEMOSYSTEM | S-1-5-21-3419697060-3810377854-678604692 | | DEMOSYSTEM\Administrator | S-1-5-21-3419697060-3810377854-678604692-500 | | DEMOSYSTEM\Guest | S-1-5-21-3419697060-3810377854-678604692-501 | | DEMOSYSTEM\CustomAccount1 | S-1-5-21-3419697060-3810377854-678604692-1000 | | DEMOSYSTEM\CustomAccount2 | S-1-5-21-3419697060-3810377854-678604692-1001 |
On a workgroup system, local accounts and groups are all there are. Authentication to a remote system using a local account requires a user name and password known to the remote system, and that SIDs are not used. The only way anything resembling single sign on happens with local accounts is that if the remote system has the same user name and password that the caller is using. SIDs are not transmitted and are not used for remote authentication.
This is an important point, and you have to realize that duplicated SIDs are perfectly valid. SIDs must be unique within the authority in which they are used. So while DEMOSYSTEM must have only one local account with the SID S-1-5-21-3419697060-3810377854-678604692-1000, it doesn’t matter if another computer uses the same SID to refer to a local account of its own.
This makes sense, and reinforces the idea that it is credentials that authorize a user, not their SID.
Which is why another of my ideas sucks
SQL Server stored Windows logins by their SID in syslogins
table:
sid name isntuser
---------------------------------------------------------- -------------- ---------
0x010500000000000515000000A837D66516C0EA32733EF67F260C0000 CONTOSO\forest 1
My SQL Database engine cannot read the SID of the current user and check if it exists in my syslogins
table, because there can be more than one user with the same sid connecting to my database engine over TCP port 1434.
Windows stores credentials somewhere
I was reading about "Pass the Hash" attacks against NTML and Kerberos. There was one interesting snippet:
Windows caches the hashed passwords in memory to implement Single Sign On or SSO, which is an essential feature of Windows enterprise environments.
I just have to figure out how to convince Windows to tell me if the user really is domain\BillG
.
Windows Authentication in Chrome and Windows Explorer
Chrome is able to transparently supply my Windows credentails to someone asking for them. If i were to request on a server, the server will deny me access (401) and indicate that i should use Negotiate
authentication:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Negotiate
My client then performs some magic, and re-issues the request, this time with my identity attached:
GET http://contoso.com/foo HTTP/1.1
Authorization: Negotiate YIIFzwYGKwYBBQUCoIIFwzCCBb+gMDAuBgkqhkiC9xIBAgIGCSqGSIb3EgECAgYKKwYBBAGCNwICHgYKKwYBBAGCNwICCqKCBYkEggWFYIIFgQYJKoZIhvcSAQICAQBuggVwMIIFbKADAgEFoQMCAQ6iBwMFACAAAACjggP4YYID9DCCA/CgAwIBBaEOGwxBVkFUT1BJQS5DT02iJTAjoAMCAQKhHDAaGwRIVFRQGxJ2YWRlci5hdmF0b3BpYS5jb22jggOwMIIDrKADAgEXoQMCATCiggOeBIIDmjMA0SnAUdqmbf8+UXZHsipRqPKt2yxqQaFia8hBF3TuQVDBgqGk8yL+CoDGnvkyGqpZK3UBsS/EuXP4Z+/0y49ZyDDQnDFqcJpF5ZY87t+u/kYQy+dr42GxEYQIjb096AQzDZio0dRWqbHleS5DlR7wCEaJ+a0CG6/vLEXL6tT20aj3avFibZc++5OKhynoxtyh10tJO3iwun2usJT+p1IfTD9yVDhfplMchLBgyp803+6IUwzm0zcwcqt7R1KnCv1i+baw3e/dhkIJz8cnoh1oNuivSXf4zOqlvp8FDlQMQEGqa9OA7LBmhg1rWDTOdyB4E9oZtVG8ipHyFYzDcyvIpWOMf9S68TTE78TgEhWjVq7g6BoH+O6IW14QIItxVk1GbSd2Ke9n9We0pbMjRxiZIMqyOvvFBgU5NlUUksdlG/yv0BTai7SILbVfNPsVwHeus//UfKQenX6YEnKUVi+XutY0kjLyp6l1L3Ce/ovkpDVmmYFebfdIT8Xbya9Zksa2nF8+7OL5S7I0tZaZUBL2Bzca9VJiGioRFvpgBXxKiChv71SukROreic+ylxHOfOWwXsEa0+ISHV6Uvhd44y3UA2VKtI3xoF8+3SZ184hIZ4fbahkfrBa1Zu5FqQ9M0rxAPgmsBZ2PwuMDWWLtraK7gJsAh+DxXGAaSTiPWaRhms59mfetBmzSnkzWBCr63G8rL71TiDgevoxhv0FP5s1JmWzWsnluJ95f9fphItuiDRI0C1358LMai9B1ZFWf9CRooeMAH4YUuL4SZ0r61/zQVnWFF1ngyt/ko/9UQ3mErLFeA/9Oq6BYfI/ExhVl9VVue0irM1vk09pIdUMS9MvQdW7YCg/C9LtOiJVpYw/aEVakn74l7TM71bIfjucDddDCBNuup41bWy5Nqkci8AHEMyoVyG9BxHmTm8NZ3FSujl+MeDAANKSt3a6P2k0C/W4Mley76ZoAGf6IYXf/9THQucvQGkasUkIN6PwIZIaxEdVt1BXiVXu1ADgt2/+0UB8rzYq+kt53R16rjev4Exvt7jpHIWUxjbDTxo2CvW0+Eh+mFyMj3CS2xQlhjrU2Q9ADQqA8wf8H88Dzp4PPWPxJnB4tC+Ecd9ZYlQwal00UX6aN47+dKPYDCp4piq6dvr2BhpzpsXxyR8QOZRKqAoXXLmb4Y1eGFWiUqH56J3Wju5h+cyzhMq+otpI4s77lfIecM41HccPrTKkggFZMIIBVaADAgEXooIBTASCAUigcKId1qR+UzSz8R00q+0o2M4+2dLnNW2vPU+uLeG9SqLJgJWsgBWUGtt6TRvPLF/GoHxP+sqST8fKJf0EHfycGfH/VJR6bnfpQYCWCgWRHjfdUpll51G/xKYqJYyy5xtNQvtKkzp+IB6CVKe1q3wopAY+uDsUk9XUvaIbUtHDEcWDATwi8BKGggVunw/idxKaZjaRmRko/Nsj5p38fiBk+OCN3yKDNSFCTDn+HUiCoCbDsv03zt2EO1eTJUPxXNhqJUjZMKYodgcsLMzNhSiyySH+kvgQZci3b8LGY1sCHMXopaL0Ysu4QgPD8UDD7dIBZ0ORmGf9srdZMgKjLIoEhXOmg+y5kqJpoPAwQaooHDizKQ8bmhFX2pOp7NjXoJ/wRvTB98seUNlDXDl5ySrt7P3Xf1Ybj7PpgMuqJykou2lKxirVhYYJ
Both IE, and Chrome, are able to turn around and do something can be used to prove i am who i am. How Windows Authentication is not documented, but there are some hints. From MSDN: HTTP-Based Cross-Platform Authentication via the Negotiate Protocol Part I - Network Infrastructure:
- When the logged-on user requests a resource from the web server, it sends the initial HTTP GET verb.
- The web server, running the SPNEGO Token Handler code, requires authentication and issues a 401 Access Denied, WWW-Authenticate: Negotiate response.
- The client calls
AcquireCredentialsHandle()
andInitializeSecurityContext()
with the SPN to build the Security Context that requests the session ticket from the TGS/KDC. - The TGS/KDC supplies the client with the necessary Kerberos Ticket (assuming the client is authorized) wrapped in a SPNEGO Token.
- The client re-sends the HTTP GET request + the Negotiate SPNEGO Token in an Authorization: Negotiate base64(token) header.
- The web server's SPNEGO Token Handler code accepts and processes the token through GSS API, authenticates the user and responds with the requested URL.
So there is some code that the client can use to generate a proof that they are who they say they are. Which means there is some way for me, on the client, to generate proof that domain user is the domain user that they say they are.
I, of course, don't need to wrap up the ticket from Kerberos in a encrypted, base-64'd, SPNEGO blob. I just need the correct API calls, in the right order, to know that i am who i say i am.