Search code examples
winapiazure-active-directorycredential-providers

Get AzureAD email address from username using Windows API?


I am using

WTSEnumerateSessionsEx(WTS_CURRENT_SERVER_HANDLE, &level, 0, &pSessionInfo, &count);

to get session information. It will tell me all logged in users. If I use it on Windows 11 with another user logged in,

WTS User: DESKTOP-VVVVVVV\joew, session id: 1, state: 0, session name: Console
WTS User: AzureAD\JoeWillcoxso_garbage, session id: 17, state: 4

From the state, joew is the active user and JoeWillcoxso_garbage. It's not exactly that, but I'm protecting it...you get the idea.

The session info will give me the domain and username. When I login to Windows for an AzureAD with a non-standard credential provider, I use my AzureAD email address and the CP is smart enough to use AzureAD as the domain name. But, when the machine was joined to AzureAD, it created a local account for the email address as JoeWillcoxso_garbage. ( I write "created a local account", but in PowerShell if I do get-localuser *, that user does not show up in the list.)

I can use LsaEnumerateLogonSessions() and LsaGetLogonSessionData() to get session data from the local security authority. I can match things between the two calls (LsaGetLogonSessionData and WTSEnumerateSessionsEx) using session id and username. However, there is no way to get an email address. I can get a PSID in the session data, not sure if there is a way to turn that into an email address. On my local machine, I'm not attached to an AD. Just running as WORKGROUP.

For an AzureAD user, calling NetUserGetInfo() always fails for the AAD user even when running elevated. I haven't yet tried as NT_AUTHORITY account...

I'm looking for an API or maybe some way to query WMI to turn the AzureAD\JoeWillcoxso_garbage user name into [email protected] email address. Or, maybe need a web API to hit login.windows.net. There are places in the registry where I can find the email address, but I don't think they are guaranteed to always be there.

Since a user may be logged in already (and on Windows 10/11 with fast switching), sometimes an unlock is not CPUS_UNLOCK_WORKSTATION but CPUS_LOGON. With fast switching, if I lock, come back 10 minutes later, and do an unlock, it's not an unlock but a logon (CPUS_LOGON) although it acts just like an unlock used to do.


For instance, at HKEY_USERS\THE_SID_OF_THE USER\Software\Microsoft\Office\Teams, HomeUserUpn has the email address. I can get the SID of the logged on users...so I could possible get the email to match up... but, that's assuming that key is there. I want something more bullet proof.


Solution

  • I wanted something to use from Win32, hence the tag. What I did find is that from a process running as NT_System account, I could iterate all the LSA sessions.

    I could use LSAEnumerateLogonSessions() to get a list of all the sessions.

    Once I had the sessions, for each session I could call LsaGetLogonSessionData() which would return a PSECURITY_LOGON_SESSION_DATA which importantly returns session ID, logon domain, logon name, and SID of the user. Looking at the logon domain, I could know if it was AzureAD because it would literally be AzureAD. Knowing it was AzureAD, I could then use the SID and do a registry lookup at key (not entire function):

    CString s;
    s.Format(L"SOFTWARE\\Microsoft\\IdentityStore\\Cache\\%s\\IdentityCache\\%s", lpwzSid, lpwzSid);
    
    ATL::CRegKey regKey;
    if (ERROR_SUCCESS == regKey.Open(HKEY_LOCAL_MACHINE, s, KEY_READ))
    {
        WCHAR szUserName[MAX_PATH] = { 0 };
        ULONG ulChars = MAX_PATH;
        if (ERROR_SUCCESS == regKey.QueryStringValue(L"UserName", szUserName, &ulChars) && ulChars > 0)
        {
            return CString(szUserName);
        }
    }
    

    That almost works except for one little nit... sometimes the AzureAD user has logged out but still has an open LSA session. I have no idea how this happens, but I have observed it on Windows 11. IDK if this is for all AzureAD users, but I have noticed it can be the case for the account used to join the AzureAD domain. So, in that case, it is necessary to backcheck and use WTSEnumerateSessionsEx() and verify that there really is an open session.