winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

WebAccountManagerAuthenticator.cpp (13331B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include <AppInstallerErrors.h>
      5 #include <AppInstallerStrings.h>
      6 #include <AppInstallerLogging.h>
      7 #include <AppInstallerRuntime.h>
      8 #include "WebAccountManagerAuthenticator.h"
      9 
     10 using namespace std::string_view_literals;
     11 using namespace winrt::Windows::Foundation;
     12 using namespace winrt::Windows::Security::Authentication::Web::Core;
     13 using namespace winrt::Windows::Security::Credentials;
     14 
     15 namespace AppInstaller::Authentication
     16 {
     17     namespace
     18     {
     19         constexpr std::wstring_view s_MicrosoftEntraIdProviderId = L"https://login.microsoft.com"sv;
     20         constexpr std::wstring_view s_MicrosoftEntraIdAuthority = L"organizations"sv;
     21         constexpr std::wstring_view s_MicrosoftEntraIdClientId = L"7b8ea11a-7f45-4b3a-ab51-794d5863af15"sv;
     22         constexpr std::wstring_view s_MicrosoftEntraIdResourceHeader = L"resource"sv;
     23         constexpr std::wstring_view s_MicrosoftEntraIdLoginHintHeader = L"LoginHint"sv;
     24     }
     25 
     26     WebAccountManagerAuthenticator::WebAccountManagerAuthenticator(AuthenticationInfo info, AuthenticationArguments args) : m_authInfo(std::move(info)), m_authArgs(std::move(args))
     27     {
     28         // WebAccountManager manages accounts as user. When running as system, it can only retrieve domain joined device token.
     29         // This is very rare scenario for rest source to require a device token. And it needs approval to provision winget client registration.
     30         THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), Runtime::IsRunningAsSystem());
     31         THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !m_authInfo.ValidateIntegrity());
     32         THROW_HR_IF(E_UNEXPECTED, m_authArgs.Mode == AuthenticationMode::Unknown);
     33 
     34         if (IsMicrosoftEntraIdAuthenticationType())
     35         {
     36             m_webAccountProvider = WebAuthenticationCoreManager::FindAccountProviderAsync(s_MicrosoftEntraIdProviderId, s_MicrosoftEntraIdAuthority).get();
     37             THROW_HR_IF_MSG(E_UNEXPECTED, !m_webAccountProvider, "Authentication Provider not found for Microsoft Entra Id");
     38             AICLI_LOG(Core, Info, << "WebAccountManagerAuthenticator created for MicrosoftEntraId. Resource: " << m_authInfo.MicrosoftEntraIdInfo->Resource << ", Scope: " << m_authInfo.MicrosoftEntraIdInfo->Scope);
     39         }
     40         else if (m_authInfo.Type == AuthenticationType::None)
     41         {
     42             THROW_HR_MSG(E_UNEXPECTED, "WebAccountManagerAuthenticator initialized with authentication type none");
     43         }
     44         else
     45         {
     46             THROW_HR(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED);
     47         }
     48     }
     49 
     50     // WebAccountManager manages token and cache at OS level.
     51     // So for each authentication request, we call WebAccountManager api to retrieve token.
     52     // We do not need to implement own cache logic.
     53     AuthenticationResult WebAccountManagerAuthenticator::AuthenticateForToken()
     54     {
     55         std::lock_guard<std::mutex> lock{ m_authLock };
     56 
     57         AICLI_LOG(Core, Info, << "Started WebAccountManagerAuthenticator::AuthenticateForToken.");
     58 
     59         AuthenticationResult result;
     60 
     61         if (!m_authenticatedAccount)
     62         {
     63             // This is the first time invocation or previous authentication failed
     64 
     65             // Find the account to use if user provided account name and the account is signed in before. Best effort only.
     66             WebAccount webAccount = nullptr;
     67             if (!m_authArgs.AuthenticationAccount.empty())
     68             {
     69                 webAccount = FindWebAccount(m_authArgs.AuthenticationAccount);
     70             }
     71 
     72             if (m_authArgs.Mode == AuthenticationMode::Interactive)
     73             {
     74                 result = GetToken(webAccount, true);
     75             }
     76             else if (m_authArgs.Mode == AuthenticationMode::SilentPreferred)
     77             {
     78                 result = GetTokenSilent(webAccount);
     79                 if (FAILED(result.Status))
     80                 {
     81                     result = GetToken(webAccount);
     82                 }
     83             }
     84             else if (m_authArgs.Mode == AuthenticationMode::Silent)
     85             {
     86                 result = GetTokenSilent(webAccount);
     87             }
     88         }
     89         else
     90         {
     91             // Previous authentication successful. Just retrieve the token with the authenticated account.
     92             // In rare cases silent flow fails, use interactive flow.
     93             result = GetTokenSilent(m_authenticatedAccount);
     94             if (FAILED(result.Status) && m_authArgs.Mode != AuthenticationMode::Silent)
     95             {
     96                 result = GetToken(m_authenticatedAccount);
     97             }
     98         }
     99 
    100         AICLI_LOG(Core, Info, << "Finished WebAccountManagerAuthenticator::AuthenticateForToken. Result: " << result.Status);
    101 
    102         return result;
    103     }
    104 
    105     WebAccount WebAccountManagerAuthenticator::FindWebAccount(std::string_view accountName)
    106     {
    107         AICLI_LOG(Core, Info, << "FindWebAccount called. Desired Account: " << accountName);
    108 
    109         WebAccount result = nullptr;
    110 
    111         if (IsMicrosoftEntraIdAuthenticationType())
    112         {
    113             auto findAccountsResult = WebAuthenticationCoreManager::FindAllAccountsAsync(m_webAccountProvider, s_MicrosoftEntraIdClientId).get();
    114             if (findAccountsResult.Status() == FindAllWebAccountsStatus::Success)
    115             {
    116                 for (auto const& account : findAccountsResult.Accounts())
    117                 {
    118                     if (Utility::CaseInsensitiveEquals(accountName, Utility::ConvertToUTF8(account.UserName())))
    119                     {
    120                         result = account;
    121                         break;
    122                     }
    123                 }
    124             }
    125             else
    126             {
    127                 AICLI_LOG(Core, Warning, << "FindAllAccountsAsync failed. Status: " << findAccountsResult.Status());
    128                 auto providerError = findAccountsResult.ProviderError();
    129                 if (providerError)
    130                 {
    131                     AICLI_LOG(Core, Warning,
    132                         << "FindAllAccountsAsync Provider Error. ErrorCode: " << providerError.ErrorCode()
    133                         << ", Message: " << Utility::ConvertToUTF8(providerError.ErrorMessage()));
    134                 }
    135             }
    136         }
    137 
    138         AICLI_LOG(Core, Info, << "FindWebAccount result: " << ((result != nullptr) ? "found" : "not found"));
    139 
    140         return result;
    141     }
    142 
    143     WebTokenRequest WebAccountManagerAuthenticator::CreateTokenRequest(bool forceInteractive)
    144     {
    145         WebTokenRequest request = nullptr;
    146 
    147         if (IsMicrosoftEntraIdAuthenticationType())
    148         {
    149             request = WebTokenRequest
    150             {
    151                 m_webAccountProvider,
    152                 Utility::ConvertToUTF16(m_authInfo.MicrosoftEntraIdInfo->Scope),
    153                 s_MicrosoftEntraIdClientId,
    154                 forceInteractive ? WebTokenRequestPromptType::ForceAuthentication : WebTokenRequestPromptType::Default
    155             };
    156 
    157             request.Properties().Insert(s_MicrosoftEntraIdResourceHeader, Utility::ConvertToUTF16(m_authInfo.MicrosoftEntraIdInfo->Resource));
    158             if (!m_authArgs.AuthenticationAccount.empty())
    159             {
    160                 request.Properties().Insert(s_MicrosoftEntraIdLoginHintHeader, Utility::ConvertToUTF16(m_authArgs.AuthenticationAccount));
    161             }
    162         }
    163 
    164         return request;
    165     }
    166 
    167     AuthenticationResult WebAccountManagerAuthenticator::GetToken(WebAccount webAccount, bool forceInteractive)
    168     {
    169         AICLI_LOG(Core, Info, << "Started GetToken. ForceInteractive: " << forceInteractive);
    170 
    171         auto request = CreateTokenRequest(forceInteractive);
    172         if (!request)
    173         {
    174             AICLI_LOG(Core, Error, << "CreateTokenRequest returned empty request");
    175             return {};
    176         }
    177 
    178         IAsyncOperation<WebTokenRequestResult> requestOperation;
    179         constexpr winrt::guid iidAsyncRequestResult{ winrt::guid_of<IAsyncOperation<WebTokenRequestResult>>() };
    180         auto authManagerFactory = winrt::get_activation_factory<WebAuthenticationCoreManager>();
    181         winrt::com_ptr<IWebAuthenticationCoreManagerInterop> authManagerInterop{ authManagerFactory.as<IWebAuthenticationCoreManagerInterop>() };
    182 
    183         HRESULT requestOperationResult = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED;
    184         AuthenticationWindowBase parentWindow;
    185         if (webAccount)
    186         {
    187             requestOperationResult = authManagerInterop->RequestTokenWithWebAccountForWindowAsync(
    188                 parentWindow.GetHandle(),
    189                 request.as<::IInspectable>().get(),
    190                 webAccount.as<::IInspectable>().get(),
    191                 iidAsyncRequestResult,
    192                 reinterpret_cast<void**>(&requestOperation));
    193         }
    194         else
    195         {
    196             requestOperationResult = authManagerInterop->RequestTokenForWindowAsync(
    197                 parentWindow.GetHandle(),
    198                 request.as<::IInspectable>().get(),
    199                 iidAsyncRequestResult,
    200                 reinterpret_cast<void**>(&requestOperation));
    201         }
    202 
    203         if (FAILED(requestOperationResult))
    204         {
    205             AICLI_LOG(Core, Error, << "RequestTokenForWindowAsync failed. Result: " << requestOperationResult);
    206             return {};
    207         }
    208 
    209         return HandleGetTokenResult(requestOperation.get());
    210     }
    211 
    212     AuthenticationResult WebAccountManagerAuthenticator::GetTokenSilent(WebAccount webAccount)
    213     {
    214         AICLI_LOG(Core, Info, << "Started GetTokenSilent.");
    215 
    216         auto request = CreateTokenRequest(false);
    217         if (!request)
    218         {
    219             AICLI_LOG(Core, Error, << "CreateTokenRequest returned empty request");
    220             return {};
    221         }
    222 
    223         if (webAccount)
    224         {
    225             return HandleGetTokenResult(WebAuthenticationCoreManager::GetTokenSilentlyAsync(request, webAccount).get());
    226         }
    227         else
    228         {
    229             return HandleGetTokenResult(WebAuthenticationCoreManager::GetTokenSilentlyAsync(request).get());
    230         }
    231     }
    232 
    233     AuthenticationResult WebAccountManagerAuthenticator::HandleGetTokenResult(WebTokenRequestResult requestResult)
    234     {
    235         AuthenticationResult result;
    236 
    237         if (!requestResult)
    238         {
    239             AICLI_LOG(Core, Error, << "WebTokenRequestResult is null");
    240             return result;
    241         }
    242 
    243         if (requestResult.ResponseStatus() == WebTokenRequestStatus::Success)
    244         {
    245             auto responseData = requestResult.ResponseData().GetAt(0);
    246             auto authenticatedAccount = responseData.WebAccount();
    247 
    248             // Check token's corresponding account matches user input if applicable.
    249             if (m_authArgs.AuthenticationAccount.empty() || Utility::CaseInsensitiveEquals(m_authArgs.AuthenticationAccount, Utility::ConvertToUTF8(authenticatedAccount.UserName())))
    250             {
    251                 result.Status = S_OK;
    252                 result.Token = Utility::ConvertToUTF8(responseData.Token());
    253                 // Assign authenticated account for future token retrieval.
    254                 m_authenticatedAccount = authenticatedAccount;
    255                 AICLI_LOG(Core, Info, << "Authentication success");
    256             }
    257             else
    258             {
    259                 AICLI_LOG(Core, Error, << "Authentication success. But the authenticated account is not the desired one.");
    260                 result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT;
    261             }
    262         }
    263         else if (requestResult.ResponseStatus() == WebTokenRequestStatus::AccountSwitch)
    264         {
    265             AICLI_LOG(Core, Error, << "Authentication failed. The authenticated account is not the desired one.");
    266             result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT;
    267         }
    268         else if (requestResult.ResponseStatus() == WebTokenRequestStatus::ProviderError ||
    269             requestResult.ResponseStatus() == WebTokenRequestStatus::AccountProviderNotAvailable)
    270         {
    271             AICLI_LOG(Core, Error, << "Authentication failed. Provider failed.");
    272             auto responseError = requestResult.ResponseError();
    273             if (responseError)
    274             {
    275                 AICLI_LOG(Core, Error, << "Provider Error. Code: " << responseError.ErrorCode() << ", Message: " << Utility::ConvertToUTF8(responseError.ErrorMessage()));
    276             }
    277             result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED;
    278         }
    279         else if (requestResult.ResponseStatus() == WebTokenRequestStatus::UserCancel)
    280         {
    281             AICLI_LOG(Core, Error, << "Authentication failed. User cancelled.");
    282             result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_CANCELLED_BY_USER;
    283         }
    284         else if (requestResult.ResponseStatus() == WebTokenRequestStatus::UserInteractionRequired)
    285         {
    286             AICLI_LOG(Core, Error, << "Authentication failed. Interactive authentication required.");
    287             result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_INTERACTIVE_REQUIRED;
    288         }
    289 
    290         AICLI_LOG(Core, Info, << "HandleGetTokenResult Result: " << result.Status);
    291 
    292         return result;
    293     }
    294 
    295     bool WebAccountManagerAuthenticator::IsMicrosoftEntraIdAuthenticationType()
    296     {
    297         return m_authInfo.Type == AuthenticationType::MicrosoftEntraId || m_authInfo.Type == AuthenticationType::MicrosoftEntraIdForAzureBlobStorage;
    298     }
    299 }