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 }