Rework error handling and state flow in the Azure connection (#5356)

This commit fixes a number of problems and code quality/health issues
with the AzureConnection.

This is a general tidying-up of the azure connection. It improves error
logging (like: it actually emits error logs...) and retry logic and the
state machine and it audits the exit points of the state machine for
exceptions and removes the HRESULT returns (so they either succeed and
transition to a new state or throw an exception or are going down
anyway).

There's also a change in here that changes how we display tenants. It
adds the "default domain" to the name, so that instead of seeing this:

Conhost (aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa)
Default Directory (bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb)

you see this

Conhost (conhost.onmicrosoft.com)
Default Directory (dustinhowett.onmicrosoft.com)

Changes:

* rework tenant/tenant storage and fix display names

  Switch to the 2020 tenant API.

  Instead of passing around four loose variables, create a Tenant class
  and use that for packing/unpacking into/out of json (and the windows
  credential store, where we "cleverly" used json for the tenant info
  there too).
  
  When displaying a tenant, use its display name if there is one, the
  unknown resource string if there isn't, and the default domain if
  there is one and the ID if there isn't.
  
  Fixes #5325.

* use {fmt} for formatting request bodies
* remove dead strings
* rework/rename Request/HeaderHelper to
  Send(Authenticated)ReqReturningJson
* rewrite polling to use std::chrono
* remove HR returns from state machine
* rename state handlers from _XHelper to _RunXState
* cleanup namespaces, prefix user input with >, remove namespaces
* Rework error handling
  - _RequestHelper no longer eats exceptions.
  - Delete the "no internet" error message.
  - Wrap exceptions coming out of Azure API in a well-known type.
  - Catch by type.
  - Extract error codes for known failures (keep polling, invalid
    grant).
  - When we get an Invalid Grant, dispose of the cached refresh token
    and force the user to log in again.
  - Catch all printable exceptions and print them.
  - Remove the NoConnect state completely -- just bail out when an
    exception hits the toplevel of the output thread.
  - Move 3x logic into _RefreshTokens and pop exceptions out of it.
  - Begin abstracting into AzureClient

Fixes #5325 (by addressing its chief complaint).
Fixes #4803 (by triggering auth flow again if the token expires).
Improves diagnosability for #4575.
This commit is contained in:
Dustin L. Howett (MSFT) 2020-04-16 17:32:52 -07:00 committed by GitHub
parent fe3f528827
commit c186c7d683
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 329 additions and 266 deletions

View file

@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "cpprest/json.h"
namespace Microsoft::Terminal::Azure
{
class AzureException : public std::runtime_error
{
std::wstring _code;
public:
static bool IsErrorPayload(const web::json::value& errorObject)
{
return errorObject.has_string_field(L"error");
}
AzureException(const web::json::value& errorObject) :
runtime_error(til::u16u8(errorObject.at(L"error_description").as_string())), // surface the human-readable description as .what()
_code(errorObject.at(L"error").as_string())
{
}
std::wstring_view GetCode() const noexcept
{
return _code;
}
};
namespace ErrorCodes
{
static constexpr std::wstring_view AuthorizationPending{ L"authorization_pending" };
static constexpr std::wstring_view InvalidGrant{ L"invalid_grant" };
}
struct Tenant
{
std::wstring ID;
std::optional<std::wstring> DisplayName;
std::optional<std::wstring> DefaultDomain;
};
}
#define THROW_IF_AZURE_ERROR(payload) \
do \
{ \
if (AzureException::IsErrorPayload((payload))) \
{ \
throw AzureException((payload)); \
} \
} while (0)

View file

@ -16,32 +16,22 @@
#include "AzureConnection.g.cpp"
#include "winrt/Windows.System.UserProfile.h"
#include "../../types/inc/Utils.hpp"
using namespace ::Microsoft::Console;
using namespace ::Microsoft::Terminal::Azure;
using namespace utility;
using namespace web;
using namespace web::json;
using namespace web::http;
using namespace web::http::client;
using namespace web::websockets::client;
using namespace concurrency::streams;
using namespace winrt::Windows::Security::Credentials;
static constexpr int CurrentCredentialVersion = 1;
static constexpr auto PasswordVaultResourceName = L"Terminal";
static constexpr auto HttpUserAgent = L"Terminal/0.0";
#define FAILOUT_IF_OPTIONAL_EMPTY(optional) \
do \
{ \
if (!((optional).has_value())) \
{ \
return E_FAIL; \
} \
} while (0, 0)
static constexpr int USER_INPUT_COLOR = 93; // yellow - the color of something the user can type
static constexpr int USER_INFO_COLOR = 97; // white - the color of clarifying information
@ -60,9 +50,12 @@ static inline std::wstring _formatResWithColoredUserInputOptions(const std::wstr
return fmt::format(std::wstring_view{ GetLibraryResourceString(resourceKey) }, (_colorize(USER_INPUT_COLOR, GetLibraryResourceString(args)))...);
}
static inline std::wstring _formatTenantLine(int tenantNumber, const std::wstring_view tenantName, const std::wstring_view tenantID)
static inline std::wstring _formatTenant(int tenantNumber, const Tenant& tenant)
{
return fmt::format(std::wstring_view{ RS_(L"AzureIthTenant") }, _colorize(USER_INPUT_COLOR, std::to_wstring(tenantNumber)), _colorize(USER_INFO_COLOR, tenantName), tenantID);
return fmt::format(std::wstring_view{ RS_(L"AzureIthTenant") },
_colorize(USER_INPUT_COLOR, std::to_wstring(tenantNumber)),
_colorize(USER_INFO_COLOR, tenant.DisplayName.value_or(std::wstring{ RS_(L"AzureUnknownTenantName") })),
tenant.DefaultDomain.value_or(tenant.ID)); // use the domain name if possible, ID if not.
}
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
@ -91,6 +84,27 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
_TerminalOutputHandlers(str + L"\r\n");
}
// Method description:
// - helper that prints exception information to the output stream.
// Arguments:
// - [IMPLICIT] the current exception context
void AzureConnection::_WriteCaughtExceptionRecord()
{
try
{
throw;
}
catch (const std::exception& runtimeException)
{
// This also catches the AzureException, which has a .what()
_TerminalOutputHandlers(_colorize(91, til::u8u16(std::string{ runtimeException.what() })));
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
}
// Method description:
// - ascribes to the ITerminalConnection interface
// - creates the output thread (where we will do the authentication and actually connect to Azure)
@ -98,12 +112,20 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
// Create our own output handling thread
// Each connection needs to make sure to drain the output from its backing host.
_hOutputThread.reset(CreateThread(nullptr,
0,
StaticOutputThreadProc,
this,
0,
nullptr));
_hOutputThread.reset(CreateThread(
nullptr,
0,
[](LPVOID lpParameter) noexcept {
AzureConnection* const pInstance = static_cast<AzureConnection*>(lpParameter);
if (pInstance)
{
return pInstance->_OutputThread();
}
return gsl::narrow_cast<DWORD>(E_INVALIDARG);
},
this,
0,
nullptr));
THROW_LAST_ERROR_IF_NULL(_hOutputThread);
@ -121,7 +143,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
_currentInputMode = mode;
_TerminalOutputHandlers(L"\x1b[92m"); // Make prompted user input green
_TerminalOutputHandlers(L"> \x1b[92m"); // Make prompted user input green
_inputEvent.wait(inputLock, [this, mode]() {
return _currentInputMode != mode || _isStateAtOrBeyond(ConnectionState::Closing);
@ -200,6 +222,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Arguments:
// - the new rows/cols values
void AzureConnection::Resize(uint32_t rows, uint32_t columns)
try
{
if (!_isConnected())
{
@ -213,14 +236,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Initialize the request
http_request terminalRequest(L"POST");
terminalRequest.set_request_uri(L"terminals/" + _terminalID + L"/size?cols=" + std::to_wstring(columns) + L"&rows=" + std::to_wstring(rows) + L"&version=2019-01-01");
_HeaderHelper(terminalRequest);
terminalRequest.set_body(json::value(L""));
terminalRequest.set_request_uri(fmt::format(L"terminals/{}/size?cols={}&rows={}&version=2019-01-01", _terminalID, columns, rows));
terminalRequest.set_body(json::value::null());
// Send the request
const auto response = _RequestHelper(terminalClient, terminalRequest);
// Send the request (don't care about the response)
(void)_SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest);
}
}
CATCH_LOG();
// Method description:
// - ascribes to the ITerminalConnection interface
@ -256,27 +279,45 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - tenant - the unparsed tenant
// Return value:
// - a tuple containing the ID and display name of the tenant.
static std::tuple<utility::string_t, utility::string_t> _crackTenant(const json::value& tenant)
static Tenant _crackTenant(const json::value& jsonTenant)
{
auto tenantId{ tenant.at(L"tenantId").as_string() };
std::wstring displayName{ tenant.has_string_field(L"displayName") ? tenant.at(L"displayName").as_string() : static_cast<std::wstring>(RS_(L"AzureUnknownTenantName")) };
return { tenantId, displayName };
Tenant tenant{};
if (jsonTenant.has_string_field(L"tenantID"))
{
// for compatibility with version 1 credentials
tenant.ID = jsonTenant.at(L"tenantID").as_string();
}
else
{
// This one comes in off the wire
tenant.ID = jsonTenant.at(L"tenantId").as_string();
}
if (jsonTenant.has_string_field(L"displayName"))
{
tenant.DisplayName = jsonTenant.at(L"displayName").as_string();
}
if (jsonTenant.has_string_field(L"defaultDomain"))
{
tenant.DefaultDomain = jsonTenant.at(L"defaultDomain").as_string();
}
return tenant;
}
// Method description:
// - this method bridges the thread to the Azure connection instance
// Arguments:
// - lpParameter: the Azure connection parameter
// Return value:
// - the exit code of the thread
DWORD WINAPI AzureConnection::StaticOutputThreadProc(LPVOID lpParameter)
static void _packTenant(json::value& jsonTenant, const Tenant& tenant)
{
AzureConnection* const pInstance = static_cast<AzureConnection*>(lpParameter);
if (pInstance)
jsonTenant[L"tenantId"] = json::value::string(tenant.ID);
if (tenant.DisplayName.has_value())
{
return pInstance->_OutputThread();
jsonTenant[L"displayName"] = json::value::string(*tenant.DisplayName);
}
if (tenant.DefaultDomain.has_value())
{
jsonTenant[L"defaultDomain"] = json::value::string(*tenant.DefaultDomain);
}
return gsl::narrow_cast<DWORD>(E_INVALIDARG);
}
// Method description:
@ -302,32 +343,32 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// or allow them to login with a different account or allow them to remove the saved settings
case AzureState::AccessStored:
{
RETURN_IF_FAILED(_AccessHelper());
_RunAccessState();
break;
}
// User has no saved connection settings or has opted to login with a different account
// Azure authentication happens here
case AzureState::DeviceFlow:
{
RETURN_IF_FAILED(_DeviceFlowHelper());
_RunDeviceFlowState();
break;
}
// User has multiple tenants in their Azure account, they need to choose which one to connect to
case AzureState::TenantChoice:
{
RETURN_IF_FAILED(_TenantChoiceHelper());
_RunTenantChoiceState();
break;
}
// Ask the user if they want to save these connection settings for future logins
case AzureState::StoreTokens:
{
RETURN_IF_FAILED(_StoreHelper());
_RunStoreState();
break;
}
// Connect to Azure, we only get here once we have everything we need (tenantID, accessToken, refreshToken)
case AzureState::TermConnecting:
{
RETURN_IF_FAILED(_ConnectHelper());
_RunConnectState();
break;
}
// We are connected, continuously read from the websocket until its closed
@ -366,28 +407,20 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
return S_OK;
}
case AzureState::NoConnect:
{
_WriteStringWithNewline(RS_(L"AzureInternetOrServerIssue"));
_transitionToState(ConnectionState::Failed);
return E_FAIL;
}
}
}
catch (...)
{
_state = AzureState::NoConnect;
_WriteCaughtExceptionRecord();
_transitionToState(ConnectionState::Failed);
return E_FAIL;
}
}
}
// Method description:
// - helper function to get the stored credentials (if any) and let the user choose what to do next
// Return value:
// - S_FALSE if there are no stored credentials
// - S_OK if the user opts to login with a stored set of credentials or login with a different account
// - E_FAIL if the user closes the tab
HRESULT AzureConnection::_AccessHelper()
void AzureConnection::_RunAccessState()
{
bool oldVersionEncountered = false;
auto vault = PasswordVault();
@ -401,10 +434,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
// No credentials are stored, so start the device flow
_state = AzureState::DeviceFlow;
return S_FALSE;
return;
}
int numTenants{ 0 };
_tenantList.clear();
for (const auto& entry : credList)
{
auto nameJson = json::value::parse(entry.UserName().c_str());
@ -422,7 +456,9 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
continue;
}
_WriteStringWithNewline(_formatTenantLine(numTenants, nameJson.at(L"displayName").as_string(), nameJson.at(L"tenantID").as_string()));
auto newTenant{ _tenantList.emplace_back(_crackTenant(nameJson)) };
_WriteStringWithNewline(_formatTenant(numTenants, newTenant));
numTenants++;
}
@ -434,7 +470,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
}
// No valid up-to-date credentials were found, so start the device flow
_state = AzureState::DeviceFlow;
return S_FALSE;
return;
}
_WriteStringWithNewline(RS_(L"AzureEnterTenant"));
@ -445,7 +481,10 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
do
{
auto maybeTenantSelection = _ReadUserInput(InputMode::Line);
FAILOUT_IF_OPTIONAL_EMPTY(maybeTenantSelection);
if (!maybeTenantSelection.has_value())
{
return;
}
const auto& tenantSelection = maybeTenantSelection.value();
if (tenantSelection == RS_(L"AzureUserEntry_RemoveStored"))
@ -453,13 +492,13 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// User wants to remove the stored settings
_RemoveCredentials();
_state = AzureState::DeviceFlow;
return S_OK;
return;
}
else if (tenantSelection == RS_(L"AzureUserEntry_NewLogin"))
{
// User wants to login with a different account
_state = AzureState::DeviceFlow;
return S_OK;
return;
}
else
{
@ -486,10 +525,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// User wants to login with one of the saved connection settings
auto desiredCredential = credList.GetAt(selectedTenant);
desiredCredential.RetrievePassword();
auto nameJson = json::value::parse(desiredCredential.UserName().c_str());
auto passWordJson = json::value::parse(desiredCredential.Password().c_str());
_displayName = nameJson.at(L"displayName").as_string();
_tenantID = nameJson.at(L"tenantID").as_string();
_currentTenant = til::at(_tenantList, selectedTenant); // we already unpacked the name info, so we should just use it
_accessToken = passWordJson.at(L"accessToken").as_string();
_refreshToken = passWordJson.at(L"refreshToken").as_string();
_expiry = std::stoi(passWordJson.at(L"expiry").as_string());
@ -500,25 +537,33 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Check if the token is close to expiring and refresh if so
if (timeNow + _expireLimit > _expiry)
{
const auto refreshResponse = _RefreshTokens();
_accessToken = refreshResponse.at(L"access_token").as_string();
_refreshToken = refreshResponse.at(L"refresh_token").as_string();
_expiry = std::stoi(refreshResponse.at(L"expires_on").as_string());
// Store the updated tokens under the same username
_StoreCredential();
try
{
_RefreshTokens();
// Store the updated tokens under the same username
_StoreCredential();
}
catch (const AzureException& e)
{
if (e.GetCode() == ErrorCodes::InvalidGrant)
{
_WriteCaughtExceptionRecord();
vault.Remove(desiredCredential);
// Delete this credential and try again.
_state = AzureState::AccessStored;
return;
}
throw; // rethrow. we couldn't handle this error.
}
}
// We have everything we need, so go ahead and connect
_state = AzureState::TermConnecting;
return S_OK;
}
// Method description:
// - helper function to start the device code flow (required for authentication to Azure)
// Return value:
// - E_FAIL if the user closes the tab, does not authenticate in time or has no tenants in their Azure account
// - S_OK otherwise
HRESULT AzureConnection::_DeviceFlowHelper()
void AzureConnection::_RunDeviceFlowState()
{
// Initiate device code flow
const auto deviceCodeResponse = _GetDeviceCode();
@ -532,124 +577,93 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
const auto expiresIn = std::stoi(deviceCodeResponse.at(L"expires_in").as_string());
// Wait for user authentication and obtain the access/refresh tokens
json::value authenticatedResponse;
try
{
authenticatedResponse = _WaitForUser(devCode, pollInterval, expiresIn);
}
catch (...)
{
_WriteStringWithNewline(RS_(L"AzureExitStr"));
return E_FAIL;
}
json::value authenticatedResponse = _WaitForUser(devCode, pollInterval, expiresIn);
_accessToken = authenticatedResponse.at(L"access_token").as_string();
_refreshToken = authenticatedResponse.at(L"refresh_token").as_string();
// Get the tenants and the required tenant id
const auto tenantsResponse = _GetTenants();
_tenantList = tenantsResponse.at(L"value");
const auto tenantListAsArray = _tenantList.as_array();
if (tenantListAsArray.size() == 0)
_PopulateTenantList();
if (_tenantList.size() == 0)
{
_WriteStringWithNewline(RS_(L"AzureNoTenants"));
_transitionToState(ConnectionState::Failed);
return E_FAIL;
return;
}
else if (_tenantList.size() == 1)
{
const auto& chosenTenant = tenantListAsArray.at(0);
std::tie(_tenantID, _displayName) = _crackTenant(chosenTenant);
_currentTenant = til::at(_tenantList, 0);
// We have to refresh now that we have the tenantID
const auto refreshResponse = _RefreshTokens();
_accessToken = refreshResponse.at(L"access_token").as_string();
_refreshToken = refreshResponse.at(L"refresh_token").as_string();
_expiry = std::stoi(refreshResponse.at(L"expires_on").as_string());
_RefreshTokens();
_state = AzureState::StoreTokens;
}
else
{
_state = AzureState::TenantChoice;
}
return S_OK;
}
// Method description:
// - helper function to list the user's tenants and let them decide which tenant they wish to connect to
// Return value:
// - E_FAIL if the user closes the tab
// - S_OK otherwise
HRESULT AzureConnection::_TenantChoiceHelper()
void AzureConnection::_RunTenantChoiceState()
{
try
auto numTenants = gsl::narrow<int>(_tenantList.size());
for (int i = 0; i < numTenants; i++)
{
const auto tenantListAsArray = _tenantList.as_array();
auto numTenants = gsl::narrow<int>(tenantListAsArray.size());
for (int i = 0; i < numTenants; i++)
{
const auto& tenant = tenantListAsArray.at(i);
const auto [tenantId, tenantDisplayName] = _crackTenant(tenant);
_WriteStringWithNewline(_formatTenantLine(i, tenantDisplayName, tenantId));
}
_WriteStringWithNewline(RS_(L"AzureEnterTenant"));
int selectedTenant{ -1 };
do
{
auto maybeTenantSelection = _ReadUserInput(InputMode::Line);
FAILOUT_IF_OPTIONAL_EMPTY(maybeTenantSelection);
const auto& tenantSelection = maybeTenantSelection.value();
try
{
selectedTenant = std::stoi(tenantSelection);
if (selectedTenant < 0 || selectedTenant >= numTenants)
{
_WriteStringWithNewline(RS_(L"AzureNumOutOfBoundsError"));
continue;
}
break;
}
catch (...)
{
// suppress exceptions in conversion
}
// if we got here, we didn't break out of the loop early and need to go 'round again
_WriteStringWithNewline(RS_(L"AzureNonNumberError"));
} while (true);
const auto& chosenTenant = tenantListAsArray.at(selectedTenant);
std::tie(_tenantID, _displayName) = _crackTenant(chosenTenant);
// We have to refresh now that we have the tenantID
const auto refreshResponse = _RefreshTokens();
_accessToken = refreshResponse.at(L"access_token").as_string();
_refreshToken = refreshResponse.at(L"refresh_token").as_string();
_expiry = std::stoi(refreshResponse.at(L"expires_on").as_string());
_state = AzureState::StoreTokens;
return S_OK;
_WriteStringWithNewline(_formatTenant(i, til::at(_tenantList, i)));
}
CATCH_RETURN();
_WriteStringWithNewline(RS_(L"AzureEnterTenant"));
int selectedTenant{ -1 };
do
{
auto maybeTenantSelection = _ReadUserInput(InputMode::Line);
if (!maybeTenantSelection.has_value())
{
return;
}
const auto& tenantSelection = maybeTenantSelection.value();
try
{
selectedTenant = std::stoi(tenantSelection);
if (selectedTenant < 0 || selectedTenant >= numTenants)
{
_WriteStringWithNewline(RS_(L"AzureNumOutOfBoundsError"));
continue;
}
break;
}
catch (...)
{
// suppress exceptions in conversion
}
// if we got here, we didn't break out of the loop early and need to go 'round again
_WriteStringWithNewline(RS_(L"AzureNonNumberError"));
} while (true);
_currentTenant = til::at(_tenantList, selectedTenant);
// We have to refresh now that we have the tenantID
_RefreshTokens();
_state = AzureState::StoreTokens;
}
// Method description:
// - helper function to ask the user if they wish to store their credentials
// Return value:
// - E_FAIL if the user closes the tab
// - S_OK otherwise
HRESULT AzureConnection::_StoreHelper()
void AzureConnection::_RunStoreState()
{
_WriteStringWithNewline(_formatResWithColoredUserInputOptions(USES_RESOURCE(L"AzureStorePrompt"), USES_RESOURCE(L"AzureUserEntry_Yes"), USES_RESOURCE(L"AzureUserEntry_No")));
// Wait for user input
do
{
auto maybeStoreCredentials = _ReadUserInput(InputMode::Line);
FAILOUT_IF_OPTIONAL_EMPTY(maybeStoreCredentials);
if (!maybeStoreCredentials.has_value())
{
return;
}
const auto& storeCredentials = maybeStoreCredentials.value();
if (storeCredentials == RS_(L"AzureUserEntry_Yes"))
@ -668,15 +682,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
} while (true);
_state = AzureState::TermConnecting;
return S_OK;
}
// Method description:
// - helper function to connect the user to the Azure cloud shell
// Return value:
// - E_FAIL if the user has not set up their cloud shell yet
// - S_OK after successful connection
HRESULT AzureConnection::_ConnectHelper()
void AzureConnection::_RunConnectState()
{
// Get user's cloud shell settings
const auto settingsResponse = _GetCloudShellUserSettings();
@ -684,7 +694,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
_WriteStringWithNewline(RS_(L"AzureNoCloudAccount"));
_transitionToState(ConnectionState::Failed);
return E_FAIL;
return;
}
// Request for a cloud shell
@ -710,36 +720,46 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
WriteInput(static_cast<winrt::hstring>(queuedUserInput)); // send the user's queued up input back through
}
return S_OK;
}
// Method description:
// - helper function to send requests and extract responses as json values
// - helper function to send requests with default headers and extract responses as json values
// Arguments:
// - a http_client
// - a http_request for the client to send
// Return value:
// - the response from the server as a json value
json::value AzureConnection::_RequestHelper(http_client theClient, http_request theRequest)
json::value AzureConnection::_SendRequestReturningJson(http_client& theClient, http_request theRequest)
{
auto& headers{ theRequest.headers() };
headers.add(L"User-Agent", HttpUserAgent);
headers.add(L"Accept", L"application/json");
json::value jsonResult;
try
{
const auto responseTask = theClient.request(theRequest);
responseTask.wait();
const auto response = responseTask.get();
const auto responseJsonTask = response.extract_json();
responseJsonTask.wait();
jsonResult = responseJsonTask.get();
}
catch (...)
{
_WriteStringWithNewline(RS_(L"AzureInternetOrServerIssue"));
}
const auto responseTask = theClient.request(theRequest);
responseTask.wait();
const auto response = responseTask.get();
const auto responseJsonTask = response.extract_json();
responseJsonTask.wait();
jsonResult = responseJsonTask.get();
THROW_IF_AZURE_ERROR(jsonResult);
return jsonResult;
}
// Method description:
// - helper function to send _authenticated_ requests with json bodies whose responses are expected
// to be json. builds on _SendRequestReturningJson.
// Arguments:
// - the http_request
json::value AzureConnection::_SendAuthenticatedRequestReturningJson(http_client& theClient, http_request theRequest)
{
auto& headers{ theRequest.headers() };
headers.add(L"Authorization", L"Bearer " + _accessToken);
return _SendRequestReturningJson(theClient, std::move(theRequest));
}
// Method description:
// - helper function to start the device code flow
// Return value:
@ -752,11 +772,11 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Initialize the request
http_request commonRequest(L"POST");
commonRequest.set_request_uri(L"common/oauth2/devicecode");
const auto body = L"client_id=" + AzureClientID + L"&resource=" + _wantedResource;
const auto body{ fmt::format(L"client_id={}&resource={}", AzureClientID, _wantedResource) };
commonRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded");
// Send the request and receive the response as a json value
return _RequestHelper(loginClient, commonRequest);
return _SendRequestReturningJson(loginClient, commonRequest);
}
// Method description:
@ -774,76 +794,88 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
http_client pollingClient(_loginUri);
// Continuously send a poll request until the user authenticates
const auto body = hstring() + L"grant_type=device_code&resource=" + _wantedResource + L"&client_id=" + AzureClientID + L"&code=" + deviceCode;
const auto body{ fmt::format(L"grant_type=device_code&resource={}&client_id={}&code={}", _wantedResource, AzureClientID, deviceCode) };
const auto requestUri = L"common/oauth2/token";
json::value responseJson;
for (int count = 0; count < expiresIn / pollInterval; count++)
// use a steady clock here so it's not impacted by local time discontinuities
const auto tokenExpiry{ std::chrono::steady_clock::now() + std::chrono::seconds(expiresIn) };
while (std::chrono::steady_clock::now() < tokenExpiry)
{
std::this_thread::sleep_for(std::chrono::seconds(pollInterval));
// User might close the tab while we wait for them to authenticate, this case handles that
if (_isStateAtOrBeyond(ConnectionState::Closing))
{
throw "Tab closed.";
// We're going down, there's no valid user for us to return
break;
}
http_request pollRequest(L"POST");
pollRequest.set_request_uri(requestUri);
pollRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded");
responseJson = _RequestHelper(pollingClient, pollRequest);
if (responseJson.has_field(L"error"))
{
Sleep(pollInterval * 1000); // Sleep takes arguments in milliseconds
continue; // Still waiting for authentication
}
else
try
{
auto response{ _SendRequestReturningJson(pollingClient, pollRequest) };
_WriteStringWithNewline(RS_(L"AzureSuccessfullyAuthenticated"));
break; // Authentication is done, break from loop
// Got a valid response: we're done
return response;
}
catch (const AzureException& e)
{
if (e.GetCode() == ErrorCodes::AuthorizationPending)
{
// Handle the "auth pending" exception by retrying.
continue;
}
throw;
} // uncaught exceptions bubble up to the caller
}
if (responseJson.has_field(L"error"))
{
throw "Time out.";
}
return responseJson;
return json::value::null();
}
// Method description:
// - helper function to acquire the user's Azure tenants
// Return value:
// - the response which contains a list of the user's Azure tenants
json::value AzureConnection::_GetTenants()
void AzureConnection::_PopulateTenantList()
{
// Initialize the client
http_client tenantClient(_resourceUri);
// Initialize the request
http_request tenantRequest(L"GET");
tenantRequest.set_request_uri(L"tenants?api-version=2018-01-01");
_HeaderHelper(tenantRequest);
tenantRequest.set_request_uri(L"tenants?api-version=2020-01-01");
// Send the request and return the response as a json value
return _RequestHelper(tenantClient, tenantRequest);
auto tenantResponse{ _SendAuthenticatedRequestReturningJson(tenantClient, tenantRequest) };
auto tenantList{ tenantResponse.at(L"value").as_array() };
_tenantList.clear();
std::transform(tenantList.begin(), tenantList.end(), std::back_inserter(_tenantList), _crackTenant);
}
// Method description:
// - helper function to refresh the access/refresh tokens
// Return value:
// - the response with the new tokens
json::value AzureConnection::_RefreshTokens()
void AzureConnection::_RefreshTokens()
{
// Initialize the client
http_client refreshClient(_loginUri);
// Initialize the request
http_request refreshRequest(L"POST");
refreshRequest.set_request_uri(_tenantID + L"/oauth2/token");
const auto body = L"client_id=" + AzureClientID + L"&resource=" + _wantedResource + L"&grant_type=refresh_token" + L"&refresh_token=" + _refreshToken;
refreshRequest.set_request_uri(_currentTenant->ID + L"/oauth2/token");
const auto body{ fmt::format(L"client_id={}&resource={}&grant_type=refresh_token&refresh_token={}", AzureClientID, _wantedResource, _refreshToken) };
refreshRequest.set_body(body.c_str(), L"application/x-www-form-urlencoded");
refreshRequest.headers().add(L"User-Agent", HttpUserAgent);
// Send the request and return the response as a json value
return _RequestHelper(refreshClient, refreshRequest);
auto refreshResponse{ _SendRequestReturningJson(refreshClient, refreshRequest) };
_accessToken = refreshResponse.at(L"access_token").as_string();
_refreshToken = refreshResponse.at(L"refresh_token").as_string();
_expiry = std::stoi(refreshResponse.at(L"expires_on").as_string());
}
// Method description:
@ -858,9 +890,8 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Initialize request
http_request settingsRequest(L"GET");
settingsRequest.set_request_uri(L"providers/Microsoft.Portal/userSettings/cloudconsole?api-version=2018-10-01");
_HeaderHelper(settingsRequest);
return _RequestHelper(settingsClient, settingsRequest);
return _SendAuthenticatedRequestReturningJson(settingsClient, settingsRequest);
}
// Method description:
@ -875,13 +906,12 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Initialize request
http_request shellRequest(L"PUT");
shellRequest.set_request_uri(L"providers/Microsoft.Portal/consoles/default?api-version=2018-10-01");
_HeaderHelper(shellRequest);
// { "properties": { "osType": "linux" } }
auto body = json::value::object({ { U("properties"), json::value::object({ { U("osType"), json::value::string(U("linux")) } }) } });
shellRequest.set_body(body);
// Send the request and get the response as a json value
const auto cloudShell = _RequestHelper(cloudShellClient, shellRequest);
const auto cloudShell = _SendAuthenticatedRequestReturningJson(cloudShellClient, shellRequest);
// Return the uri
return cloudShell.at(L"properties").at(L"uri").as_string() + L"/";
@ -898,44 +928,33 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// Initialize the request
http_request terminalRequest(L"POST");
terminalRequest.set_request_uri(L"terminals?cols=" + std::to_wstring(_initialCols) + L"&rows=" + std::to_wstring(_initialRows) + L"&version=2019-01-01&shell=" + shellType);
_HeaderHelper(terminalRequest);
terminalRequest.set_request_uri(fmt::format(L"terminals?cols={}&rows={}&version=2019-01-01&shell={}", _initialCols, _initialRows, shellType));
// LOAD-BEARING. the API returns "'content-type' should be 'application/json' or 'multipart/form-data'"
terminalRequest.set_body(json::value::null());
// Send the request and get the response as a json value
const auto terminalResponse = _RequestHelper(terminalClient, terminalRequest);
const auto terminalResponse = _SendAuthenticatedRequestReturningJson(terminalClient, terminalRequest);
_terminalID = terminalResponse.at(L"id").as_string();
// Return the uri
return terminalResponse.at(L"socketUri").as_string();
}
// Method description:
// - helper function to set the headers of a http_request
// Arguments:
// - the http_request
void AzureConnection::_HeaderHelper(http_request theRequest)
{
theRequest.headers().add(L"Accept", L"application/json");
theRequest.headers().add(L"Content-Type", L"application/json");
theRequest.headers().add(L"Authorization", L"Bearer " + _accessToken);
theRequest.headers().add(L"User-Agent", HttpUserAgent);
}
// Method description:
// - helper function to store the credentials
// - we store the display name, tenant ID, access/refresh tokens, and token expiry
void AzureConnection::_StoreCredential()
{
auto vault = PasswordVault();
json::value userName;
userName[U("ver")] = CurrentCredentialVersion;
userName[U("displayName")] = json::value::string(_displayName);
userName[U("tenantID")] = json::value::string(_tenantID);
_packTenant(userName, *_currentTenant);
json::value passWord;
passWord[U("accessToken")] = json::value::string(_accessToken);
passWord[U("refreshToken")] = json::value::string(_refreshToken);
passWord[U("expiry")] = json::value::string(std::to_wstring(_expiry));
auto newCredential = PasswordCredential(PasswordVaultResourceName, userName.serialize(), passWord.serialize());
PasswordVault vault;
PasswordCredential newCredential{ PasswordVaultResourceName, userName.serialize(), passWord.serialize() };
vault.Add(newCredential);
}
@ -943,7 +962,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
// - helper function to remove all stored credentials
void AzureConnection::_RemoveCredentials()
{
auto vault = PasswordVault();
PasswordVault vault;
winrt::Windows::Foundation::Collections::IVectorView<PasswordCredential> credList;
// FindAllByResource throws an exception if there are no credentials stored under the given resource so we wrap it in a try-catch block
try

View file

@ -13,6 +13,7 @@
#include "../cascadia/inc/cppwinrt_utils.h"
#include "ConnectionStateHolder.h"
#include "AzureClient.h"
namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
{
@ -40,44 +41,43 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation
StoreTokens,
TermConnecting,
TermConnected,
NoConnect
};
AzureState _state{ AzureState::AccessStored };
wil::unique_handle _hOutputThread;
static DWORD WINAPI StaticOutputThreadProc(LPVOID lpParameter);
DWORD _OutputThread();
HRESULT _AccessHelper();
HRESULT _DeviceFlowHelper();
HRESULT _TenantChoiceHelper();
HRESULT _StoreHelper();
HRESULT _ConnectHelper();
void _RunAccessState();
void _RunDeviceFlowState();
void _RunTenantChoiceState();
void _RunStoreState();
void _RunConnectState();
const utility::string_t _loginUri{ U("https://login.microsoftonline.com/") };
const utility::string_t _resourceUri{ U("https://management.azure.com/") };
const utility::string_t _wantedResource{ U("https://management.core.windows.net/") };
const int _expireLimit{ 2700 };
web::json::value _tenantList;
utility::string_t _displayName;
utility::string_t _tenantID;
utility::string_t _accessToken;
utility::string_t _refreshToken;
int _expiry{ 0 };
utility::string_t _cloudShellUri;
utility::string_t _terminalID;
std::vector<::Microsoft::Terminal::Azure::Tenant> _tenantList;
std::optional<::Microsoft::Terminal::Azure::Tenant> _currentTenant;
void _WriteStringWithNewline(const std::wstring_view str);
web::json::value _RequestHelper(web::http::client::http_client theClient, web::http::http_request theRequest);
void _WriteCaughtExceptionRecord();
web::json::value _SendRequestReturningJson(web::http::client::http_client& theClient, web::http::http_request theRequest);
web::json::value _SendAuthenticatedRequestReturningJson(web::http::client::http_client& theClient, web::http::http_request theRequest);
web::json::value _GetDeviceCode();
web::json::value _WaitForUser(utility::string_t deviceCode, int pollInterval, int expiresIn);
web::json::value _GetTenants();
web::json::value _RefreshTokens();
void _PopulateTenantList();
void _RefreshTokens();
web::json::value _GetCloudShellUserSettings();
utility::string_t _GetCloudShell();
utility::string_t _GetTerminal(utility::string_t shellType);
void _HeaderHelper(web::http::http_request theRequest);
void _StoreCredential();
void _RemoveCredentials();

View file

@ -123,15 +123,15 @@
<data name="AzureEnterTenant" xml:space="preserve">
<value>Please enter the desired tenant number.</value>
</data>
<data name="AzureNewLogin" xml:space="default">
<data name="AzureNewLogin" xml:space="preserve">
<value>Enter {0} to login with a new account</value>
<comment>{0} will be replaced with the resource from AzureUserEntry_NewLogin; it is intended to be a single-character shorthand for "new account"</comment>
</data>
<data name="AzureRemoveStored" xml:space="default">
<data name="AzureRemoveStored" xml:space="preserve">
<value>Enter {0} to remove the above saved connection settings.</value>
<comment>{0} will be replaced with the resource from AzureUserEntry_RemoveStored; it is intended to be a single-character shorthand for "remove stored"</comment>
</data>
<data name="AzureInvalidAccessInput" xml:space="default">
<data name="AzureInvalidAccessInput" xml:space="preserve">
<value>Please enter a valid number to access the stored connection settings, {0} to log in with a new account, or {1} to remove the saved connection settings.</value>
<comment>{0} will be replaced with the resource from AzureUserEntry_NewLogin, and {1} will be replaced with AzureUserEntry_RemoveStored. This is an error message, used after AzureNewLogin/AzureRemoveStored if the user enters an invalid value.</comment>
</data>
@ -148,11 +148,11 @@
<value>You have not set up your cloud shell account yet. Please go to https://shell.azure.com to set it up.</value>
<comment>{Locked="https://shell.azure.com"} This URL should not be localized. Everything else should.</comment>
</data>
<data name="AzureStorePrompt" xml:space="default">
<data name="AzureStorePrompt" xml:space="preserve">
<value>Do you want to save these connection settings for future logins? [{0}/{1}]</value>
<comment>{0} and {1} will be replaced with AzureUserEntry_Yes and AzureUserEntry_No. They are single-character shorthands for "yes" and "no" in this language.</comment>
</data>
<data name="AzureInvalidStoreInput" xml:space="default">
<data name="AzureInvalidStoreInput" xml:space="preserve">
<value>Please enter {0} or {1}</value>
<comment>{0} and {1} will be replaced with AzureUserEntry_Yes and AzureUserEntry_No. This resource will be used as an error response after AzureStorePrompt.</comment>
</data>
@ -174,22 +174,13 @@
<data name="AzureTokensRemoved" xml:space="preserve">
<value>Saved connection settings removed.</value>
</data>
<data name="AzureExitStr" xml:space="preserve">
<value>Exit.</value>
</data>
<data name="AzureAuthString" xml:space="preserve">
<value>Authenticated.</value>
</data>
<data name="AzureInternetOrServerIssue" xml:space="preserve">
<value>Could not connect to Azure. You may not have internet or the server might be down.</value>
</data>
<data name="AzureOldCredentialsFlushedMessage" xml:space="preserve">
<value>Authentication parameters changed. You'll need to log in again.</value>
</data>
<data name="AzureUnknownTenantName" xml:space="preserve">
<value>&lt;unknown tenant name&gt;</value>
</data>
<data name="AzureIthTenant" xml:space="default">
<data name="AzureIthTenant" xml:space="preserve">
<value>Tenant {0}: {1} ({2})</value>
<comment>{0} is the tenant's number, which the user will enter to connect to the tenant. {1} is the tenant's display name, which will be meaningful for the user. {2} is the tenant's internal ID number.</comment>
</data>