winget-cli

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

commit 3bcd69d14e348732a63026e56e26c816f67f3f65
parent d3eee5c78038a8601f1f9429d32d6323c0709712
Author: yao-msft <50888816+yao-msft@users.noreply.github.com>
Date:   Thu,  1 Feb 2024 13:40:49 -0800

Support Microsoft Entra Id authentication for rest source (#4123)

## Change

This pr adds Microsoft Entra Id support for rest source. To achieve
this, rest source /information contract is updated with below. The
/information endpoint should never use authentication as it is the first
handshake between winget client and rest source.

```
// "Authentication": {
//     "AuthenticationType": "microsoftEntraId",
//     "MicrosoftEntraIdAuthenticationInfo" : {
//         "Resource": "Resource",
//         "Scope" : "test"
//     }
// }
```
Rest source interface will read this info and acquire authentication
using WebAccountManager OS api during all following source interaction
(/packageManifests, /manifestSearch)

CLI arguments updated to specify authentication behavior

COM interface updated to specify authentication behavior from caller and
provide authentication info of package catalog to caller

## Validation

- Due to lack of e2e rest source tests and complex in getting real
Microsoft Entra Id token. E2e validation is manually performed. I tested
cli (packaged and unpackaged), InProcCom and OutOfProcCom . Both
interactive and silent flow works correctly. I setup a test rest source
with Microsoft Entra Id protection (based on tenants) and it's working
as expected
- Added a bunch of unit tests: AuthenticationInfo parsing, RestClient
and RestInterface creation, and RestInterface GetManifest and Search
operation with override Microsoft Entra Id token

###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4123)
Diffstat:
M.github/actions/spelling/allow.txt | 1+
M.github/actions/spelling/expect.txt | 4++++
Mdoc/windows/package-manager/winget/returnCodes.md | 11+++++++++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 4++--
Msrc/AppInstallerCLICore/Argument.cpp | 12+++++++++++-
Msrc/AppInstallerCLICore/Command.cpp | 12+++++++++++-
Msrc/AppInstallerCLICore/Commands/DownloadCommand.cpp | 3+++
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 8+++++---
Msrc/AppInstallerCLICore/Commands/ListCommand.cpp | 2++
Msrc/AppInstallerCLICore/Commands/PinCommand.cpp | 6++++++
Msrc/AppInstallerCLICore/Commands/SearchCommand.cpp | 2++
Msrc/AppInstallerCLICore/Commands/ShowCommand.cpp | 2++
Msrc/AppInstallerCLICore/Commands/UninstallCommand.cpp | 2++
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 6++++--
Msrc/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionArgs.h | 3+++
Msrc/AppInstallerCLICore/ExecutionReporter.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionReporter.h | 1+
Msrc/AppInstallerCLICore/Resources.h | 4++++
Msrc/AppInstallerCLICore/Workflows/SourceFlow.cpp | 6++++++
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 42+++++++++++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 4++++
Msrc/AppInstallerCLIPackage/Package.appxmanifest | 4+++-
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Msrc/AppInstallerCLITests/CustomHeader.cpp | 51++++++++++++---------------------------------------
Msrc/AppInstallerCLITests/RestClient.cpp | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Asrc/AppInstallerCLITests/RestInterface_1_7.cpp | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestHooks.h | 22++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestRestRequestHandler.cpp | 28++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestRestRequestHandler.h | 3+++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 4++++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 15+++++++++++++++
Asrc/AppInstallerCommonCore/Authentication/Authentication.cpp | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.cpp | 287+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.h | 31+++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/Public/winget/Authentication.h | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/pch.h | 3+++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 4++++
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 15+++++++++++++++
Msrc/AppInstallerRepositoryCore/ISource.h | 5+++++
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySource.h | 11+++++++++--
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 8++++++++
Msrc/AppInstallerRepositoryCore/Rest/RestClient.cpp | 63++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/AppInstallerRepositoryCore/Rest/RestClient.h | 8+++++---
Msrc/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp | 15+++++++++++----
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h | 5++++-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp | 13+++++++++----
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_1/Interface.h | 3+--
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_1/RestInterface_1_1.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_4/Interface.h | 2+-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_4/RestInterface_1_4.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_5/Interface.h | 2+-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_5/RestInterface_1_5.cpp | 2+-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_6/Interface.h | 2+-
Msrc/AppInstallerRepositoryCore/Rest/Schema/1_6/RestInterface_1_6.cpp | 2+-
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_7/Interface.h | 27+++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/1_7/RestInterface_1_7.cpp | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.cpp | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.h | 21+++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h | 1+
Msrc/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp | 34++++++++++++++++++++++++++++------
Msrc/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h | 10++++++----
Msrc/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h | 2++
Msrc/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp | 3+++
Msrc/AppInstallerSharedLib/Errors.cpp | 8+++++++-
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 7+++++++
Msrc/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest | 4++++
Msrc/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs | 12++++++++++++
Msrc/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs | 2++
Asrc/Microsoft.Management.Deployment/AuthenticationArguments.cpp | 33+++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Deployment/AuthenticationArguments.h | 35+++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Deployment/AuthenticationInfo.cpp | 31+++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Deployment/AuthenticationInfo.h | 26++++++++++++++++++++++++++
Msrc/Microsoft.Management.Deployment/ComClsids.cpp | 5+++++
Msrc/Microsoft.Management.Deployment/Converters.cpp | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Deployment/Converters.h | 4++++
Msrc/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj | 6++++++
Msrc/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters | 6++++++
Asrc/Microsoft.Management.Deployment/MicrosoftEntraIdAuthenticationInfo.cpp | 22++++++++++++++++++++++
Asrc/Microsoft.Management.Deployment/MicrosoftEntraIdAuthenticationInfo.h | 25+++++++++++++++++++++++++
Msrc/Microsoft.Management.Deployment/PackageCatalogReference.cpp | 40++++++++++++++++++++++++++++++++++++++--
Msrc/Microsoft.Management.Deployment/PackageCatalogReference.h | 8+++++++-
Msrc/Microsoft.Management.Deployment/PackageManager.idl | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/Microsoft.Management.Deployment/Public/ComClsids.h | 3+++
Msrc/Microsoft.Management.Deployment/pch.h | 2++
Msrc/WindowsPackageManager/main.cpp | 1+
88 files changed, 2210 insertions(+), 128 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -78,6 +78,7 @@ emoji ENDDIALOG ensureandinsert ensurepathexists +entra ENU EOAC errorlevel diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -247,6 +247,7 @@ mdmp MDs megamorf meme +microsoftentraid midl minexample minidump @@ -394,6 +395,7 @@ SHGDN SHOWNORMAL Sideload SIGNATUREHASH +silentpreferred similarissues similaritytolerance Skipx @@ -461,6 +463,7 @@ UParse upgradable upgradecode URLZONE +USEDEFAULT userfilesetting userprofile UWP @@ -491,6 +494,7 @@ winreg winrtact winstring withstarts +WNDCLASS WNDCLASSEX WPARAM wpr diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md @@ -121,6 +121,17 @@ ms.localizationpriority: medium | 0x8A15006B | -1978335125 | APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES | Failed to download package dependencies. | | 0x8A15006C | -1978335124 | APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED | Failed to download package. Download for offline installation is prohibited. | | 0x8A15006D | -1978335123 | APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE | A required service is busy or unavailable. Try again later. | +| 0x8A15006E | -1978335122 | APPINSTALLER_CLI_ERROR_RESUME_ID_NOT_FOUND | The guid provided does not correspond to a valid resume state. | +| 0x8A15006F | -1978335121 | APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH | The current client version did not match the client version of the saved state. | +| 0x8A150070 | -1978335120 | APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE | The resume state data is invalid. | +| 0x8A150071 | -1978335119 | APPINSTALLER_CLI_ERROR_CANNOT_OPEN_CHECKPOINT_INDEX | Unable to open the checkpoint database. | +| 0x8A150072 | -1978335118 | APPINSTALLER_CLI_ERROR_RESUME_LIMIT_EXCEEDED | Exceeded max resume limit. | +| 0x8A150073 | -1978335117 | APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO | Invalid authentication info. | +| 0x8A150074 | -1978335116 | APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED | Authentication method not supported. | +| 0x8A150075 | -1978335115 | APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED | Authentication failed. | +| 0x8A150076 | -1978335114 | APPINSTALLER_CLI_ERROR_AUTHENTICATION_INTERACTIVE_REQUIRED | Authentication failed. Interactive authentication required. | +| 0x8A150077 | -1978335113 | APPINSTALLER_CLI_ERROR_AUTHENTICATION_CANCELLED_BY_USER | Authentication failed. User cancelled. | +| 0x8A150078 | -1978335112 | APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT | Authentication failed. Authenticated account is not the desired account. | ## Install errors. diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -436,9 +436,9 @@ <ClCompile Include="ConfigurationCommon.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClInclude Include="Commands\DownloadCommand.cpp"> + <ClCompile Include="Commands\DownloadCommand.cpp"> <Filter>Commands</Filter> - </ClInclude> + </ClCompile> <ClCompile Include="Commands\ResumeCommand.cpp"> <Filter>Commands</Filter> </ClCompile> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -229,13 +229,19 @@ namespace AppInstaller::CLI case Execution::Args::Type::DependencySource: return { type, "dependency-source"_liv, ArgTypeCategory::ExtendedSource }; case Execution::Args::Type::CustomHeader: - return { type, "header"_liv, ArgTypeCategory::QuerySource }; + return { type, "header"_liv, ArgTypeCategory::ExtendedSource }; case Execution::Args::Type::AcceptSourceAgreements: return { type, "accept-source-agreements"_liv, ArgTypeCategory::ExtendedSource }; case Execution::Args::Type::ToolVersion: return { type, "version"_liv, 'v' }; + // Authentication arguments + case Execution::Args::Type::AuthenticationMode: + return { type, "authentication-mode"_liv }; + case Execution::Args::Type::AuthenticationAccount: + return { type, "authentication-account"_liv }; + // Used for demonstration purposes case Execution::Args::Type::ExperimentalArg: return { type, "arg"_liv }; @@ -341,6 +347,10 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::HeaderArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::AcceptSourceAgreements: return Argument{ type, Resource::String::AcceptSourceAgreementsArgumentDescription, ArgumentType::Flag }; + case Args::Type::AuthenticationMode: + return Argument{ type, Resource::String::AuthenticationModeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; + case Args::Type::AuthenticationAccount: + return Argument{ type, Resource::String::AuthenticationAccountArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }; case Args::Type::ExperimentalArg: return Argument{ type, Resource::String::ExperimentalArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::ExperimentalArg }; case Args::Type::Rename: diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -7,6 +7,7 @@ #include <AppInstallerRuntime.h> #include <winget/Locale.h> #include <winget/Reboot.h> +#include <winget/Authentication.h> using namespace std::string_view_literals; using namespace AppInstaller::Utility::literals; @@ -716,7 +717,7 @@ namespace AppInstaller::CLI { if (Manifest::ConvertToScopeEnum(execArgs.GetArg(Execution::Args::Type::InstallScope)) == Manifest::ScopeEnum::Unknown) { - auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{ "user"_lis, "machine"_lis}); + auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{ "user"_lis, "machine"_lis }); throw CommandException(Resource::String::InvalidArgumentValueError(ArgumentCommon::ForType(Execution::Args::Type::InstallScope).Name, validOptions)); } } @@ -730,6 +731,15 @@ namespace AppInstaller::CLI } } + if (execArgs.Contains(Execution::Args::Type::AuthenticationMode)) + { + if (Authentication::ConvertToAuthenticationMode(execArgs.GetArg(Execution::Args::Type::AuthenticationMode)) == Authentication::AuthenticationMode::Unknown) + { + auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{ "interactive"_lis, "silentPreferred"_lis, "silent"_lis }); + throw CommandException(Resource::String::InvalidArgumentValueError(ArgumentCommon::ForType(Execution::Args::Type::AuthenticationMode).Name, validOptions)); + } + } + Argument::ValidateExclusiveArguments(execArgs); ValidateArgumentsInternal(execArgs); diff --git a/src/AppInstallerCLICore/Commands/DownloadCommand.cpp b/src/AppInstallerCLICore/Commands/DownloadCommand.cpp @@ -33,6 +33,9 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Locale), Argument::ForType(Args::Type::HashOverride), Argument::ForType(Args::Type::SkipDependencies), + Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), Argument::ForType(Execution::Args::Type::AcceptPackageAgreements), Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -49,6 +49,8 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::AcceptPackageAgreements), Argument::ForType(Args::Type::NoUpgrade), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Args::Type::Rename), Argument::ForType(Args::Type::UninstallPrevious), @@ -123,7 +125,7 @@ namespace AppInstaller::CLI Workflow::GetManifestFromArg << Workflow::SelectInstaller << Workflow::EnsureApplicableInstaller << - Workflow::Checkpoint("exampleCheckpoint", {}) << // TODO: Checkpoint example + Workflow::Checkpoint("PreInstallCheckpoint", {}) << // TODO: Capture context data Workflow::InstallSinglePackage; } else @@ -135,7 +137,7 @@ namespace AppInstaller::CLI if (!context.Args.Contains(Execution::Args::Type::Force)) { context << - Workflow::OpenCompositeSource(Repository::PredefinedSource::Installed, false, Repository::CompositeSearchBehavior::AvailablePackages); + Workflow::OpenCompositeSource(Workflow::DetermineInstalledSource(context), false, Repository::CompositeSearchBehavior::AvailablePackages); } if (context.Args.Contains(Execution::Args::Type::MultiQuery)) @@ -153,7 +155,7 @@ namespace AppInstaller::CLI else { context << - Workflow::Checkpoint("exampleCheckpoint", {}) << // TODO: Checkpoint example + Workflow::Checkpoint("PreInstallCheckpoint", {}) << // TODO: Capture context data Workflow::InstallOrUpgradeSinglePackage(OperationType::Install); } } diff --git a/src/AppInstallerCLICore/Commands/ListCommand.cpp b/src/AppInstallerCLICore/Commands/ListCommand.cpp @@ -25,6 +25,8 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::Exact), Argument{ Execution::Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Execution::Args::Type::CustomHeader), + Argument::ForType(Execution::Args::Type::AuthenticationMode), + Argument::ForType(Execution::Args::Type::AuthenticationAccount), Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), Argument{ Execution::Args::Type::Upgrade, Resource::String::UpgradeArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help }, Argument{ Execution::Args::Type::IncludeUnknown, Resource::String::IncludeUnknownInListArgumentDescription, ArgumentType::Flag }, diff --git a/src/AppInstallerCLICore/Commands/PinCommand.cpp b/src/AppInstallerCLICore/Commands/PinCommand.cpp @@ -59,6 +59,8 @@ namespace AppInstaller::CLI Argument{ Args::Type::GatedVersion, Resource::String::GatedVersionArgumentDescription, ArgumentType::Standard }, Argument::ForType(Args::Type::Source), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument::ForType(Args::Type::Force), Argument{ Args::Type::BlockingPin, Resource::String::PinAddBlockingArgumentDescription, ArgumentType::Flag }, @@ -151,6 +153,8 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Command), Argument::ForType(Args::Type::Exact), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), Argument::ForType(Args::Type::AcceptSourceAgreements), Argument{ Args::Type::PinInstalled, Resource::String::PinInstalledArgumentDescription, ArgumentType::Flag }, }; @@ -237,6 +241,8 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Command), Argument::ForType(Args::Type::Exact), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), Argument::ForType(Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.cpp b/src/AppInstallerCLICore/Commands/SearchCommand.cpp @@ -25,6 +25,8 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::Count), Argument::ForType(Execution::Args::Type::Exact), Argument::ForType(Execution::Args::Type::CustomHeader), + Argument::ForType(Execution::Args::Type::AuthenticationMode), + Argument::ForType(Execution::Args::Type::AuthenticationAccount), Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), Argument::ForType(Execution::Args::Type::ListVersions), }; diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -31,6 +31,8 @@ namespace AppInstaller::CLI Argument::ForType(Execution::Args::Type::Locale), Argument::ForType(Execution::Args::Type::ListVersions), Argument::ForType(Execution::Args::Type::CustomHeader), + Argument::ForType(Execution::Args::Type::AuthenticationMode), + Argument::ForType(Execution::Args::Type::AuthenticationAccount), Argument::ForType(Execution::Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.cpp b/src/AppInstallerCLICore/Commands/UninstallCommand.cpp @@ -35,6 +35,8 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::Preserve), Argument::ForType(Args::Type::Log), Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), Argument::ForType(Args::Type::AcceptSourceAgreements), }; } diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -55,7 +55,7 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::CustomSwitches), Argument::ForType(Args::Type::Override), Argument::ForType(Args::Type::InstallLocation), // -l - Argument{ Execution::Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, + Argument{ Args::Type::InstallScope, Resource::String::InstalledScopeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument::ForType(Args::Type::InstallArchitecture), // -a Argument::ForType(Args::Type::InstallerType), Argument::ForType(Args::Type::Locale), @@ -65,7 +65,9 @@ namespace AppInstaller::CLI Argument::ForType(Args::Type::IgnoreLocalArchiveMalwareScan), Argument::ForType(Args::Type::AcceptPackageAgreements), Argument::ForType(Args::Type::AcceptSourceAgreements), - Argument::ForType(Execution::Args::Type::CustomHeader), + Argument::ForType(Args::Type::CustomHeader), + Argument::ForType(Args::Type::AuthenticationMode), + Argument::ForType(Args::Type::AuthenticationAccount), Argument{ Args::Type::All, Resource::String::UpdateAllArgumentDescription, ArgumentType::Flag }, Argument{ Args::Type::IncludeUnknown, Resource::String::IncludeUnknownArgumentDescription, ArgumentType::Flag }, Argument{ Args::Type::IncludePinned, Resource::String::IncludePinnedArgumentDescription, ArgumentType::Flag}, diff --git a/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp b/src/AppInstallerCLICore/ConfigurationWingetDscModuleUnitValidation.cpp @@ -336,6 +336,7 @@ namespace AppInstaller::CLI::Configuration } else { + source.SetCaller("winget-cli-configuration-unit-module-validation"); ProgressCallback empty; source.Open(empty); Repository::SearchRequest searchRequest; diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -137,6 +137,9 @@ namespace AppInstaller::CLI::Execution CustomHeader, // Optional Rest source header AcceptSourceAgreements, // Accept all source agreements + AuthenticationMode, // Authentication mode (silent, silentPreferred or interactive) + AuthenticationAccount, // Authentication account to be used + ToolVersion, // Used for demonstration purposes diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -21,6 +21,7 @@ namespace AppInstaller::CLI::Execution const Sequence& ConvertToUpgradeFlowEmphasis = TextFormat::Foreground::BrightYellow; const Sequence& ConfigurationIntentEmphasis = TextFormat::Foreground::Bright; const Sequence& ConfigurationUnitEmphasis = TextFormat::Foreground::BrightCyan; + const Sequence& AuthenticationEmphasis = TextFormat::Foreground::BrightYellow; Reporter::Reporter(std::ostream& outStream, std::istream& inStream) : Reporter(std::make_shared<BaseStream>(outStream, true, ConsoleModeRestore::Instance().IsVTEnabled()), inStream) diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -194,4 +194,5 @@ namespace AppInstaller::CLI::Execution extern const VirtualTerminal::Sequence& ConvertToUpgradeFlowEmphasis; extern const VirtualTerminal::Sequence& ConfigurationIntentEmphasis; extern const VirtualTerminal::Sequence& ConfigurationUnitEmphasis; + extern const VirtualTerminal::Sequence& AuthenticationEmphasis; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -33,6 +33,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ArchiveFailedMalwareScan); WINGET_DEFINE_RESOURCE_STRINGID(ArchiveFailedMalwareScanOverridden); WINGET_DEFINE_RESOURCE_STRINGID(ArgumentForSinglePackageProvidedWithMultipleQueries); + WINGET_DEFINE_RESOURCE_STRINGID(AuthenticationAccountArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(AuthenticationModeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(AvailableArguments); WINGET_DEFINE_RESOURCE_STRINGID(AvailableCommandAliases); WINGET_DEFINE_RESOURCE_STRINGID(AvailableCommands); @@ -495,6 +497,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceAddBegin); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceAddFailedAuthenticationNotSupported); WINGET_DEFINE_RESOURCE_STRINGID(SourceAddOpenSourceFailed); WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsMarketMessage); WINGET_DEFINE_RESOURCE_STRINGID(SourceAgreementsNotAgreedTo); @@ -529,6 +532,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveOne); + WINGET_DEFINE_RESOURCE_STRINGID(SourceRequiresAuthentication); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -119,6 +119,12 @@ namespace AppInstaller::CLI::Workflow } } + if (sourceToAdd.GetInformation().Authentication.Type == Authentication::AuthenticationType::Unknown) + { + context.Reporter.Error() << Resource::String::SourceAddFailedAuthenticationNotSupported << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED); + } + context << Workflow::HandleSourceAgreements(sourceToAdd); if (context.IsTerminated()) { diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -106,6 +106,7 @@ namespace AppInstaller::CLI::Workflow auto openFunction = [&](IProgressCallback& progress)->std::vector<Repository::SourceDetails> { source.SetCaller("winget-cli"); + source.SetAuthenticationArguments(GetAuthenticationArguments(context)); return source.Open(progress); }; auto updateFailures = context.Reporter.ExecuteWithProgress(openFunction, true); @@ -115,6 +116,22 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate(Utility::LocIndView{ s.Name }) << std::endl; } + + // Report sources that may need authentication + if (source.IsComposite()) + { + for (const auto& s : source.GetAvailableSources()) + { + if (s.GetInformation().Authentication.Type != Authentication::AuthenticationType::None) + { + context.Reporter.Info() << Execution::AuthenticationEmphasis << Resource::String::SourceRequiresAuthentication(Utility::LocIndView{ s.GetDetails().Name }) << std::endl; + } + } + } + else if (source.GetInformation().Authentication.Type != Authentication::AuthenticationType::None) + { + context.Reporter.Info() << Execution::AuthenticationEmphasis << Resource::String::SourceRequiresAuthentication(Utility::LocIndView{ source.GetDetails().Name }) << std::endl; + } } catch (const wil::ResultException& re) { @@ -256,6 +273,30 @@ namespace AppInstaller::CLI::Workflow return installedSource; } + Authentication::AuthenticationArguments GetAuthenticationArguments(const Execution::Context& context) + { + AppInstaller::Authentication::AuthenticationArguments authArgs; + + if (context.Args.Contains(Execution::Args::Type::AuthenticationMode)) + { + authArgs.Mode = Authentication::ConvertToAuthenticationMode(context.Args.GetArg(Execution::Args::Type::AuthenticationMode)); + } + else + { + // If user did not specify authentication mode, determine based on if disable interactivity flag exists. + authArgs.Mode = context.Args.Contains(Execution::Args::Type::DisableInteractivity) ? Authentication::AuthenticationMode::Silent : Authentication::AuthenticationMode::SilentPreferred; + } + + if (context.Args.Contains(Execution::Args::Type::AuthenticationAccount)) + { + authArgs.AuthenticationAccount = context.Args.GetArg(Execution::Args::Type::AuthenticationAccount); + } + + AICLI_LOG(CLI, Info, << "Created authentication arguments. Mode: " << Authentication::AuthenticationModeToString(authArgs.Mode) << ", Account: " << authArgs.AuthenticationAccount); + + return authArgs; + } + HRESULT HandleException(Execution::Context& context, std::exception_ptr exception) { try @@ -384,7 +425,6 @@ namespace AppInstaller::CLI::Workflow auto openFunction = [&](IProgressCallback& progress)->std::vector<Repository::SourceDetails> { - source.SetCaller("winget-cli"); return source.Open(progress); }; context.Reporter.ExecuteWithProgress(openFunction, true); diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -6,6 +6,7 @@ #include <winget/ExperimentalFeature.h> #include <winget/RepositorySearch.h> #include <winget/RepositorySource.h> +#include <winget/Authentication.h> #include <string> #include <string_view> @@ -78,6 +79,9 @@ namespace AppInstaller::CLI::Workflow // Helper to determine installed source to use based on context input. Repository::PredefinedSource DetermineInstalledSource(const Execution::Context& context); + // Helper to create authentication arguments from context input. + Authentication::AuthenticationArguments GetAuthenticationArguments(const Execution::Context& context); + // Helper to report exceptions and return the HRESULT. HRESULT HandleException(Execution::Context& context, std::exception_ptr exception); diff --git a/src/AppInstallerCLIPackage/Package.appxmanifest b/src/AppInstallerCLIPackage/Package.appxmanifest @@ -77,7 +77,9 @@ <com:Class Id ="C9ED7917-66AB-4E31-A92A-F65F18EF7933" DisplayName="Configuration Statics Server"> </com:Class> <com:Class Id ="8EF324ED-367C-4880-83E5-BB2ABD0B72F6" DisplayName="DownloadOptions Server"> - </com:Class> + </com:Class> + <com:Class Id ="6484A61D-50FA-41F0-B71E-F4370C6EB37C" DisplayName="AuthenticationArguments Server"> + </com:Class> </com:ExeServer> </com:ComServer> </com:Extension> diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -875,7 +875,7 @@ They can be configured through the settings file 'winget settings'.</value> </data> <data name="InstallScopeDescription" xml:space="preserve"> <value>Select install scope (user or machine)</value> - <comment>This argument allows the user to select between installing for just the user or for the entire machine.</comment> + <comment>{Locked="user","machine"} This argument allows the user to select between installing for just the user or for the entire machine.</comment> </data> <data name="InvalidArgumentValueError" xml:space="preserve"> <value>The value provided for the `{0}` argument is invalid; valid values are: {1}</value> @@ -1615,7 +1615,7 @@ Please specify one of them using the --source option to proceed.</value> </data> <data name="InstalledScopeArgumentDescription" xml:space="preserve"> <value>Select installed package scope filter (user or machine)</value> - <comment>This argument allows the user to select installed packages for just the user or for the entire machine.</comment> + <comment>{Locked="user","machine"} This argument allows the user to select installed packages for just the user or for the entire machine.</comment> </data> <data name="PinAdded" xml:space="preserve"> <value>Pin added successfully</value> @@ -2422,6 +2422,42 @@ Please specify one of them using the --source option to proceed.</value> <data name="APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED" xml:space="preserve"> <value>Failed to download package. Download for offline installation is prohibited.</value> </data> + <data name="APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE" xml:space="preserve"> + <value>A required service is busy or unavailable. Try again later.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_RESUME_ID_NOT_FOUND" xml:space="preserve"> + <value>The guid provided does not correspond to a valid resume state.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH" xml:space="preserve"> + <value>The current client version did not match the client version of the saved state.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE" xml:space="preserve"> + <value>The resume state data is invalid.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_CANNOT_OPEN_CHECKPOINT_INDEX" xml:space="preserve"> + <value>Unable to open the checkpoint database.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_RESUME_LIMIT_EXCEEDED" xml:space="preserve"> + <value>Exceeded max resume limit.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO" xml:space="preserve"> + <value>Invalid authentication info.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED" xml:space="preserve"> + <value>Authentication method not supported.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED" xml:space="preserve"> + <value>Authentication failed.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_AUTHENTICATION_INTERACTIVE_REQUIRED" xml:space="preserve"> + <value>Authentication failed. Interactive authentication required.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_AUTHENTICATION_CANCELLED_BY_USER" xml:space="preserve"> + <value>Authentication failed. User cancelled.</value> + </data> + <data name="APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT" xml:space="preserve"> + <value>Authentication failed. Authenticated account is not the desired account.</value> + </data> <data name="APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE" xml:space="preserve"> <value>Application is currently running. Exit the application then try again.</value> </data> @@ -2706,4 +2742,19 @@ Please specify one of them using the --source option to proceed.</value> <value>WinGetPackage configuration unit package cannot be validated. Package Id: {0}</value> <comment>{Locked="WinGetPackage,{0}"}</comment> </data> + <data name="AuthenticationModeArgumentDescription" xml:space="preserve"> + <value>Specify authentication window preference (silent, silentPreferred or interactive)</value> + <comment>{Locked="silent","silentPreferred","interactive"} This argument allows the user to select authentication window popup behavior.</comment> + </data> + <data name="AuthenticationAccountArgumentDescription" xml:space="preserve"> + <value>Specify the account to be used for authentication</value> + </data> + <data name="SourceAddFailedAuthenticationNotSupported" xml:space="preserve"> + <value>Failed to add source. This winget version does not support the source's authentication method. Try upgrade to latest winget version.</value> + <comment>{Locked="winget"}</comment> + </data> + <data name="SourceRequiresAuthentication" xml:space="preserve"> + <value>The {0} source requires authentication. Authentication prompt may appear when necessary. Authenticated information will be shared with the source for access authorization.</value> + <comment>{Locked="{0}"}</comment> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -250,6 +250,7 @@ <ClCompile Include="RestInterface_1_4.cpp" /> <ClCompile Include="RestInterface_1_5.cpp" /> <ClCompile Include="RestInterface_1_6.cpp" /> + <ClCompile Include="RestInterface_1_7.cpp" /> <ClCompile Include="ResumeFlow.cpp" /> <ClCompile Include="Runtime.cpp" /> <ClCompile Include="SearchRequestSerializer.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -341,6 +341,9 @@ <ClCompile Include="AppShutdown.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="RestInterface_1_7.cpp"> + <Filter>Source Files\Repository</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/CustomHeader.cpp b/src/AppInstallerCLITests/CustomHeader.cpp @@ -26,7 +26,7 @@ namespace constexpr std::string_view s_EmptySources = R"( Sources: )"sv; - + utility::string_t sampleSearchResponse = _XPLATSTR( R"delimiter({ "Data" : [ @@ -38,33 +38,6 @@ namespace { "PackageVersion": "1.0.0" }] }] })delimiter"); - - std::shared_ptr<TestRestRequestHandler> GetCustomHeaderVerificationHandler( - const web::http::status_code statusCode, const utility::string_t& sampleResponseString, const std::pair<utility::string_t, utility::string_t>& customHeader) - { - return std::make_shared<TestRestRequestHandler>([statusCode, sampleResponseString, customHeader](web::http::http_request request) -> - pplx::task<web::http::http_response> - { - web::http::http_response response; - auto& headers = request.headers(); - if (!headers.has(customHeader.first) || - (utility::conversions::to_utf8string(customHeader.second).compare(utility::conversions::to_utf8string(headers[customHeader.first]))) != 0) - { - response.set_body(utf16string{ L"Bad Request" }); - response.set_status_code(web::http::status_codes::BadRequest); - return pplx::task_from_result(response); - } - - if (!sampleResponseString.empty()) - { - response.set_body(web::json::value::parse(sampleResponseString)); - } - - response.headers().set_content_type(web::http::details::mime_types::application_json); - response.set_status_code(statusCode); - return pplx::task_from_result(response); - }); - } } TEST_CASE("RestClient_CustomHeader", "[RestSource][CustomHeader]") @@ -80,8 +53,8 @@ TEST_CASE("RestClient_CustomHeader", "[RestSource][CustomHeader]") std::optional<std::string> customHeader = "Testing custom header"; auto header = std::make_pair<>(CustomHeaderName, JSON::GetUtilityString(customHeader.value())); - HttpClientHelper helper{ GetCustomHeaderVerificationHandler(web::http::status_codes::OK, sample, header) }; - RestClient client = RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), customHeader, {}, std::move(helper)); + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, sample, header) }; + RestClient client = RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), customHeader, {}, {}, std::move(helper)); REQUIRE(client.GetSourceIdentifier() == "Source123"); } @@ -89,7 +62,7 @@ TEST_CASE("RestSourceSearch_CustomHeader", "[RestSource][CustomHeader]") { utility::string_t customHeader = L"Testing custom header"; auto header = std::make_pair<>(CustomHeaderName, customHeader); - HttpClientHelper helper{ GetCustomHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; std::unordered_map<utility::string_t, utility::string_t> headers; headers.emplace(CustomHeaderName, customHeader); @@ -103,7 +76,7 @@ TEST_CASE("RestSourceSearch_WhitespaceCustomHeader", "[RestSource][CustomHeader] { utility::string_t customHeader = L" "; auto header = std::make_pair<>(CustomHeaderName, customHeader); - HttpClientHelper helper{ GetCustomHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; std::unordered_map<utility::string_t, utility::string_t> headers; headers.emplace(CustomHeaderName, customHeader); @@ -116,7 +89,7 @@ TEST_CASE("RestSourceSearch_NoCustomHeader", "[RestSource][CustomHeader]") { utility::string_t customHeader = L" "; auto header = std::make_pair<>(CustomHeaderName, customHeader); - HttpClientHelper helper{ GetCustomHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; std::unordered_map<utility::string_t, utility::string_t> headers; headers.emplace(CustomHeaderName, customHeader); @@ -128,9 +101,9 @@ TEST_CASE("RestSourceSearch_CustomHeaderExceedingSize", "[RestSource][CustomHead { std::string customHeader = "This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. This is a custom header that is longer than 1024 characters. "; auto header = std::make_pair<>(CustomHeaderName, JSON::GetUtilityString(customHeader)); - HttpClientHelper helper{ GetCustomHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, sampleSearchResponse, header) }; - REQUIRE_THROWS_HR(RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), customHeader, {}, std::move(helper)), + REQUIRE_THROWS_HR(RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), customHeader, {}, {}, std::move(helper)), APPINSTALLER_CLI_ERROR_CUSTOMHEADER_EXCEEDS_MAXLENGTH); } @@ -147,8 +120,8 @@ TEST_CASE("RestClient_CustomUserAgentHeader", "[RestSource][CustomHeader]") std::string testCaller = "TestCaller"; auto header = std::make_pair<>(web::http::header_names::user_agent, JSON::GetUtilityString(Runtime::GetUserAgent(testCaller))); - HttpClientHelper helper{ GetCustomHeaderVerificationHandler(web::http::status_codes::OK, sample, header) }; - RestClient client = RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), {}, testCaller, std::move(helper)); + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, sample, header) }; + RestClient client = RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), {}, testCaller, {}, std::move(helper)); REQUIRE(client.GetSourceIdentifier() == "Source123"); } @@ -164,7 +137,7 @@ TEST_CASE("RestClient_DefaultUserAgentHeader", "[RestSource][CustomHeader]") }})delimiter"); auto header = std::make_pair<>(web::http::header_names::user_agent, JSON::GetUtilityString(Runtime::GetDefaultUserAgent())); - HttpClientHelper helper{ GetCustomHeaderVerificationHandler(web::http::status_codes::OK, sample, header) }; - RestClient client = RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), {}, {}, std::move(helper)); + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, sample, header) }; + RestClient client = RestClient::Create(utility::conversions::to_utf8string("https://restsource.com/api"), {}, {}, {}, std::move(helper)); REQUIRE(client.GetSourceIdentifier() == "Source123"); } \ No newline at end of file diff --git a/src/AppInstallerCLITests/RestClient.cpp b/src/AppInstallerCLITests/RestClient.cpp @@ -6,15 +6,15 @@ #include <Rest/RestClient.h> #include <Rest/Schema/IRestClient.h> #include <AppInstallerVersions.h> -#include <set> #include <AppInstallerErrors.h> +#include <AppInstallerRuntime.h> using namespace AppInstaller; using namespace AppInstaller::Utility; using namespace AppInstaller::Repository::Rest; using namespace AppInstaller::Repository::Rest::Schema; -const utility::string_t TestRestUri = L"http://restsource.net"; +const std::string TestRestUri = "http://restsource.net"; TEST_CASE("GetLatestCommonVersion", "[RestSource]") { @@ -47,11 +47,25 @@ TEST_CASE("GetSupportedInterface", "[RestSource]") IRestClient::Information info{ "TestId", { "1.0.0" } }; Version version{ "1.0.0" }; - REQUIRE(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), {}, info, version)->GetVersion() == version); + REQUIRE(RestClient::GetSupportedInterface(TestRestUri, {}, info, {}, version)->GetVersion() == version); // Update this test to next version so that we don't forget to add to supported versions before rest e2e tests are available. - Version invalid{ "1.7.0" }; - REQUIRE_THROWS(RestClient::GetSupportedInterface(utility::conversions::to_utf8string(TestRestUri), {}, info, invalid)); + Version invalid{ "1.8.0" }; + REQUIRE_THROWS_HR(RestClient::GetSupportedInterface(TestRestUri, {}, info, {}, invalid), APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION); + + Authentication::AuthenticationArguments authArgs; + authArgs.Mode = Authentication::AuthenticationMode::Silent; + Version version_1_7{ "1.7.0" }; + + // GetSupportedInterface throws on unknown authentication type. + IRestClient::Information infoWithUnknownAuthenticationType{ "TestId", { "1.7.0" } }; + infoWithUnknownAuthenticationType.Authentication.Type = Authentication::AuthenticationType::Unknown; + REQUIRE_THROWS_HR(RestClient::GetSupportedInterface(TestRestUri, {}, infoWithUnknownAuthenticationType, authArgs, version_1_7), APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED); + + // GetSupportedInterface throws on invalid authentication info. + IRestClient::Information infoWithInvalidAuthenticationInfo{ "TestId", { "1.7.0" } }; + infoWithInvalidAuthenticationInfo.Authentication.Type = Authentication::AuthenticationType::MicrosoftEntraId; + REQUIRE_THROWS_HR(RestClient::GetSupportedInterface(TestRestUri, {}, infoWithInvalidAuthenticationInfo, authArgs, version_1_7), APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO); } TEST_CASE("GetInformation_Success", "[RestSource]") @@ -62,7 +76,8 @@ TEST_CASE("GetInformation_Success", "[RestSource]") "SourceIdentifier": "Source123", "ServerSupportedVersions": [ "1.0.0", - "1.1.0"], + "1.1.0" + ], "SourceAgreements": { "AgreementsIdentifier": "agreementV1", "Agreements": [{ @@ -87,7 +102,7 @@ TEST_CASE("GetInformation_Success", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - IRestClient::Information information = RestClient::GetInformation(TestRestUri, {}, std::move(helper)); + IRestClient::Information information = RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper)); REQUIRE(information.SourceIdentifier == "Source123"); REQUIRE(information.ServerSupportedVersions.size() == 2); REQUIRE(information.ServerSupportedVersions.at(0) == "1.0.0"); @@ -105,6 +120,71 @@ TEST_CASE("GetInformation_Success", "[RestSource]") REQUIRE(information.UnsupportedQueryParameters.at(0) == "Moniker"); REQUIRE(information.UnsupportedPackageMatchFields.size() == 1); REQUIRE(information.UnsupportedPackageMatchFields.at(0) == "Moniker"); + REQUIRE(information.Authentication.Type == Authentication::AuthenticationType::None); + REQUIRE_FALSE(information.Authentication.MicrosoftEntraIdInfo.has_value()); +} + +TEST_CASE("GetInformation_WithAuthenticationInfo_Success", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "SourceAgreements": { + "AgreementsIdentifier": "agreementV1", + "Agreements": [{ + "AgreementLabel": "EULA", + "Agreement": "this is store agreement", + "AgreementUrl": "https://store.agreement" + } + ] + }, + "RequiredQueryParameters": [ + "Market" + ], + "RequiredPackageMatchFields": [ + "Market" + ], + "UnsupportedQueryParameters": [ + "Moniker" + ], + "UnsupportedPackageMatchFields": [ + "Moniker" + ], + "Authentication": { + "AuthenticationType": "microsoftEntraId", + "MicrosoftEntraIdAuthenticationInfo" : { + "Resource": "GUID", + "Scope" : "test" + } + } + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + IRestClient::Information information = RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper)); + REQUIRE(information.SourceIdentifier == "Source123"); + REQUIRE(information.ServerSupportedVersions.size() == 1); + REQUIRE(information.ServerSupportedVersions.at(0) == "1.7.0"); + REQUIRE(information.SourceAgreementsIdentifier == "agreementV1"); + REQUIRE(information.SourceAgreements.size() == 1); + REQUIRE(information.SourceAgreements.at(0).Label == "EULA"); + REQUIRE(information.SourceAgreements.at(0).Text == "this is store agreement"); + REQUIRE(information.SourceAgreements.at(0).Url == "https://store.agreement"); + REQUIRE(information.RequiredQueryParameters.size() == 1); + REQUIRE(information.RequiredQueryParameters.at(0) == "Market"); + REQUIRE(information.RequiredPackageMatchFields.size() == 1); + REQUIRE(information.RequiredPackageMatchFields.at(0) == "Market"); + REQUIRE(information.UnsupportedQueryParameters.size() == 1); + REQUIRE(information.UnsupportedQueryParameters.at(0) == "Moniker"); + REQUIRE(information.UnsupportedPackageMatchFields.size() == 1); + REQUIRE(information.UnsupportedPackageMatchFields.at(0) == "Moniker"); + REQUIRE(information.Authentication.Type == Authentication::AuthenticationType::MicrosoftEntraId); + REQUIRE(information.Authentication.MicrosoftEntraIdInfo.has_value()); + REQUIRE(information.Authentication.MicrosoftEntraIdInfo->Resource == "GUID"); + REQUIRE(information.Authentication.MicrosoftEntraIdInfo->Scope == "test"); } TEST_CASE("GetInformation_Fail_AgreementsWithoutIdentifier", "[RestSource]") @@ -127,7 +207,62 @@ TEST_CASE("GetInformation_Fail_AgreementsWithoutIdentifier", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - REQUIRE_THROWS_HR(RestClient::GetInformation(TestRestUri, {}, std::move(helper)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); + REQUIRE_THROWS_HR(RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); +} + +TEST_CASE("GetInformation_Fail_InvalidMicrosoftEntraIdInfo", "[RestSource]") +{ + utility::string_t sample1 = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "Authentication": { + "AuthenticationType": "microsoftEntraId" + } + }})delimiter"); + + HttpClientHelper helper1{ GetTestRestRequestHandler(web::http::status_codes::OK, sample1) }; + REQUIRE_THROWS_HR(RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper1)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); + + utility::string_t sample2 = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "Authentication": { + "AuthenticationType": "microsoftEntraId", + "MicrosoftEntraIdAuthenticationInfo" : { + "Resource": "", + "Scope" : "test" + } + } + }})delimiter"); + + HttpClientHelper helper2{ GetTestRestRequestHandler(web::http::status_codes::OK, sample2) }; + REQUIRE_THROWS_HR(RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper2)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); + + utility::string_t sample3 = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "Authentication": { + "AuthenticationType": "microsoftEntraId", + "MicrosoftEntraIdAuthenticationInfo" : { + "Scope" : "test" + } + } + }})delimiter"); + + HttpClientHelper helper3{ GetTestRestRequestHandler(web::http::status_codes::OK, sample3) }; + REQUIRE_THROWS_HR(RestClient::GetInformation(TestRestUri, {}, {}, std::move(helper3)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); } TEST_CASE("RestClientCreate_UnsupportedVersion", "[RestSource]") @@ -142,7 +277,50 @@ TEST_CASE("RestClientCreate_UnsupportedVersion", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - REQUIRE_THROWS_HR(RestClient::Create("https://restsource.com/api", {}, {}, std::move(helper)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); + REQUIRE_THROWS_HR(RestClient::Create("https://restsource.com/api", {}, {}, {}, std::move(helper)), APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE); +} + +TEST_CASE("RestClientCreate_UnsupportedAuthenticationMethod", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "Authentication": { + "AuthenticationType": "unknown" + } + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + Authentication::AuthenticationArguments authArgs; + authArgs.Mode = Authentication::AuthenticationMode::Silent; + REQUIRE_THROWS_HR(RestClient::Create("https://restsource.com/api", {}, {}, std::move(authArgs), std::move(helper)), APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED); +} + +TEST_CASE("RestClientCreate_InvalidAuthenticationArguments", "[RestSource]") +{ + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "Authentication": { + "AuthenticationType": "microsoftEntraId", + "MicrosoftEntraIdAuthenticationInfo" : { + "Resource" : "test" + } + } + }})delimiter"); + + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + Authentication::AuthenticationArguments authArgs; + authArgs.Mode = Authentication::AuthenticationMode::Unknown; + REQUIRE_THROWS_HR(RestClient::Create("https://restsource.com/api", {}, {}, std::move(authArgs), std::move(helper)), E_UNEXPECTED); } TEST_CASE("RestClientCreate_1.0_Success", "[RestSource]") @@ -157,7 +335,7 @@ TEST_CASE("RestClientCreate_1.0_Success", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - RestClient client = RestClient::Create(utility::conversions::to_utf8string(TestRestUri), {}, {}, std::move(helper)); + RestClient client = RestClient::Create(TestRestUri, {}, {}, {}, std::move(helper)); REQUIRE(client.GetSourceIdentifier() == "Source123"); } @@ -194,7 +372,73 @@ TEST_CASE("RestClientCreate_1.1_Success", "[RestSource]") }})delimiter"); HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; - RestClient client = RestClient::Create(utility::conversions::to_utf8string(TestRestUri), {}, {}, std::move(helper)); + RestClient client = RestClient::Create(TestRestUri, {}, {}, {}, std::move(helper)); + REQUIRE(client.GetSourceIdentifier() == "Source123"); + auto information = client.GetSourceInformation(); + REQUIRE(information.SourceAgreementsIdentifier == "agreementV1"); + REQUIRE(information.SourceAgreements.size() == 1); + REQUIRE(information.SourceAgreements.at(0).Label == "EULA"); + REQUIRE(information.SourceAgreements.at(0).Text == "this is store agreement"); + REQUIRE(information.SourceAgreements.at(0).Url == "https://store.agreement"); + REQUIRE(information.RequiredQueryParameters.size() == 1); + REQUIRE(information.RequiredQueryParameters.at(0) == "Market"); + REQUIRE(information.RequiredPackageMatchFields.size() == 1); + REQUIRE(information.RequiredPackageMatchFields.at(0) == "Market"); + REQUIRE(information.UnsupportedQueryParameters.size() == 1); + REQUIRE(information.UnsupportedQueryParameters.at(0) == "Moniker"); + REQUIRE(information.UnsupportedPackageMatchFields.size() == 1); + REQUIRE(information.UnsupportedPackageMatchFields.at(0) == "Moniker"); +} + +TEST_CASE("RestClientCreate_1.7_Success", "[RestSource]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + utility::string_t sample = _XPLATSTR( + R"delimiter({ + "Data" : { + "SourceIdentifier": "Source123", + "ServerSupportedVersions": [ + "1.7.0" + ], + "SourceAgreements": { + "AgreementsIdentifier": "agreementV1", + "Agreements": [{ + "AgreementLabel": "EULA", + "Agreement": "this is store agreement", + "AgreementUrl": "https://store.agreement" + } + ] + }, + "RequiredQueryParameters": [ + "Market" + ], + "RequiredPackageMatchFields": [ + "Market" + ], + "UnsupportedQueryParameters": [ + "Moniker" + ], + "UnsupportedPackageMatchFields": [ + "Moniker" + ], + "Authentication": { + "AuthenticationType": "microsoftEntraId", + "MicrosoftEntraIdAuthenticationInfo" : { + "Resource": "GUID", + "Scope" : "test" + } + } + }})delimiter"); + + Authentication::AuthenticationArguments authArgs; + authArgs.Mode = Authentication::AuthenticationMode::Silent; + HttpClientHelper helper{ GetTestRestRequestHandler(web::http::status_codes::OK, sample) }; + RestClient client = RestClient::Create(TestRestUri, {}, {}, std::move(authArgs), std::move(helper)); REQUIRE(client.GetSourceIdentifier() == "Source123"); auto information = client.GetSourceInformation(); REQUIRE(information.SourceAgreementsIdentifier == "agreementV1"); @@ -210,4 +454,8 @@ TEST_CASE("RestClientCreate_1.1_Success", "[RestSource]") REQUIRE(information.UnsupportedQueryParameters.at(0) == "Moniker"); REQUIRE(information.UnsupportedPackageMatchFields.size() == 1); REQUIRE(information.UnsupportedPackageMatchFields.at(0) == "Moniker"); + REQUIRE(information.Authentication.Type == Authentication::AuthenticationType::MicrosoftEntraId); + REQUIRE(information.Authentication.MicrosoftEntraIdInfo.has_value()); + REQUIRE(information.Authentication.MicrosoftEntraIdInfo->Resource == "GUID"); + REQUIRE(information.Authentication.MicrosoftEntraIdInfo->Scope == "test"); } diff --git a/src/AppInstallerCLITests/RestInterface_1_7.cpp b/src/AppInstallerCLITests/RestInterface_1_7.cpp @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include "TestHooks.h" +#include "TestRestRequestHandler.h" +#include <Rest/Schema/1_7/Interface.h> +#include <Rest/Schema/IRestClient.h> +#include <AppInstallerVersions.h> +#include <AppInstallerErrors.h> +#include <winget/Authentication.h> +#include <winget/JsonUtil.h> + +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::Authentication; +using namespace AppInstaller::Utility; +using namespace AppInstaller::Manifest; +using namespace AppInstaller::Repository; +using namespace AppInstaller::Repository::Rest; +using namespace AppInstaller::Repository::Rest::Schema; +using namespace AppInstaller::Repository::Rest::Schema::V1_7; + +namespace +{ + const std::string TestRestUriString = "http://restsource.com/api"; + + IRestClient::Information GetTestSourceInformation() + { + IRestClient::Information result; + + result.Authentication.Type = AuthenticationType::MicrosoftEntraId; + MicrosoftEntraIdAuthenticationInfo microsoftEntraIdInfo; + microsoftEntraIdInfo.Resource = "GUID"; + result.Authentication.MicrosoftEntraIdInfo = std::move(microsoftEntraIdInfo); + + return result; + } + + AuthenticationArguments GetTestAuthenticationArguments() + { + AuthenticationArguments result; + result.Mode = AuthenticationMode::Silent; + return result; + } + + utility::string_t SampleSearchResponse = _XPLATSTR( + R"delimiter({ + "Data" : [ + { + "PackageIdentifier": "git.package", + "PackageName": "package", + "Publisher": "git", + "Versions": [ + { "PackageVersion": "1.0.0" }] + }] + })delimiter"); + + utility::string_t SampleGetManifestResponse = _XPLATSTR( + R"delimiter({ + "Data": { + "PackageIdentifier": "Foo.Bar", + "Versions": [ + { + "PackageVersion": "5.0.0", + "DefaultLocale": { + "PackageLocale": "en-us", + "Publisher": "Foo", + "PackageName": "Bar", + "License": "Foo bar license", + "ShortDescription": "Foo bar description" + }, + "Installers": [ + { + "Architecture": "x64", + "InstallerSha256": "011048877dfaef109801b3f3ab2b60afc74f3fc4f7b3430e0c897f5da1df84b6", + "InstallerType": "exe", + "InstallerUrl": "https://installer.example.com/foobar.exe" + } + ] + } + ] + } + })delimiter"); +} + +TEST_CASE("GetManifests_GoodRequest_Authentication", "[RestSource][Interface_1_7]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + std::string expectedToken = "TestToken"; + + // Set good authentication result + AuthenticationResult authResultOverride; + authResultOverride.Status = S_OK; + authResultOverride.Token = expectedToken; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + // GetManifest should succeed with expected value. + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, SampleGetManifestResponse, { web::http::header_names::authorization, JSON::GetUtilityString(CreateBearerToken(expectedToken)) }, web::http::status_codes::Unauthorized) }; + Interface v1_7{ TestRestUriString, GetTestSourceInformation(), {}, GetTestAuthenticationArguments(), std::move(helper) }; + auto manifestResult = v1_7.GetManifestByVersion("Foo.Bar", "5.0.0", ""); + REQUIRE(manifestResult.has_value()); + const auto& manifest = manifestResult.value(); + REQUIRE(manifest.Id == "Foo.Bar"); + REQUIRE(manifest.Version == "5.0.0"); +} + +TEST_CASE("GetManifests_BadRequest_AuthenticationFailed", "[RestSource][Interface_1_7]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + std::string expectedToken = "TestToken"; + + // Set authentication failed result + AuthenticationResult authResultOverride; + authResultOverride.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + // GetManifest should fail with authentication failure + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, SampleGetManifestResponse, { web::http::header_names::authorization, JSON::GetUtilityString(CreateBearerToken(expectedToken)) }, web::http::status_codes::Unauthorized) }; + Interface v1_7{ TestRestUriString, GetTestSourceInformation(), {}, GetTestAuthenticationArguments(), std::move(helper) }; + REQUIRE_THROWS_HR(v1_7.GetManifestByVersion("Foo.Bar", "5.0.0", ""), APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED); +} + +TEST_CASE("GetManifests_BadRequest_InvalidAuthenticationToken", "[RestSource][Interface_1_7]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + std::string expectedToken = "TestToken"; + + // Set authentication result with incorrect token + AuthenticationResult authResultOverride; + authResultOverride.Status = S_OK; + authResultOverride.Token = "OtherToken"; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + // GetManifest should fail with access denied + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, SampleGetManifestResponse, { web::http::header_names::authorization, JSON::GetUtilityString(CreateBearerToken(expectedToken)) }, web::http::status_codes::Unauthorized) }; + Interface v1_7{ TestRestUriString, GetTestSourceInformation(), {}, GetTestAuthenticationArguments(), std::move(helper) }; + REQUIRE_THROWS_HR(v1_7.GetManifestByVersion("Foo.Bar", "5.0.0", ""), HTTP_E_STATUS_DENIED); +} + +TEST_CASE("Search_GoodRequest_Authentication", "[RestSource][Interface_1_7]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + std::string expectedToken = "TestToken"; + + // Set good authentication result + AuthenticationResult authResultOverride; + authResultOverride.Status = S_OK; + authResultOverride.Token = expectedToken; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + // Search should succeed with expected value. + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, SampleSearchResponse, { web::http::header_names::authorization, JSON::GetUtilityString(CreateBearerToken(expectedToken)) }, web::http::status_codes::Unauthorized) }; + Interface v1_7{ TestRestUriString, GetTestSourceInformation(), {}, GetTestAuthenticationArguments(), std::move(helper) }; + SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Name, MatchType::Exact, "package" }; + request.Filters.emplace_back(std::move(filter)); + IRestClient::SearchResult searchResponse = v1_7.Search(request); + REQUIRE(searchResponse.Matches.size() == 1); + IRestClient::Package package = searchResponse.Matches.at(0); + REQUIRE(package.PackageInformation.PackageIdentifier.compare("git.package") == 0); +} + +TEST_CASE("Search_BadRequest_AuthenticationFailed", "[RestSource][Interface_1_7]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + std::string expectedToken = "TestToken"; + + // Set authentication failed result + AuthenticationResult authResultOverride; + authResultOverride.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + // Search should fail with authentication failure + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, SampleSearchResponse, { web::http::header_names::authorization, JSON::GetUtilityString(CreateBearerToken(expectedToken)) }, web::http::status_codes::Unauthorized) }; + Interface v1_7{ TestRestUriString, GetTestSourceInformation(), {}, GetTestAuthenticationArguments(), std::move(helper) }; + SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Name, MatchType::Exact, "package" }; + request.Filters.emplace_back(std::move(filter)); + REQUIRE_THROWS_HR(v1_7.Search(request), APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED); +} + +TEST_CASE("Search_BadRequest_InvalidAuthenticationToken", "[RestSource][Interface_1_7]") +{ + if (Runtime::IsRunningAsSystem()) + { + WARN("Test does not support running as system. Skipped."); + return; + } + + std::string expectedToken = "TestToken"; + + // Set authentication result with incorrect token + AuthenticationResult authResultOverride; + authResultOverride.Status = S_OK; + authResultOverride.Token = "OtherToken"; + TestHook::SetAuthenticationResult_Override setAuthenticationResultOverride(authResultOverride); + + // Search should fail with access denied + HttpClientHelper helper{ GetHeaderVerificationHandler(web::http::status_codes::OK, SampleSearchResponse, { web::http::header_names::authorization, JSON::GetUtilityString(CreateBearerToken(expectedToken)) }, web::http::status_codes::Unauthorized) }; + Interface v1_7{ TestRestUriString, GetTestSourceInformation(), {}, GetTestAuthenticationArguments(), std::move(helper) }; + SearchRequest request; + PackageMatchFilter filter{ PackageMatchField::Name, MatchType::Exact, "package" }; + request.Filters.emplace_back(std::move(filter)); + REQUIRE_THROWS_HR(v1_7.Search(request), HTTP_E_STATUS_DENIED); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -12,6 +12,7 @@ #include <winget/UserSettings.h> #include <winget/Filesystem.h> #include <winget/IconExtraction.h> +#include <winget/Authentication.h> #ifdef AICLI_DISABLE_TEST_HOOKS static_assert(false, "Test hooks have been disabled"); @@ -75,6 +76,11 @@ namespace AppInstaller void TestHook_SetInitiateRebootResult_Override(bool* status); void TestHook_SetRegisterForRestartResult_Override(bool* status); } + + namespace Authentication + { + void TestHook_SetAuthenticationResult_Override(Authentication::AuthenticationResult* authResult); + } } namespace TestHook @@ -212,4 +218,20 @@ namespace TestHook private: bool m_status; }; + + struct SetAuthenticationResult_Override + { + SetAuthenticationResult_Override(AppInstaller::Authentication::AuthenticationResult authResult) : m_authResult(authResult) + { + AppInstaller::Authentication::TestHook_SetAuthenticationResult_Override(&m_authResult); + } + + ~SetAuthenticationResult_Override() + { + AppInstaller::Authentication::TestHook_SetAuthenticationResult_Override(nullptr); + } + + private: + AppInstaller::Authentication::AuthenticationResult m_authResult; + }; } \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestRestRequestHandler.cpp b/src/AppInstallerCLITests/TestRestRequestHandler.cpp @@ -42,3 +42,30 @@ std::shared_ptr<TestRestRequestHandler> GetTestRestRequestHandler( return pplx::task_from_result(response); }); } + +std::shared_ptr<TestRestRequestHandler> GetHeaderVerificationHandler( + const web::http::status_code statusCode, const utility::string_t& sampleResponseString, const std::pair<utility::string_t, utility::string_t>& header, web::http::status_code statusCodeOnFailure) +{ + return std::make_shared<TestRestRequestHandler>([statusCode, sampleResponseString, header, statusCodeOnFailure](web::http::http_request request) -> + pplx::task<web::http::http_response> + { + web::http::http_response response; + auto& headers = request.headers(); + if (!headers.has(header.first) || + (utility::conversions::to_utf8string(header.second).compare(utility::conversions::to_utf8string(headers[header.first]))) != 0) + { + response.set_body(utf16string{ L"Expected header not found" }); + response.set_status_code(statusCodeOnFailure); + return pplx::task_from_result(response); + } + + if (!sampleResponseString.empty()) + { + response.set_body(web::json::value::parse(sampleResponseString)); + } + + response.headers().set_content_type(web::http::details::mime_types::application_json); + response.set_status_code(statusCode); + return pplx::task_from_result(response); + }); +}+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/TestRestRequestHandler.h b/src/AppInstallerCLITests/TestRestRequestHandler.h @@ -22,3 +22,6 @@ std::shared_ptr<TestRestRequestHandler> GetTestRestRequestHandler( std::shared_ptr<TestRestRequestHandler> GetTestRestRequestHandler( std::function<web::http::status_code(const web::http::http_request& request)> handler); + +std::shared_ptr<TestRestRequestHandler> GetHeaderVerificationHandler( + const web::http::status_code statusCode, const utility::string_t& sampleResponseString, const std::pair<utility::string_t, utility::string_t>& header, web::http::status_code statusCodeOnFailure = web::http::status_codes::BadRequest); diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -389,7 +389,9 @@ </ClCompile> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="Authentication\WebAccountManagerAuthenticator.h" /> <ClInclude Include="DODownloader.h" /> + <ClInclude Include="Public\winget\Authentication.h" /> <ClInclude Include="Public\winget\FolderFileWatcher.h" /> <ClInclude Include="Public\winget\MsixManifest.h" /> <ClInclude Include="Public\winget\AdminSettings.h" /> @@ -442,6 +444,8 @@ </ItemGroup> <ItemGroup> <ClCompile Include="AdminSettings.cpp" /> + <ClCompile Include="Authentication\Authentication.cpp" /> + <ClCompile Include="Authentication\WebAccountManagerAuthenticator.cpp" /> <ClCompile Include="Debugging.cpp" /> <ClCompile Include="DependenciesGraph.cpp" /> <ClCompile Include="DODownloader.cpp" /> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -28,6 +28,9 @@ <Filter Include="Telemetry"> <UniqueIdentifier>{552a58eb-8d07-41b2-87b5-3e71b9fb3cfd}</UniqueIdentifier> </Filter> + <Filter Include="Authentication"> + <UniqueIdentifier>{9dd9ab66-00f0-4b18-90ca-6c5da3dc01c4}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -180,6 +183,12 @@ <ClInclude Include="Public\winget\Reboot.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Authentication\WebAccountManagerAuthenticator.h"> + <Filter>Authentication</Filter> + </ClInclude> + <ClInclude Include="Public\winget\Authentication.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -323,6 +332,12 @@ <ClCompile Include="Reboot.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Authentication\Authentication.cpp"> + <Filter>Authentication</Filter> + </ClCompile> + <ClCompile Include="Authentication\WebAccountManagerAuthenticator.cpp"> + <Filter>Authentication</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/Authentication/Authentication.cpp b/src/AppInstallerCommonCore/Authentication/Authentication.cpp @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/winget/Authentication.h" +#include "WebAccountManagerAuthenticator.h" +#include <AppInstallerStrings.h> +#include <AppInstallerLogging.h> + +using namespace std::string_view_literals; + +namespace AppInstaller::Authentication +{ + namespace + { + const std::string c_BearerTokenPrefix = "Bearer "; + } + + Authenticator::Authenticator(AuthenticationInfo info, AuthenticationArguments args) + { + THROW_HR_IF(E_UNEXPECTED, args.Mode == AuthenticationMode::Unknown); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED, info.Type == AuthenticationType::Unknown); + THROW_HR_IF(E_UNEXPECTED, info.Type == AuthenticationType::None); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !info.ValidateIntegrity()); + + AICLI_LOG(Core, Info, << "AuthenticationArguments values. Mode: " << AuthenticationModeToString(args.Mode) << ", Account: " << args.AuthenticationAccount); + + if (info.Type == AuthenticationType::MicrosoftEntraId) + { + AICLI_LOG(Core, Info, << "Creating WebAccountManagerAuthenticator for MicrosoftEntraId"); + m_authProvider = std::make_unique<WebAccountManagerAuthenticator>(std::move(info), std::move(args)); + } + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + static AuthenticationResult* s_AuthenticationResult_TestHook_Override = nullptr; + + void TestHook_SetAuthenticationResult_Override(Authentication::AuthenticationResult* authResult) + { + s_AuthenticationResult_TestHook_Override = authResult; + } +#endif + + // Each authentication provider uses its own mechanism for caching. + // Here we directly call authentication provider to authenticate. + AuthenticationResult Authenticator::AuthenticateForToken() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_AuthenticationResult_TestHook_Override) + { + return *s_AuthenticationResult_TestHook_Override; + } +#endif + + THROW_HR_IF(E_UNEXPECTED, !m_authProvider); + + return m_authProvider->AuthenticateForToken(); + } + + bool AuthenticationInfo::ValidateIntegrity() + { + // For MicrosoftEntraId, Resource is required. + if (Type == AuthenticationType::MicrosoftEntraId) + { + return MicrosoftEntraIdInfo.has_value() && !MicrosoftEntraIdInfo->Resource.empty(); + } + + return true; + } + + AuthenticationWindowBase::AuthenticationWindowBase() + { + InitializeWindowThread(); + } + + HWND AuthenticationWindowBase::GetHandle() + { + return m_windowHandle; + } + + AuthenticationWindowBase::~AuthenticationWindowBase() + { + if (!PostMessageW(m_windowHandle, WM_CLOSE, 0, 0)) + { + m_terminateWindowThread = true; + } + + if (m_windowThread.joinable()) + { + m_windowThread.join(); + } + } + + void AuthenticationWindowBase::InitializeWindowThread() + { + static std::once_flag s_registerWindowClassOnce; + static LPCWSTR s_windowsClassName = L"WingetAuthenticationParentWindowClass"; + static HMODULE hModule = GetModuleHandle(NULL); + THROW_LAST_ERROR_IF_NULL_MSG(hModule, "Failed to get resource module for authentication window"); + + std::call_once(s_registerWindowClassOnce, + [&]() + { + WNDCLASS wc = {}; + wc.lpfnWndProc = AuthenticationWindowBase::WindowProcessFunction; + wc.hInstance = hModule; + wc.lpszClassName = s_windowsClassName; + THROW_LAST_ERROR_IF_MSG(!RegisterClassW(&wc), "Failed to get resource module for authentication window"); + }); + + wil::unique_event waitForWindowReady; + waitForWindowReady.create(); + + m_windowThread = std::thread( + [&]() + { + m_windowHandle = CreateWindowW( + s_windowsClassName, + L"WingetAuthenticationParentWindow", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, /* size and position */ + NULL, /* hWndParent */ + NULL, /* hMenu */ + hModule, + NULL); /* lpParam */ + THROW_LAST_ERROR_IF_NULL_MSG(hModule, "Failed to create authentication parent window"); + + // Best effort only + SetForegroundWindow(m_windowHandle); + + m_windowThreadId = GetCurrentThreadId(); + + // Set window ready event + waitForWindowReady.SetEvent(); + + // Message loop + MSG msg; + BOOL getMsgResult; + while ((getMsgResult = GetMessage(&msg, NULL, 0, 0)) != 0) + { + if (m_terminateWindowThread || getMsgResult == -1) + { + return; + } + else + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + } + }); + + THROW_HR_IF_MSG(APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED, !waitForWindowReady.wait(10000), "Creating authentication parent window timed out"); + } + + LRESULT __stdcall AuthenticationWindowBase::WindowProcessFunction(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + switch (uMsg) + { + case WM_ENDSESSION: + case WM_CLOSE: + DestroyWindow(hWnd); + break; + case WM_DESTROY: + PostQuitMessage(0); + break; + default: + return DefWindowProc(hWnd, uMsg, wParam, lParam); + } + + return 0; + } + + std::string_view AuthenticationTypeToString(AuthenticationType in) + { + switch (in) + { + case AuthenticationType::None: + return "none"sv; + case AuthenticationType::MicrosoftEntraId: + return "microsoftEntraId"sv; + } + + return "unknown"sv; + } + + AuthenticationType ConvertToAuthenticationType(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + AuthenticationType result = AuthenticationType::Unknown; + + if (inStrLower == "none") + { + result = AuthenticationType::None; + } + else if (inStrLower == "microsoftentraid") + { + result = AuthenticationType::MicrosoftEntraId; + } + + return result; + } + + std::string_view AuthenticationModeToString(AuthenticationMode in) + { + switch (in) + { + case AuthenticationMode::Silent: + return "silent"sv; + case AuthenticationMode::SilentPreferred: + return "silentPreferred"sv; + case AuthenticationMode::Interactive: + return "interactive"sv; + } + + return "unknown"sv; + } + + AuthenticationMode ConvertToAuthenticationMode(std::string_view in) + { + std::string inStrLower = Utility::ToLower(in); + AuthenticationMode result = AuthenticationMode::Unknown; + + if (inStrLower == "silent") + { + result = AuthenticationMode::Silent; + } + else if (inStrLower == "silentpreferred") + { + result = AuthenticationMode::SilentPreferred; + } + else if (inStrLower == "interactive") + { + result = AuthenticationMode::Interactive; + } + + return result; + } + + std::string AppInstaller::Authentication::CreateBearerToken(std::string rawToken) + { + return c_BearerTokenPrefix + rawToken; + } +} diff --git a/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.cpp b/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.cpp @@ -0,0 +1,287 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include <AppInstallerErrors.h> +#include <AppInstallerStrings.h> +#include <AppInstallerLogging.h> +#include <AppInstallerRuntime.h> +#include "WebAccountManagerAuthenticator.h" + +using namespace std::string_view_literals; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Security::Authentication::Web::Core; +using namespace winrt::Windows::Security::Credentials; + +namespace AppInstaller::Authentication +{ + namespace + { + constexpr std::wstring_view s_MicrosoftEntraIdProviderId = L"https://login.microsoft.com"sv; + constexpr std::wstring_view s_MicrosoftEntraIdAuthority = L"organizations"sv; + constexpr std::wstring_view s_MicrosoftEntraIdClientId = L"7b8ea11a-7f45-4b3a-ab51-794d5863af15"sv; + constexpr std::wstring_view s_MicrosoftEntraIdResourceHeader = L"resource"sv; + constexpr std::wstring_view s_MicrosoftEntraIdLoginHintHeader = L"LoginHint"sv; + } + + WebAccountManagerAuthenticator::WebAccountManagerAuthenticator(AuthenticationInfo info, AuthenticationArguments args) : m_authInfo(std::move(info)), m_authArgs(std::move(args)) + { + // WebAccountManager manages accounts as user. When running as system, it can only retrieve domain joined device token. + // This is very rare scenario for rest source to require a device token. And it needs approval to provision winget client registration. + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), Runtime::IsRunningAsSystem()); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !m_authInfo.ValidateIntegrity()); + THROW_HR_IF(E_UNEXPECTED, m_authArgs.Mode == AuthenticationMode::Unknown); + + if (m_authInfo.Type == AuthenticationType::MicrosoftEntraId) + { + m_webAccountProvider = WebAuthenticationCoreManager::FindAccountProviderAsync(s_MicrosoftEntraIdProviderId, s_MicrosoftEntraIdAuthority).get(); + THROW_HR_IF_MSG(E_UNEXPECTED, !m_webAccountProvider, "Authentication Provider not found for Microsoft Entra Id"); + AICLI_LOG(Core, Info, << "WebAccountManagerAuthenticator created for MicrosoftEntraId. Resource: " << m_authInfo.MicrosoftEntraIdInfo->Resource << ", Scope: " << m_authInfo.MicrosoftEntraIdInfo->Scope); + } + else if (m_authInfo.Type == AuthenticationType::None) + { + THROW_HR_MSG(E_UNEXPECTED, "WebAccountManagerAuthenticator initialized with authentication type none"); + } + else + { + THROW_HR(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED); + } + } + + // WebAccountManager manages token and cache at OS level. + // So for each authentication request, we call WebAccountManager api to retrieve token. + // We do not need to implement own cache logic. + AuthenticationResult WebAccountManagerAuthenticator::AuthenticateForToken() + { + std::lock_guard<std::mutex> lock{ m_authLock }; + + AICLI_LOG(Core, Info, << "Started WebAccountManagerAuthenticator::AuthenticateForToken."); + + AuthenticationResult result; + + if (!m_authenticatedAccount) + { + // This is the first time invocation or previous authentication failed + + // Find the account to use if user provided account name and the account is signed in before. Best effort only. + WebAccount webAccount = nullptr; + if (!m_authArgs.AuthenticationAccount.empty()) + { + webAccount = FindWebAccount(m_authArgs.AuthenticationAccount); + } + + if (m_authArgs.Mode == AuthenticationMode::Interactive) + { + result = GetToken(webAccount, true); + } + else if (m_authArgs.Mode == AuthenticationMode::SilentPreferred) + { + result = GetTokenSilent(webAccount); + if (FAILED(result.Status)) + { + result = GetToken(webAccount); + } + } + else if (m_authArgs.Mode == AuthenticationMode::Silent) + { + result = GetTokenSilent(webAccount); + } + } + else + { + // Previous authentication successful. Just retrieve the token with the authenticated account. + // In rare cases silent flow fails, use interactive flow. + result = GetTokenSilent(m_authenticatedAccount); + if (FAILED(result.Status) && m_authArgs.Mode != AuthenticationMode::Silent) + { + result = GetToken(m_authenticatedAccount); + } + } + + AICLI_LOG(Core, Info, << "Finished WebAccountManagerAuthenticator::AuthenticateForToken. Result: " << result.Status); + + return result; + } + + WebAccount WebAccountManagerAuthenticator::FindWebAccount(std::string_view accountName) + { + AICLI_LOG(Core, Info, << "FindWebAccount called. Desired Account: " << accountName); + + WebAccount result = nullptr; + + if (m_authInfo.Type == AuthenticationType::MicrosoftEntraId) + { + auto findAccountsResult = WebAuthenticationCoreManager::FindAllAccountsAsync(m_webAccountProvider, s_MicrosoftEntraIdClientId).get(); + if (findAccountsResult.Status() == FindAllWebAccountsStatus::Success) + { + for (auto const& account : findAccountsResult.Accounts()) + { + if (Utility::CaseInsensitiveEquals(accountName, Utility::ConvertToUTF8(account.UserName()))) + { + result = account; + break; + } + } + } + else + { + AICLI_LOG(Core, Warning, << "FindAllAccountsAsync failed. Status: " << findAccountsResult.Status()); + auto providerError = findAccountsResult.ProviderError(); + if (providerError) + { + AICLI_LOG(Core, Warning, + << "FindAllAccountsAsync Provider Error. ErrorCode: " << providerError.ErrorCode() + << ", Message: " << Utility::ConvertToUTF8(providerError.ErrorMessage())); + } + } + } + + AICLI_LOG(Core, Info, << "FindWebAccount result: " << ((result != nullptr) ? "found" : "not found")); + + return result; + } + + WebTokenRequest WebAccountManagerAuthenticator::CreateTokenRequest(bool forceInteractive) + { + WebTokenRequest request = nullptr; + + if (m_authInfo.Type == AuthenticationType::MicrosoftEntraId) + { + request = WebTokenRequest + { + m_webAccountProvider, + Utility::ConvertToUTF16(m_authInfo.MicrosoftEntraIdInfo->Scope), + s_MicrosoftEntraIdClientId, + forceInteractive ? WebTokenRequestPromptType::ForceAuthentication : WebTokenRequestPromptType::Default + }; + + request.Properties().Insert(s_MicrosoftEntraIdResourceHeader, Utility::ConvertToUTF16(m_authInfo.MicrosoftEntraIdInfo->Resource)); + if (!m_authArgs.AuthenticationAccount.empty()) + { + request.Properties().Insert(s_MicrosoftEntraIdLoginHintHeader, Utility::ConvertToUTF16(m_authArgs.AuthenticationAccount)); + } + } + + return request; + } + + AuthenticationResult WebAccountManagerAuthenticator::GetToken(WebAccount webAccount, bool forceInteractive) + { + AICLI_LOG(Core, Info, << "Started GetToken. ForceInteractive: " << forceInteractive); + + auto request = CreateTokenRequest(forceInteractive); + if (!request) + { + AICLI_LOG(Core, Error, << "CreateTokenRequest returned empty request"); + return {}; + } + + IAsyncOperation<WebTokenRequestResult> requestOperation; + constexpr winrt::guid iidAsyncRequestResult{ winrt::guid_of<IAsyncOperation<WebTokenRequestResult>>() }; + auto authManagerFactory = winrt::get_activation_factory<WebAuthenticationCoreManager>(); + winrt::com_ptr<IWebAuthenticationCoreManagerInterop> authManagerInterop{ authManagerFactory.as<IWebAuthenticationCoreManagerInterop>() }; + + HRESULT requestOperationResult = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED; + AuthenticationWindowBase parentWindow; + if (webAccount) + { + requestOperationResult = authManagerInterop->RequestTokenWithWebAccountForWindowAsync( + parentWindow.GetHandle(), + request.as<::IInspectable>().get(), + webAccount.as<::IInspectable>().get(), + iidAsyncRequestResult, + reinterpret_cast<void**>(&requestOperation)); + } + else + { + requestOperationResult = authManagerInterop->RequestTokenForWindowAsync( + parentWindow.GetHandle(), + request.as<::IInspectable>().get(), + iidAsyncRequestResult, + reinterpret_cast<void**>(&requestOperation)); + } + + if (FAILED(requestOperationResult)) + { + AICLI_LOG(Core, Error, << "RequestTokenForWindowAsync failed. Result: " << requestOperationResult); + return {}; + } + + return HandleGetTokenResult(requestOperation.get()); + } + + AuthenticationResult WebAccountManagerAuthenticator::GetTokenSilent(WebAccount webAccount) + { + AICLI_LOG(Core, Info, << "Started GetTokenSilent."); + + auto request = CreateTokenRequest(false); + if (!request) + { + AICLI_LOG(Core, Error, << "CreateTokenRequest returned empty request"); + return {}; + } + + return HandleGetTokenResult(WebAuthenticationCoreManager::GetTokenSilentlyAsync(request).get()); + } + + AuthenticationResult WebAccountManagerAuthenticator::HandleGetTokenResult(WebTokenRequestResult requestResult) + { + AuthenticationResult result; + + if (!requestResult) + { + AICLI_LOG(Core, Error, << "WebTokenRequestResult is null"); + return result; + } + + if (requestResult.ResponseStatus() == WebTokenRequestStatus::Success) + { + auto responseData = requestResult.ResponseData().GetAt(0); + auto authenticatedAccount = responseData.WebAccount(); + + // Check token's corresponding account matches user input if applicable. + if (m_authArgs.AuthenticationAccount.empty() || Utility::CaseInsensitiveEquals(m_authArgs.AuthenticationAccount, Utility::ConvertToUTF8(authenticatedAccount.UserName()))) + { + result.Status = S_OK; + result.Token = Utility::ConvertToUTF8(responseData.Token()); + // Assign authenticated account for future token retrieval. + m_authenticatedAccount = authenticatedAccount; + AICLI_LOG(Core, Info, << "Authentication success"); + } + else + { + AICLI_LOG(Core, Error, << "Authentication success. But the authenticated account is not the desired one."); + result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT; + } + } + else if (requestResult.ResponseStatus() == WebTokenRequestStatus::AccountSwitch) + { + AICLI_LOG(Core, Error, << "Authentication failed. The authenticated account is not the desired one."); + result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT; + } + else if (requestResult.ResponseStatus() == WebTokenRequestStatus::ProviderError || + requestResult.ResponseStatus() == WebTokenRequestStatus::AccountProviderNotAvailable) + { + AICLI_LOG(Core, Error, << "Authentication failed. Provider failed."); + auto responseError = requestResult.ResponseError(); + if (responseError) + { + AICLI_LOG(Core, Error, << "Provider Error. Code: " << responseError.ErrorCode() << ", Message: " << Utility::ConvertToUTF8(responseError.ErrorMessage())); + } + result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED; + } + else if (requestResult.ResponseStatus() == WebTokenRequestStatus::UserCancel) + { + AICLI_LOG(Core, Error, << "Authentication failed. User cancelled."); + result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_CANCELLED_BY_USER; + } + else if (requestResult.ResponseStatus() == WebTokenRequestStatus::UserInteractionRequired) + { + AICLI_LOG(Core, Error, << "Authentication failed. Interactive authentication required."); + result.Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_INTERACTIVE_REQUIRED; + } + + AICLI_LOG(Core, Info, << "HandleGetTokenResult Result: " << result.Status); + + return result; + } +} diff --git a/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.h b/src/AppInstallerCommonCore/Authentication/WebAccountManagerAuthenticator.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <atomic> +#include "Public/winget/Authentication.h" +#include <winrt/Windows.Security.Authentication.Web.Core.h> +#include <winrt/Windows.Security.Credentials.h> + +namespace AppInstaller::Authentication +{ + struct WebAccountManagerAuthenticator : public IAuthenticationProvider + { + WebAccountManagerAuthenticator(AuthenticationInfo info, AuthenticationArguments args); + + AuthenticationResult AuthenticateForToken(); + + private: + AuthenticationInfo m_authInfo; + AuthenticationArguments m_authArgs; + winrt::Windows::Security::Credentials::WebAccountProvider m_webAccountProvider = nullptr; + winrt::Windows::Security::Credentials::WebAccount m_authenticatedAccount = nullptr; + std::mutex m_authLock; + + winrt::Windows::Security::Credentials::WebAccount FindWebAccount(std::string_view accountName); + winrt::Windows::Security::Authentication::Web::Core::WebTokenRequest CreateTokenRequest(bool forceInteractive); + AuthenticationResult GetToken(winrt::Windows::Security::Credentials::WebAccount webAccount, bool forceInteractive = false); + AuthenticationResult GetTokenSilent(winrt::Windows::Security::Credentials::WebAccount webAccount); + AuthenticationResult HandleGetTokenResult(winrt::Windows::Security::Authentication::Web::Core::WebTokenRequestResult requestResult); + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Authentication.h b/src/AppInstallerCommonCore/Public/winget/Authentication.h @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <string> +#include <string_view> +#include <optional> +#include "AppInstallerErrors.h" + +namespace AppInstaller::Authentication +{ + // The authentication type supported + enum class AuthenticationType + { + Unknown, + None, + MicrosoftEntraId, + }; + + std::string_view AuthenticationTypeToString(AuthenticationType in); + AuthenticationType ConvertToAuthenticationType(std::string_view in); + + // The authentication modes + enum class AuthenticationMode + { + Unknown, + + // Always do interactive authentication on first request, following requests may use cached result. + Interactive, + + // Try silent flow first. If failed, use interactive flow. + SilentPreferred, + + // Only do silent flow. If failed, the authentication failed. + Silent, + }; + + std::string_view AuthenticationModeToString(AuthenticationMode in); + AuthenticationMode ConvertToAuthenticationMode(std::string_view in); + + // Authentication info for Microsoft Entra Id authentication; + struct MicrosoftEntraIdAuthenticationInfo + { + // Resource is required + std::string Resource; + + // Scope is optional + std::string Scope; + }; + + // Authentication info struct used to initialize Authenticator, this is from source information. + struct AuthenticationInfo + { + AuthenticationType Type = AuthenticationType::None; + std::optional<MicrosoftEntraIdAuthenticationInfo> MicrosoftEntraIdInfo; + + // Validates data integrity against known authentication type. + bool ValidateIntegrity(); + }; + + // Authentication arguments struct used to initialize Authenticator, this is from user input. + struct AuthenticationArguments + { + AuthenticationMode Mode = AuthenticationMode::Unknown; + + // Optional. If set, the value will be used to acquire the specific account and also be validated with authentication result. + std::string AuthenticationAccount; + }; + + // The authentication result + struct AuthenticationResult + { + // Default to failed. S_OK on authentication success. + HRESULT Status = APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED; + + // The token result on authentication success. + std::string Token; + }; + + // Individual authentication provider interface. Authenticator will delegate authentication to authentication provider. + struct IAuthenticationProvider + { + virtual ~IAuthenticationProvider() = default; + + // Authenticate and return string result. + virtual AuthenticationResult AuthenticateForToken() = 0; + }; + + // The public facing authenticator + struct Authenticator + { + Authenticator(AuthenticationInfo info, AuthenticationArguments args); + + // Authenticate and return string result. + AuthenticationResult AuthenticateForToken(); + + private: + std::unique_ptr<IAuthenticationProvider> m_authProvider; + }; + + // This is the class for authentication window parent window. + // When authenticating interactively, some api needs handle to a parent window. + // This class will initiate a new thread and create a hidden window but with foreground priority (best effort). + // This class handles terminating the window thread on destruction + struct AuthenticationWindowBase + { + // The constructor will initiate the authentication parent window and thread + AuthenticationWindowBase(); + + AuthenticationWindowBase(const AuthenticationWindowBase&) = delete; + AuthenticationWindowBase& operator=(const AuthenticationWindowBase&) = delete; + + AuthenticationWindowBase(AuthenticationWindowBase&&) = delete; + AuthenticationWindowBase& operator=(AuthenticationWindowBase&&) = delete; + + // Get the native window handle + HWND GetHandle(); + + // The destructor will terminate the authentication parent window and thread + ~AuthenticationWindowBase(); + + private: + HWND m_windowHandle; + DWORD m_windowThreadId; + std::thread m_windowThread; + // In case PostThreadMessage() fails, let window thread exit immediately. + std::atomic<bool> m_terminateWindowThread = false; + + void InitializeWindowThread(); + static LRESULT WINAPI WindowProcessFunction(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + }; + + // Create bearer token from a raw token + std::string CreateBearerToken(std::string rawToken); +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -92,6 +92,8 @@ #include <winrt/Windows.Web.Http.Filters.h> #include <winrt/Windows.Globalization.h> #include <winrt/Windows.ApplicationModel.Store.Preview.InstallControl.h> +#include <winrt/Windows.Security.Authentication.Web.Core.h> +#include <winrt/Windows.Security.Credentials.h> #endif @@ -103,3 +105,4 @@ #include <shcore.h> #include <AppxPackaging.h> +#include <WebAuthenticationCoreManagerInterop.h> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -430,6 +430,8 @@ <ClInclude Include="Rest\Schema\1_5\Json\ManifestDeserializer.h" /> <ClInclude Include="Rest\Schema\1_6\Interface.h" /> <ClInclude Include="Rest\Schema\1_6\Json\ManifestDeserializer.h" /> + <ClInclude Include="Rest\Schema\1_7\Interface.h" /> + <ClInclude Include="Rest\Schema\AuthenticationInfoParser.h" /> <ClInclude Include="Rest\Schema\CommonRestConstants.h" /> <ClInclude Include="Rest\Schema\HttpClientHelper.h" /> <ClInclude Include="Rest\Schema\InformationResponseDeserializer.h" /> @@ -511,6 +513,8 @@ <ClCompile Include="Rest\Schema\1_5\RestInterface_1_5.cpp" /> <ClCompile Include="Rest\Schema\1_6\Json\ManifestDeserializer_1_6.cpp" /> <ClCompile Include="Rest\Schema\1_6\RestInterface_1_6.cpp" /> + <ClCompile Include="Rest\Schema\1_7\RestInterface_1_7.cpp" /> + <ClCompile Include="Rest\Schema\AuthenticationInfoParser.cpp" /> <ClCompile Include="Rest\Schema\HttpClientHelper.cpp" /> <ClCompile Include="Rest\Schema\InformationResponseDeserializer.cpp" /> <ClCompile Include="Rest\Schema\RestHelper.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -94,6 +94,9 @@ <Filter Include="Microsoft\Schema\Checkpoint_1_0"> <UniqueIdentifier>{a3f9c7ed-f487-40d6-9ee7-e9a052e55c29}</UniqueIdentifier> </Filter> + <Filter Include="Rest\Schema\1_7"> + <UniqueIdentifier>{f42acfce-4fc0-435b-a9a2-bf5528aecbcc}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -393,6 +396,12 @@ <ClInclude Include="SourceUpdateChecks.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Rest\Schema\1_7\Interface.h"> + <Filter>Rest\Schema\1_7</Filter> + </ClInclude> + <ClInclude Include="Rest\Schema\AuthenticationInfoParser.h"> + <Filter>Rest\Schema</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -617,6 +626,12 @@ <ClCompile Include="Microsoft\Schema\Pinning_1_0\PinningIndexInterface_1_0.cpp"> <Filter>Microsoft\Schema\Pinning_1_0</Filter> </ClCompile> + <ClCompile Include="Rest\Schema\1_7\RestInterface_1_7.cpp"> + <Filter>Rest\Schema\1_7</Filter> + </ClCompile> + <ClCompile Include="Rest\Schema\AuthenticationInfoParser.cpp"> + <Filter>Rest\Schema</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/ISource.h b/src/AppInstallerRepositoryCore/ISource.h @@ -68,6 +68,8 @@ namespace AppInstaller::Repository // Internal interface to represents source information; basically SourceDetails but with methods to enable differential behaviors. struct ISourceReference { + virtual ~ISourceReference() = default; + // Gets the source's identifier; a unique identifier independent of the name // that will not change between a remove/add or between additional adds. // Must be suitable for filesystem names unless the source is internal to winget, @@ -86,6 +88,9 @@ namespace AppInstaller::Repository // Set caller. virtual void SetCaller(std::string) {} + // Set authentication arguments. + virtual void SetAuthenticationArguments(Authentication::AuthenticationArguments) {} + // Determine if the source needs to be updated before being opened. virtual bool ShouldUpdateBeforeOpen(const std::optional<TimeSpan>&) { return false; } diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -5,6 +5,7 @@ #include <winget/PackageTrackingCatalog.h> #include <AppInstallerProgress.h> #include <winget/Certificates.h> +#include <winget/Authentication.h> #include <chrono> #include <filesystem> @@ -169,6 +170,9 @@ namespace AppInstaller::Repository // Required query parameters in get manifest request. std::vector<std::string> RequiredQueryParameters; + + // Source authentication info. + Authentication::AuthenticationInfo Authentication; }; // Allows calling code to inquire about specific features of an ISource implementation. @@ -235,12 +239,15 @@ namespace AppInstaller::Repository // Returns true if the origin type can contain available packages. bool ContainsAvailablePackages() const; - // Set custom header. + // Set custom header. Must be set before Open to have effect. bool SetCustomHeader(std::optional<std::string> header); - // Set caller. + // Set caller. Must be set before Open to have effect. void SetCaller(std::string caller); + // Set authentication arguments. Must be set before Open to have effect. + void SetAuthenticationArguments(Authentication::AuthenticationArguments args); + // Set background update check interval. void SetBackgroundUpdateInterval(TimeSpan interval); diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -547,6 +547,14 @@ namespace AppInstaller::Repository } } + void Source::SetAuthenticationArguments(Authentication::AuthenticationArguments args) + { + for (auto& sourceReference : m_sourceReferences) + { + sourceReference->SetAuthenticationArguments(args); + } + } + void Source::SetBackgroundUpdateInterval(TimeSpan interval) { m_backgroundUpdateInterval = interval; diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.cpp b/src/AppInstallerRepositoryCore/Rest/RestClient.cpp @@ -7,6 +7,7 @@ #include "Rest/Schema/1_4/Interface.h" #include "Rest/Schema/1_5/Interface.h" #include "Rest/Schema/1_6/Interface.h" +#include "Rest/Schema/1_7/Interface.h" #include "Rest/Schema/HttpClientHelper.h" #include <winget/JsonUtil.h> #include "Rest/Schema/InformationResponseDeserializer.h" @@ -20,15 +21,16 @@ using namespace AppInstaller::Utility; namespace AppInstaller::Repository::Rest { // Supported versions - std::set<Version> WingetSupportedContracts = { Version_1_0_0, Version_1_1_0, Version_1_4_0, Version_1_5_0, Version_1_6_0 }; + std::set<Version> WingetSupportedContracts = { Version_1_0_0, Version_1_1_0, Version_1_4_0, Version_1_5_0, Version_1_6_0, Version_1_7_0 }; constexpr std::string_view WindowsPackageManagerHeader = "Windows-Package-Manager"sv; constexpr size_t WindowsPackageManagerHeaderMaxLength = 1024; - namespace { - std::unordered_map<utility::string_t, utility::string_t> GetHeaders(std::optional<std::string> customHeader, std::string_view caller) + namespace + { + HttpClientHelper::HttpRequestHeaders GetHeaders(std::optional<std::string> customHeader, std::string_view caller) { - std::unordered_map<utility::string_t, utility::string_t> headers; + HttpClientHelper::HttpRequestHeaders headers; if (customHeader) { @@ -52,6 +54,21 @@ namespace AppInstaller::Repository::Rest return headers; } + + IRestClient::Information GetInformationInternal( + const utility::string_t& restApi, const HttpClientHelper::HttpRequestHeaders& additionalHeaders, const HttpClientHelper& clientHelper) + { + // Call information endpoint + utility::string_t endpoint = RestHelper::AppendPathToUri(restApi, JSON::GetUtilityString(InformationGetEndpoint)); + std::optional<web::json::value> response = clientHelper.HandleGet(endpoint, additionalHeaders); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !response); + + InformationResponseDeserializer responseDeserializer; + IRestClient::Information information = responseDeserializer.Deserialize(response.value()); + + return information; + } } RestClient::RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface, std::string sourceIdentifier) @@ -79,21 +96,6 @@ namespace AppInstaller::Repository::Rest return m_interface->GetSourceInformation(); } - IRestClient::Information RestClient::GetInformation( - const utility::string_t& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const HttpClientHelper& clientHelper) - { - // Call information endpoint - utility::string_t endpoint = RestHelper::AppendPathToUri(restApi, JSON::GetUtilityString(InformationGetEndpoint)); - std::optional<web::json::value> response = clientHelper.HandleGet(endpoint, additionalHeaders); - - THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !response); - - InformationResponseDeserializer responseDeserializer; - IRestClient::Information information = responseDeserializer.Deserialize(response.value()); - - return information; - } - std::optional<Version> RestClient::GetLatestCommonVersion( const std::vector<std::string>& serverSupportedVersions, const std::set<Version>& wingetSupportedVersions) @@ -131,10 +133,21 @@ namespace AppInstaller::Repository::Rest return *commonVersions.rbegin(); } + Schema::IRestClient::Information RestClient::GetInformation(const std::string& restApi, std::optional<std::string> customHeader, std::string_view caller, const Schema::HttpClientHelper& helper) + { + utility::string_t restEndpoint = RestHelper::GetRestAPIBaseUri(restApi); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(restEndpoint)); + + auto headers = GetHeaders(customHeader, caller); + + return GetInformationInternal(restEndpoint, headers, helper); + } + std::unique_ptr<Schema::IRestClient> RestClient::GetSupportedInterface( const std::string& api, - const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, + const HttpClientHelper::HttpRequestHeaders& additionalHeaders, const IRestClient::Information& information, + const Authentication::AuthenticationArguments& authArgs, const Version& version) { if (version == Version_1_0_0) @@ -157,22 +170,26 @@ namespace AppInstaller::Repository::Rest { return std::make_unique<Schema::V1_6::Interface>(api, information, additionalHeaders); } + else if (version == Version_1_7_0) + { + return std::make_unique<Schema::V1_7::Interface>(api, information, additionalHeaders, authArgs); + } THROW_HR(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_VERSION); } - RestClient RestClient::Create(const std::string& restApi, std::optional<std::string> customHeader, std::string_view caller, const HttpClientHelper& helper) + RestClient RestClient::Create(const std::string& restApi, std::optional<std::string> customHeader, std::string_view caller, const Authentication::AuthenticationArguments& authArgs, const HttpClientHelper& helper) { utility::string_t restEndpoint = RestHelper::GetRestAPIBaseUri(restApi); THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTSOURCE_INVALID_URL, !RestHelper::IsValidUri(restEndpoint)); auto headers = GetHeaders(customHeader, caller); - IRestClient::Information information = GetInformation(restEndpoint, headers, helper); + IRestClient::Information information = GetInformationInternal(restEndpoint, headers, helper); std::optional<Version> latestCommonVersion = GetLatestCommonVersion(information.ServerSupportedVersions, WingetSupportedContracts); THROW_HR_IF(APPINSTALLER_CLI_ERROR_UNSUPPORTED_RESTSOURCE, !latestCommonVersion); - std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), headers, information, latestCommonVersion.value()); + std::unique_ptr<Schema::IRestClient> supportedInterface = GetSupportedInterface(utility::conversions::to_utf8string(restEndpoint), headers, information, authArgs, latestCommonVersion.value()); return RestClient{ std::move(supportedInterface), information.SourceIdentifier }; } } diff --git a/src/AppInstallerRepositoryCore/Rest/RestClient.h b/src/AppInstallerRepositoryCore/Rest/RestClient.h @@ -29,11 +29,13 @@ namespace AppInstaller::Repository::Rest static std::optional<AppInstaller::Utility::Version> GetLatestCommonVersion(const std::vector<std::string>& serverSupportedVersions, const std::set<AppInstaller::Utility::Version>& wingetSupportedVersions); - static Schema::IRestClient::Information GetInformation(const utility::string_t& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const Schema::HttpClientHelper& httpClientHelper); + // Responsible for getting the source information contracts with minimal validation. Does not try to create a rest interface out of it. + static Schema::IRestClient::Information GetInformation(const std::string& restApi, std::optional<std::string> customHeader, std::string_view caller, const Schema::HttpClientHelper& helper = {}); - static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, const Schema::IRestClient::Information& information, const AppInstaller::Utility::Version& version); + static std::unique_ptr<Schema::IRestClient> GetSupportedInterface(const std::string& restApi, const Schema::HttpClientHelper::HttpRequestHeaders& additionalHeaders, const Schema::IRestClient::Information& information, const Authentication::AuthenticationArguments& authArgs, const AppInstaller::Utility::Version& version); - static RestClient Create(const std::string& restApi, std::optional<std::string> customHeader, std::string_view caller, const Schema::HttpClientHelper& helper = {}); + // Creates the rest client. Full validation performed (just as opening the source) + static RestClient Create(const std::string& restApi, std::optional<std::string> customHeader, std::string_view caller, const Authentication::AuthenticationArguments& authArgs = {}, const Schema::HttpClientHelper& helper = {}); private: RestClient(std::unique_ptr<Schema::IRestClient> supportedInterface, std::string sourceIdentifier); diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp @@ -42,10 +42,15 @@ namespace AppInstaller::Repository::Rest m_caller = std::move(caller); } + void SetAuthenticationArguments(Authentication::AuthenticationArguments authArgs) override + { + m_authArgs = std::move(authArgs); + } + std::shared_ptr<ISource> Open(IProgressCallback&) override { Initialize(); - RestClient restClient = RestClient::Create(m_details.Arg, m_customHeader, m_caller, m_httpClientHelper); + RestClient restClient = RestClient::Create(m_details.Arg, m_customHeader, m_caller, m_authArgs, m_httpClientHelper); return std::make_shared<RestSource>(m_details, m_information, std::move(restClient)); } @@ -56,11 +61,10 @@ namespace AppInstaller::Repository::Rest [&]() { m_httpClientHelper.SetPinningConfiguration(m_details.CertificatePinningConfiguration); - RestClient restClient = RestClient::Create(m_details.Arg, m_customHeader, m_caller, m_httpClientHelper); + auto sourceInformation = RestClient::GetInformation(m_details.Arg, m_customHeader, m_caller, m_httpClientHelper); - m_details.Identifier = restClient.GetSourceIdentifier(); + m_details.Identifier = sourceInformation.SourceIdentifier; - const auto& sourceInformation = restClient.GetSourceInformation(); m_information.UnsupportedPackageMatchFields = sourceInformation.UnsupportedPackageMatchFields; m_information.RequiredPackageMatchFields = sourceInformation.RequiredPackageMatchFields; m_information.UnsupportedQueryParameters = sourceInformation.UnsupportedQueryParameters; @@ -71,6 +75,8 @@ namespace AppInstaller::Repository::Rest { m_information.SourceAgreements.emplace_back(agreement.Label, agreement.Text, agreement.Url); } + + m_information.Authentication = sourceInformation.Authentication; }); } @@ -79,6 +85,7 @@ namespace AppInstaller::Repository::Rest SourceInformation m_information; std::optional<std::string> m_customHeader; std::string m_caller; + Authentication::AuthenticationArguments m_authArgs; std::once_flag m_initializeFlag; }; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/Interface.h @@ -38,7 +38,10 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 virtual SearchResult GetSearchResult(const web::json::value& searchResponseObject) const; virtual std::vector<Manifest::Manifest> GetParsedManifests(const web::json::value& manifestsResponseObject) const; - std::unordered_map<utility::string_t, utility::string_t> m_requiredRestApiHeaders; + // Gets auth headers if source requires authentication for access. + virtual HttpClientHelper::HttpRequestHeaders GetAuthHeaders() const; + + HttpClientHelper::HttpRequestHeaders m_requiredRestApiHeaders; private: std::string m_restApiUri; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_0/RestInterface_1_0.cpp @@ -73,7 +73,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 { SearchResult results; utility::string_t continuationToken; - std::unordered_map<utility::string_t, utility::string_t> searchHeaders = m_requiredRestApiHeaders; + HttpClientHelper::HttpRequestHeaders searchHeaders = m_requiredRestApiHeaders; do { if (!continuationToken.empty()) @@ -82,7 +82,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 searchHeaders.insert_or_assign(AppInstaller::JSON::GetUtilityString(ContinuationToken), continuationToken); } - std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost(m_searchEndpoint, GetValidatedSearchBody(request), searchHeaders); + std::optional<web::json::value> jsonObject = m_httpClientHelper.HandlePost(m_searchEndpoint, GetValidatedSearchBody(request), searchHeaders, GetAuthHeaders()); utility::string_t ct; if (jsonObject) @@ -208,8 +208,8 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 std::vector<Manifest::Manifest> results; utility::string_t continuationToken; - std::unordered_map<utility::string_t, utility::string_t> searchHeaders = m_requiredRestApiHeaders; - std::optional<web::json::value> jsonObject = m_httpClientHelper.HandleGet(GetManifestByVersionEndpoint(m_restApiUri, packageId, validatedParams), m_requiredRestApiHeaders); + HttpClientHelper::HttpRequestHeaders searchHeaders = m_requiredRestApiHeaders; + std::optional<web::json::value> jsonObject = m_httpClientHelper.HandleGet(GetManifestByVersionEndpoint(m_restApiUri, packageId, validatedParams), searchHeaders, GetAuthHeaders()); if (!jsonObject) { @@ -266,4 +266,9 @@ namespace AppInstaller::Repository::Rest::Schema::V1_0 JSON::ManifestJSONParser manifestParser{ GetVersion() }; return manifestParser.Deserialize(manifestsResponseObject); } + + HttpClientHelper::HttpRequestHeaders Interface::GetAuthHeaders() const + { + return {}; + } } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/Interface.h @@ -8,7 +8,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1 // Interface to this schema version exposed through IRestClient. struct Interface : public V1_0::Interface { - Interface(const std::string& restApi, IRestClient::Information information, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); + Interface(const std::string& restApi, IRestClient::Information information, const HttpClientHelper::HttpRequestHeaders& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); Interface(const Interface&) = delete; Interface& operator=(const Interface&) = delete; @@ -31,7 +31,6 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1 PackageMatchField ConvertStringToPackageMatchField(std::string_view field) const; - private: IRestClient::Information m_information; }; } diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_1/RestInterface_1_1.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_1/RestInterface_1_1.cpp @@ -27,7 +27,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_1 Interface::Interface( const std::string& restApi, IRestClient::Information information, - const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, + const HttpClientHelper::HttpRequestHeaders& additionalHeaders, const HttpClientHelper& httpClientHelper) : V1_0::Interface(restApi, httpClientHelper), m_information(std::move(information)) { m_requiredRestApiHeaders[JSON::GetUtilityString(ContractVersion)] = JSON::GetUtilityString(Version_1_1_0.ToString()); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Interface.h @@ -8,7 +8,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_4 // Interface to this schema version exposed through IRestClient. struct Interface : public V1_1::Interface { - Interface(const std::string& restApi, IRestClient::Information information, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); + Interface(const std::string& restApi, IRestClient::Information information, const HttpClientHelper::HttpRequestHeaders& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); Interface(const Interface&) = delete; Interface& operator=(const Interface&) = delete; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/RestInterface_1_4.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/RestInterface_1_4.cpp @@ -12,7 +12,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_4 Interface::Interface( const std::string& restApi, IRestClient::Information information, - const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, + const HttpClientHelper::HttpRequestHeaders& additionalHeaders, const HttpClientHelper& httpClientHelper) : V1_1::Interface(restApi, std::move(information), additionalHeaders, httpClientHelper) { m_requiredRestApiHeaders[JSON::GetUtilityString(ContractVersion)] = JSON::GetUtilityString(Version_1_4_0.ToString()); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_5/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_5/Interface.h @@ -8,7 +8,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_5 // Interface to this schema version exposed through IRestClient. struct Interface : public V1_4::Interface { - Interface(const std::string& restApi, IRestClient::Information information, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); + Interface(const std::string& restApi, IRestClient::Information information, const HttpClientHelper::HttpRequestHeaders& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); Interface(const Interface&) = delete; Interface& operator=(const Interface&) = delete; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_5/RestInterface_1_5.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_5/RestInterface_1_5.cpp @@ -12,7 +12,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_5 Interface::Interface( const std::string& restApi, IRestClient::Information information, - const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, + const HttpClientHelper::HttpRequestHeaders& additionalHeaders, const HttpClientHelper& httpClientHelper) : V1_4::Interface(restApi, std::move(information), additionalHeaders, httpClientHelper) { m_requiredRestApiHeaders[JSON::GetUtilityString(ContractVersion)] = JSON::GetUtilityString(Version_1_5_0.ToString()); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_6/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_6/Interface.h @@ -8,7 +8,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_6 // Interface to this schema version exposed through IRestClient. struct Interface : public V1_5::Interface { - Interface(const std::string& restApi, IRestClient::Information information, const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); + Interface(const std::string& restApi, IRestClient::Information information, const HttpClientHelper::HttpRequestHeaders& additionalHeaders = {}, const HttpClientHelper& httpClientHelper = {}); Interface(const Interface&) = delete; Interface& operator=(const Interface&) = delete; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_6/RestInterface_1_6.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_6/RestInterface_1_6.cpp @@ -12,7 +12,7 @@ namespace AppInstaller::Repository::Rest::Schema::V1_6 Interface::Interface( const std::string& restApi, IRestClient::Information information, - const std::unordered_map<utility::string_t, utility::string_t>& additionalHeaders, + const HttpClientHelper::HttpRequestHeaders& additionalHeaders, const HttpClientHelper& httpClientHelper) : V1_5::Interface(restApi, std::move(information), additionalHeaders, httpClientHelper) { m_requiredRestApiHeaders[JSON::GetUtilityString(ContractVersion)] = JSON::GetUtilityString(Version_1_6_0.ToString()); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_7/Interface.h b/src/AppInstallerRepositoryCore/Rest/Schema/1_7/Interface.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Rest/Schema/1_6/Interface.h" + +namespace AppInstaller::Repository::Rest::Schema::V1_7 +{ + // Interface to this schema version exposed through IRestClient. + struct Interface : public V1_6::Interface + { + Interface(const std::string& restApi, IRestClient::Information information, const HttpClientHelper::HttpRequestHeaders& additionalHeaders = {}, Authentication::AuthenticationArguments authArgs = {}, const HttpClientHelper& httpClientHelper = {}); + + Interface(const Interface&) = delete; + Interface& operator=(const Interface&) = delete; + + Interface(Interface&&) = default; + Interface& operator=(Interface&&) = default; + + Utility::Version GetVersion() const override; + + HttpClientHelper::HttpRequestHeaders GetAuthHeaders() const override; + + protected: + std::unique_ptr<Authentication::Authenticator> m_authenticator; + Authentication::AuthenticationArguments m_authArgs; + }; +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_7/RestInterface_1_7.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_7/RestInterface_1_7.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Rest/Schema/1_7/Interface.h" +#include "Rest/Schema/IRestClient.h" +#include "Rest/Schema/HttpClientHelper.h" +#include "Rest/Schema/CommonRestConstants.h" +#include <winget/JsonUtil.h> + +namespace AppInstaller::Repository::Rest::Schema::V1_7 +{ + Interface::Interface( + const std::string& restApi, + IRestClient::Information information, + const HttpClientHelper::HttpRequestHeaders& additionalHeaders, + Authentication::AuthenticationArguments authArgs, + const HttpClientHelper& httpClientHelper) : V1_6::Interface(restApi, std::move(information), additionalHeaders, httpClientHelper), m_authArgs(std::move(authArgs)) + { + m_requiredRestApiHeaders[JSON::GetUtilityString(ContractVersion)] = JSON::GetUtilityString(Version_1_7_0.ToString()); + + if (m_information.Authentication.Type == Authentication::AuthenticationType::MicrosoftEntraId) + { + AICLI_LOG(Repo, Info, << "Creating authenticator for MicrosoftEntraId authentication. Source Identifier: " << m_information.SourceIdentifier); + m_authenticator = std::make_unique<Authentication::Authenticator>(m_information.Authentication, m_authArgs); + } + else if (m_information.Authentication.Type == Authentication::AuthenticationType::Unknown) + { + AICLI_LOG(Repo, Error, << "Authentication type unknown for rest source. Source Identifier: " << m_information.SourceIdentifier); + THROW_HR(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED); + } + } + + Utility::Version Interface::GetVersion() const + { + return Version_1_7_0; + } + + HttpClientHelper::HttpRequestHeaders Interface::GetAuthHeaders() const + { + HttpClientHelper::HttpRequestHeaders result; + + if (m_information.Authentication.Type == Authentication::AuthenticationType::MicrosoftEntraId) + { + auto authResult = m_authenticator->AuthenticateForToken(); + if (FAILED(authResult.Status)) + { + AICLI_LOG(Repo, Error, << "Authentication failed. Result: " << authResult.Status); + THROW_HR_MSG(authResult.Status, "Failed to authenticate for MicrosoftEntraId"); + } + result.insert_or_assign(web::http::header_names::authorization, JSON::GetUtilityString(Authentication::CreateBearerToken(authResult.Token))); + } + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.cpp @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AuthenticationInfoParser.h" +#include <winget/JsonUtil.h> + +namespace AppInstaller::Repository::Rest::Schema +{ + namespace + { + // Authentication info constants + constexpr std::string_view Authentication = "Authentication"sv; + constexpr std::string_view AuthenticationType = "AuthenticationType"sv; + constexpr std::string_view MicrosoftEntraIdAuthenticationInfo = "MicrosoftEntraIdAuthenticationInfo"sv; + constexpr std::string_view MicrosoftEntraId_Resource = "Resource"sv; + constexpr std::string_view MicrosoftEntraId_Scope = "Scope"sv; + } + + // The authentication info json looks like below: + // "Authentication": { + // "AuthenticationType": "microsoftEntraId", + // "MicrosoftEntraIdAuthenticationInfo" : { + // "Resource": "GUID", + // "Scope" : "test" + // } + // } + Authentication::AuthenticationInfo ParseAuthenticationInfo(const web::json::value& dataObject, std::optional<Manifest::ManifestVer>) + { + auto authenticationObject = JSON::GetJsonValueFromNode(dataObject, JSON::GetUtilityString(Authentication)); + if (!authenticationObject) + { + AICLI_LOG(Repo, Info, << "Authentication node not found. Assuming authentication type none."); + return {}; + } + + const auto& authenticationObjectNode = authenticationObject.value().get(); + if (authenticationObjectNode.is_null()) + { + AICLI_LOG(Repo, Info, << "Authentication node is null. Assuming authentication type none."); + return {}; + } + + Authentication::AuthenticationInfo result; + result.Type = Authentication::AuthenticationType::Unknown; + + auto authenticationTypeString = JSON::GetRawStringValueFromJsonNode(authenticationObjectNode, JSON::GetUtilityString(AuthenticationType)); + // AuthenticationType required if Authentication exists and is not null. + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !JSON::IsValidNonEmptyStringValue(authenticationTypeString)); + result.Type = Authentication::ConvertToAuthenticationType(authenticationTypeString.value()); + + // Parse MicrosoftEntraId info + auto microsoftEntraIdInfoObject = JSON::GetJsonValueFromNode(authenticationObjectNode, JSON::GetUtilityString(MicrosoftEntraIdAuthenticationInfo)); + if (microsoftEntraIdInfoObject) + { + const auto& microsoftEntraIdInfoNode = microsoftEntraIdInfoObject.value().get(); + + Authentication::MicrosoftEntraIdAuthenticationInfo microsoftEntraIdInfo; + + auto resourceString = JSON::GetRawStringValueFromJsonNode(microsoftEntraIdInfoNode, JSON::GetUtilityString(MicrosoftEntraId_Resource)); + // Resource required if MicrosoftEntraIdAuthenticationInfo exists and is not null. + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !JSON::IsValidNonEmptyStringValue(resourceString)); + microsoftEntraIdInfo.Resource = std::move(resourceString.value()); + + auto scopeString = JSON::GetRawStringValueFromJsonNode(microsoftEntraIdInfoNode, JSON::GetUtilityString(MicrosoftEntraId_Scope)); + if (JSON::IsValidNonEmptyStringValue(scopeString)) + { + microsoftEntraIdInfo.Scope = std::move(scopeString.value()); + } + + result.MicrosoftEntraIdInfo = std::move(microsoftEntraIdInfo); + } + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, !result.ValidateIntegrity()); + + return result; + } +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.h b/src/AppInstallerRepositoryCore/Rest/Schema/AuthenticationInfoParser.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <cpprest/json.h> +#include <winget/Authentication.h> +#include <winget/ManifestCommon.h> + +namespace AppInstaller::Repository::Rest::Schema +{ + // Parses AuthenticationInfo from json object. + // This could be used for installer level parsing as well in manifest deserializer (currently not supported, manifestVersion not used). + // The authentication info json looks like below: + // "Authentication": { + // "AuthenticationType": "microsoftEntraId", + // "MicrosoftEntraIdAuthenticationInfo" : { + // "Resource": "GUID", + // "Scope" : "test" + // } + // } + Authentication::AuthenticationInfo ParseAuthenticationInfo(const web::json::value& dataObject, std::optional<Manifest::ManifestVer> manifestVersion = {}); +} diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h b/src/AppInstallerRepositoryCore/Rest/Schema/CommonRestConstants.h @@ -11,6 +11,7 @@ namespace AppInstaller::Repository::Rest::Schema const Utility::Version Version_1_4_0{ "1.4.0" }; const Utility::Version Version_1_5_0{ "1.5.0" }; const Utility::Version Version_1_6_0{ "1.6.0" }; + const Utility::Version Version_1_7_0{ "1.7.0" }; // General API response constants constexpr std::string_view Data = "Data"sv; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp @@ -45,7 +45,10 @@ namespace AppInstaller::Repository::Rest::Schema HttpClientHelper::HttpClientHelper(std::shared_ptr<web::http::http_pipeline_stage> stage) : m_defaultRequestHandlerStage(std::move(stage)) {} pplx::task<web::http::http_response> HttpClientHelper::Post( - const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + const utility::string_t& uri, + const web::json::value& body, + const HttpClientHelper::HttpRequestHeaders& headers, + const HttpClientHelper::HttpRequestHeaders& authHeaders) const { AICLI_LOG(Repo, Info, << "Sending http POST request to: " << utility::conversions::to_utf8string(uri)); web::http::client::http_client client = GetClient(uri); @@ -62,14 +65,23 @@ namespace AppInstaller::Repository::Rest::Schema AICLI_LOG(Repo, Verbose, << "Http POST request details:\n" << utility::conversions::to_utf8string(request.to_string())); + // Add auth headers after logging + for (auto& pair : authHeaders) + { + request.headers().add(pair.first, pair.second); + } + return client.request(request); } std::optional<web::json::value> HttpClientHelper::HandlePost( - const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + const utility::string_t& uri, + const web::json::value& body, + const HttpClientHelper::HttpRequestHeaders& headers, + const HttpClientHelper::HttpRequestHeaders& authHeaders) const { web::http::http_response httpResponse; - HttpClientHelper::Post(uri, body, headers).then([&httpResponse](const web::http::http_response& response) + HttpClientHelper::Post(uri, body, headers, authHeaders).then([&httpResponse](const web::http::http_response& response) { httpResponse = response; }).wait(); @@ -78,7 +90,9 @@ namespace AppInstaller::Repository::Rest::Schema } pplx::task<web::http::http_response> HttpClientHelper::Get( - const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + const utility::string_t& uri, + const HttpClientHelper::HttpRequestHeaders& headers, + const HttpClientHelper::HttpRequestHeaders& authHeaders) const { AICLI_LOG(Repo, Info, << "Sending http GET request to: " << utility::conversions::to_utf8string(uri)); web::http::client::http_client client = GetClient(uri); @@ -94,14 +108,22 @@ namespace AppInstaller::Repository::Rest::Schema AICLI_LOG(Repo, Verbose, << "Http GET request details:\n" << utility::conversions::to_utf8string(request.to_string())); + // Add auth headers after logging + for (auto& pair : authHeaders) + { + request.headers().add(pair.first, pair.second); + } + return client.request(request); } std::optional<web::json::value> HttpClientHelper::HandleGet( - const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers) const + const utility::string_t& uri, + const HttpClientHelper::HttpRequestHeaders& headers, + const HttpClientHelper::HttpRequestHeaders& authHeaders) const { web::http::http_response httpResponse; - Get(uri, headers).then([&httpResponse](const web::http::http_response& response) + Get(uri, headers, authHeaders).then([&httpResponse](const web::http::http_response& response) { httpResponse = response; }).wait(); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h @@ -13,15 +13,17 @@ namespace AppInstaller::Repository::Rest::Schema { struct HttpClientHelper { + using HttpRequestHeaders = std::unordered_map<utility::string_t, utility::string_t>; + HttpClientHelper(std::shared_ptr<web::http::http_pipeline_stage> = {}); - pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t> &headers = {}) const; + pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const HttpRequestHeaders& headers = {}, const HttpRequestHeaders& authHeaders = {}) const; - std::optional<web::json::value> HandlePost(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; + std::optional<web::json::value> HandlePost(const utility::string_t& uri, const web::json::value& body, const HttpRequestHeaders& headers = {}, const HttpRequestHeaders& authHeaders = {}) const; - pplx::task<web::http::http_response> Get(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; + pplx::task<web::http::http_response> Get(const utility::string_t& uri, const HttpRequestHeaders& headers = {}, const HttpRequestHeaders& authHeaders = {}) const; - std::optional<web::json::value> HandleGet(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; + std::optional<web::json::value> HandleGet(const utility::string_t& uri, const HttpRequestHeaders& headers = {}, const HttpRequestHeaders& authHeaders = {}) const; void SetPinningConfiguration(const Certificates::PinningConfiguration& configuration); protected: diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h b/src/AppInstallerRepositoryCore/Rest/Schema/IRestClient.h @@ -5,6 +5,7 @@ #include <winget/Manifest.h> #include <winget/RepositorySearch.h> #include <AppInstallerVersions.h> +#include <winget/Authentication.h> #include <vector> namespace AppInstaller::Repository::Rest::Schema @@ -73,6 +74,7 @@ namespace AppInstaller::Repository::Rest::Schema std::vector<std::string> RequiredPackageMatchFields; std::vector<std::string> UnsupportedQueryParameters; std::vector<std::string> RequiredQueryParameters; + Authentication::AuthenticationInfo Authentication; Information() {} Information(std::string sourceId, std::vector<std::string> versions) diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/InformationResponseDeserializer.cpp @@ -4,6 +4,7 @@ #include "Rest/Schema/IRestClient.h" #include <winget/JsonUtil.h> #include "Rest/Schema/CommonRestConstants.h" +#include "AuthenticationInfoParser.h" #include "InformationResponseDeserializer.h" namespace AppInstaller::Repository::Rest::Schema @@ -123,6 +124,8 @@ namespace AppInstaller::Repository::Rest::Schema info.RequiredQueryParameters = JSON::GetRawStringArrayFromJsonNode(dataValue, JSON::GetUtilityString(RequiredQueryParameters)); info.UnsupportedQueryParameters = JSON::GetRawStringArrayFromJsonNode(dataValue, JSON::GetUtilityString(UnsupportedQueryParameters)); + info.Authentication = ParseAuthenticationInfo(dataValue); + return info; } catch (const std::exception& e) diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp @@ -201,7 +201,13 @@ namespace AppInstaller WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH, "The current client version did not match the client version of the saved state."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE, "The resume state data is invalid."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_CANNOT_OPEN_CHECKPOINT_INDEX, "Unable to open the checkpoint database."), - + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESUME_LIMIT_EXCEEDED, "Exceeded max resume limit."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO, "Invalid authentication info."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED, "Authentication method not supported."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED, "Authentication failed."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_AUTHENTICATION_INTERACTIVE_REQUIRED, "Authentication failed. Interactive authentication required."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_AUTHENTICATION_CANCELLED_BY_USER, "Authentication failed. User cancelled."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT, "Authentication failed. Authenticated account is not the desired account."), // Install errors. WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE, "Application is currently running. Exit the application then try again."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -132,6 +132,13 @@ #define APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE ((HRESULT)0x8A150070) #define APPINSTALLER_CLI_ERROR_CANNOT_OPEN_CHECKPOINT_INDEX ((HRESULT)0x8A150071) #define APPINSTALLER_CLI_ERROR_RESUME_LIMIT_EXCEEDED ((HRESULT)0x8A150072) +#define APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO ((HRESULT)0x8A150073) +#define APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED ((HRESULT)0x8A150074) +#define APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED ((HRESULT)0x8A150075) +#define APPINSTALLER_CLI_ERROR_AUTHENTICATION_INTERACTIVE_REQUIRED ((HRESULT)0x8A150076) +#define APPINSTALLER_CLI_ERROR_AUTHENTICATION_CANCELLED_BY_USER ((HRESULT)0x8A150077) +#define APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT ((HRESULT)0x8A150078) + // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) #define APPINSTALLER_CLI_ERROR_INSTALL_INSTALL_IN_PROGRESS ((HRESULT)0x8A150102) diff --git a/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest b/src/Microsoft.Management.Deployment.InProc/Microsoft.Management.Deployment.InProc.dll.manifest @@ -36,6 +36,10 @@ threadingModel="Both" description="PackageMatchFilter"/> <comClass + clsid="{8D593114-1CF1-43B9-8722-4DBB30103296}" + threadingModel="Both" + description="AuthenticationArguments"/> + <comClass clsid="{80CF9D63-5505-4342-B9B4-BB87895CA8BB}" threadingModel="Both" description="PackageManagerSettings"/> diff --git a/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs b/src/Microsoft.Management.Deployment.Projection/ClassesDefinition.cs @@ -94,6 +94,18 @@ namespace Microsoft.Management.Deployment.Projection } }, + [typeof(AuthenticationArguments)] = new() + { + ProjectedClassType = typeof(AuthenticationArguments), + InterfaceType = typeof(IAuthenticationArguments), + Clsids = new Dictionary<ClsidContext, Guid>() + { + [ClsidContext.InProc] = new Guid("8D593114-1CF1-43B9-8722-4DBB30103296"), + [ClsidContext.OutOfProc] = new Guid("BA580786-BDE3-4F6C-B8F3-44698AC8711A"), + [ClsidContext.OutOfProcDev] = new Guid("6484A61D-50FA-41F0-B71E-F4370C6EB37C"), + } + }, + [typeof(PackageManagerSettings)] = new() { ProjectedClassType = typeof(PackageManagerSettings), diff --git a/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs b/src/Microsoft.Management.Deployment.Projection/WinGetProjectionFactory.cs @@ -31,6 +31,8 @@ namespace Microsoft.Management.Deployment.Projection public PackageMatchFilter CreatePackageMatchFilter() => InstanceInitializer.CreateInstance<PackageMatchFilter>(); + public AuthenticationArguments CreateAuthenticationArguments() => InstanceInitializer.CreateInstance<AuthenticationArguments>(); + public PackageManagerSettings CreatePackageManagerSettings() => InstanceInitializer.CreateInstance<PackageManagerSettings>(); } } diff --git a/src/Microsoft.Management.Deployment/AuthenticationArguments.cpp b/src/Microsoft.Management.Deployment/AuthenticationArguments.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#pragma warning( push ) +#pragma warning ( disable : 4467 6388) +// 6388 Allow CreateInstance. +#include <wil\cppwinrt_wrl.h> +// 4467 Allow use of uuid attribute for com object creation. +#include "AuthenticationArguments.h" +#pragma warning( pop ) +#include "AuthenticationArguments.g.cpp" +#include "Helpers.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + winrt::Microsoft::Management::Deployment::AuthenticationMode AuthenticationArguments::AuthenticationMode() + { + return m_authenticationMode; + } + void AuthenticationArguments::AuthenticationMode(winrt::Microsoft::Management::Deployment::AuthenticationMode const& value) + { + m_authenticationMode = value; + } + hstring AuthenticationArguments::AuthenticationAccount() + { + return winrt::hstring(m_authenticationAccount); + } + void AuthenticationArguments::AuthenticationAccount(hstring const& value) + { + m_authenticationAccount = value; + } + CoCreatableMicrosoftManagementDeploymentClass(AuthenticationArguments); +} diff --git a/src/Microsoft.Management.Deployment/AuthenticationArguments.h b/src/Microsoft.Management.Deployment/AuthenticationArguments.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "AuthenticationArguments.g.h" +#include "Public/ComClsids.h" + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + [uuid(WINGET_OUTOFPROC_COM_CLSID_AuthenticationArguments)] + struct AuthenticationArguments : AuthenticationArgumentsT<AuthenticationArguments> + { + AuthenticationArguments() = default; + + winrt::Microsoft::Management::Deployment::AuthenticationMode AuthenticationMode(); + void AuthenticationMode(winrt::Microsoft::Management::Deployment::AuthenticationMode const& value); + hstring AuthenticationAccount(); + void AuthenticationAccount(hstring const& value); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::AuthenticationMode m_authenticationMode = winrt::Microsoft::Management::Deployment::AuthenticationMode::Silent; + std::wstring m_authenticationAccount = L""; +#endif + }; +} + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) +namespace winrt::Microsoft::Management::Deployment::factory_implementation +{ + struct AuthenticationArguments : AuthenticationArgumentsT<AuthenticationArguments, implementation::AuthenticationArguments> + { + }; +} +#endif+ \ No newline at end of file diff --git a/src/Microsoft.Management.Deployment/AuthenticationInfo.cpp b/src/Microsoft.Management.Deployment/AuthenticationInfo.cpp @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AuthenticationInfo.h" +#include "AuthenticationInfo.g.cpp" +#include "MicrosoftEntraIdAuthenticationInfo.h" +#include "Converters.h" +#include <wil\cppwinrt_wrl.h> + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void AuthenticationInfo::Initialize(::AppInstaller::Authentication::AuthenticationInfo authenticationInfo) + { + m_authenticationType = GetDeploymentAuthenticationType(authenticationInfo.Type); + + if (authenticationInfo.MicrosoftEntraIdInfo.has_value()) + { + auto microsoftEntraIdAuthenticationInfo = winrt::make_self<wil::details::module_count_wrapper<winrt::Microsoft::Management::Deployment::implementation::MicrosoftEntraIdAuthenticationInfo>>(); + microsoftEntraIdAuthenticationInfo->Initialize(authenticationInfo.MicrosoftEntraIdInfo.value()); + m_microsoftEntraIdAuthenticationInfo = *microsoftEntraIdAuthenticationInfo; + } + } + winrt::Microsoft::Management::Deployment::AuthenticationType AuthenticationInfo::AuthenticationType() + { + return m_authenticationType; + } + winrt::Microsoft::Management::Deployment::MicrosoftEntraIdAuthenticationInfo AuthenticationInfo::MicrosoftEntraIdAuthenticationInfo() + { + return m_microsoftEntraIdAuthenticationInfo; + } +} diff --git a/src/Microsoft.Management.Deployment/AuthenticationInfo.h b/src/Microsoft.Management.Deployment/AuthenticationInfo.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "AuthenticationInfo.g.h" +#include <winget/Authentication.h> + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct AuthenticationInfo : AuthenticationInfoT<AuthenticationInfo> + { + AuthenticationInfo() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize(::AppInstaller::Authentication::AuthenticationInfo authenticationInfo); +#endif + + winrt::Microsoft::Management::Deployment::AuthenticationType AuthenticationType(); + winrt::Microsoft::Management::Deployment::MicrosoftEntraIdAuthenticationInfo MicrosoftEntraIdAuthenticationInfo(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + winrt::Microsoft::Management::Deployment::AuthenticationType m_authenticationType = winrt::Microsoft::Management::Deployment::AuthenticationType::None; + winrt::Microsoft::Management::Deployment::MicrosoftEntraIdAuthenticationInfo m_microsoftEntraIdAuthenticationInfo{ nullptr }; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/ComClsids.cpp b/src/Microsoft.Management.Deployment/ComClsids.cpp @@ -13,6 +13,7 @@ #include "PackageMatchFilter.h" #include "PackageManagerSettings.h" #include "DownloadOptions.h" +#include "AuthenticationArguments.h" #pragma warning( pop ) namespace winrt::Microsoft::Management::Deployment @@ -47,6 +48,10 @@ namespace winrt::Microsoft::Management::Deployment { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageMatchFilter); } + else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_AuthenticationArguments)) + { + return __uuidof(winrt::Microsoft::Management::Deployment::implementation::AuthenticationArguments); + } else if (IsEqualCLSID(clsid, WINGET_INPROC_COM_CLSID_PackageManagerSettings)) { return __uuidof(winrt::Microsoft::Management::Deployment::implementation::PackageManagerSettings); diff --git a/src/Microsoft.Management.Deployment/Converters.cpp b/src/Microsoft.Management.Deployment/Converters.cpp @@ -195,6 +195,18 @@ namespace winrt::Microsoft::Management::Deployment::implementation case APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS: resultStatus = winrt::Microsoft::Management::Deployment::FindPackagesResultStatus::InvalidOptions; break; + case APPINSTALLER_CLI_ERROR_INVALID_AUTHENTICATION_INFO: + case APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED: + case APPINSTALLER_CLI_ERROR_AUTHENTICATION_FAILED: + case APPINSTALLER_CLI_ERROR_AUTHENTICATION_INTERACTIVE_REQUIRED: + case APPINSTALLER_CLI_ERROR_AUTHENTICATION_CANCELLED_BY_USER: + case APPINSTALLER_CLI_ERROR_AUTHENTICATION_INCORRECT_ACCOUNT: + resultStatus = winrt::Microsoft::Management::Deployment::FindPackagesResultStatus::AuthenticationError; + break; + case HTTP_E_STATUS_DENIED: + case HTTP_E_STATUS_FORBIDDEN: + resultStatus = winrt::Microsoft::Management::Deployment::FindPackagesResultStatus::AccessDenied; + break; case APPINSTALLER_CLI_ERROR_COMMAND_FAILED: case APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX: case APPINSTALLER_CLI_ERROR_INDEX_INTEGRITY_COMPROMISED: @@ -441,4 +453,46 @@ namespace winrt::Microsoft::Management::Deployment::implementation return Microsoft::Management::Deployment::IconTheme::Unknown; } + + winrt::Microsoft::Management::Deployment::AuthenticationType GetDeploymentAuthenticationType(::AppInstaller::Authentication::AuthenticationType authType) + { + switch (authType) + { + case ::AppInstaller::Authentication::AuthenticationType::None: + return Microsoft::Management::Deployment::AuthenticationType::None; + case ::AppInstaller::Authentication::AuthenticationType::MicrosoftEntraId: + return Microsoft::Management::Deployment::AuthenticationType::MicrosoftEntraId; + } + + return Microsoft::Management::Deployment::AuthenticationType::Unknown; + } + + ::AppInstaller::Authentication::AuthenticationMode GetAuthenticationMode(winrt::Microsoft::Management::Deployment::AuthenticationMode authMode) + { + switch (authMode) + { + case winrt::Microsoft::Management::Deployment::AuthenticationMode::Interactive: + return ::AppInstaller::Authentication::AuthenticationMode::Interactive; + case winrt::Microsoft::Management::Deployment::AuthenticationMode::SilentPreferred: + return ::AppInstaller::Authentication::AuthenticationMode::SilentPreferred; + case winrt::Microsoft::Management::Deployment::AuthenticationMode::Silent: + return ::AppInstaller::Authentication::AuthenticationMode::Silent; + } + + return ::AppInstaller::Authentication::AuthenticationMode::Unknown; + } + + ::AppInstaller::Authentication::AuthenticationArguments GetAuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments authArgs) + { + ::AppInstaller::Authentication::AuthenticationArguments result; + result.Mode = ::AppInstaller::Authentication::AuthenticationMode::Silent; // Default to silent for com invocations. + + if (authArgs) + { + result.Mode = GetAuthenticationMode(authArgs.AuthenticationMode()); + result.AuthenticationAccount = ::AppInstaller::Utility::ConvertToUTF8(authArgs.AuthenticationAccount()); + } + + return result; + } } diff --git a/src/Microsoft.Management.Deployment/Converters.h b/src/Microsoft.Management.Deployment/Converters.h @@ -5,6 +5,7 @@ #include <AppInstallerArchitecture.h> #include <winget/RepositorySource.h> #include <Workflows/WorkflowBase.h> +#include <winget/Authentication.h> namespace winrt::Microsoft::Management::Deployment::implementation { @@ -26,6 +27,9 @@ namespace winrt::Microsoft::Management::Deployment::implementation winrt::Microsoft::Management::Deployment::IconFileType GetDeploymentIconFileType(::AppInstaller::Manifest::IconFileTypeEnum iconFileType); winrt::Microsoft::Management::Deployment::IconResolution GetDeploymentIconResolution(::AppInstaller::Manifest::IconResolutionEnum iconResolution); winrt::Microsoft::Management::Deployment::IconTheme GetDeploymentIconTheme(::AppInstaller::Manifest::IconThemeEnum iconTheme); + winrt::Microsoft::Management::Deployment::AuthenticationType GetDeploymentAuthenticationType(::AppInstaller::Authentication::AuthenticationType authType); + ::AppInstaller::Authentication::AuthenticationMode GetAuthenticationMode(winrt::Microsoft::Management::Deployment::AuthenticationMode authMode); + ::AppInstaller::Authentication::AuthenticationArguments GetAuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments authArgs); #define WINGET_GET_OPERATION_RESULT_STATUS(_installResultStatus_, _uninstallResultStatus_, _downloadResultStatus_) \ if constexpr (std::is_same_v<TStatus, winrt::Microsoft::Management::Deployment::InstallResultStatus>) \ diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj @@ -183,6 +183,8 @@ </ClCompile> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="AuthenticationArguments.h" /> + <ClInclude Include="AuthenticationInfo.h" /> <ClInclude Include="CatalogPackage.h" /> <ClInclude Include="CheckInstalledStatusResult.h" /> <ClInclude Include="ConnectResult.h" /> @@ -199,6 +201,7 @@ <ClInclude Include="InstallOptions.h" /> <ClInclude Include="InstallResult.h" /> <ClInclude Include="MatchResult.h" /> + <ClInclude Include="MicrosoftEntraIdAuthenticationInfo.h" /> <ClInclude Include="PackageAgreement.h" /> <ClInclude Include="PackageCatalog.h" /> <ClInclude Include="PackageCatalogInfo.h" /> @@ -219,6 +222,8 @@ <ClInclude Include="UninstallResult.h" /> </ItemGroup> <ItemGroup> + <ClCompile Include="AuthenticationArguments.cpp" /> + <ClCompile Include="AuthenticationInfo.cpp" /> <ClCompile Include="CatalogPackage.cpp" /> <ClCompile Include="CheckInstalledStatusResult.cpp" /> <ClCompile Include="ComClsids.cpp" /> @@ -236,6 +241,7 @@ <ClCompile Include="InstallOptions.cpp" /> <ClCompile Include="InstallResult.cpp" /> <ClCompile Include="MatchResult.cpp" /> + <ClCompile Include="MicrosoftEntraIdAuthenticationInfo.cpp" /> <ClCompile Include="PackageAgreement.cpp" /> <ClCompile Include="PackageCatalog.cpp" /> <ClCompile Include="PackageCatalogInfo.cpp" /> diff --git a/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters b/src/Microsoft.Management.Deployment/Microsoft.Management.Deployment.vcxproj.filters @@ -35,6 +35,9 @@ <ClCompile Include="PackageManagerSettings.cpp" /> <ClCompile Include="PackageInstallerInfo.cpp" /> <ClCompile Include="PackageInstallerInstalledStatus.cpp" /> + <ClCompile Include="AuthenticationArguments.cpp" /> + <ClCompile Include="AuthenticationInfo.cpp" /> + <ClCompile Include="MicrosoftEntraIdAuthenticationInfo.cpp" /> </ItemGroup> <ItemGroup> <ClInclude Include="CatalogPackage.h" /> @@ -75,6 +78,9 @@ <ClInclude Include="Public\CoCreatableMicrosoftManagementDeploymentClass.h"> <Filter>Public</Filter> </ClInclude> + <ClInclude Include="AuthenticationArguments.h" /> + <ClInclude Include="AuthenticationInfo.h" /> + <ClInclude Include="MicrosoftEntraIdAuthenticationInfo.h" /> </ItemGroup> <ItemGroup> <Midl Include="PackageManager.idl" /> diff --git a/src/Microsoft.Management.Deployment/MicrosoftEntraIdAuthenticationInfo.cpp b/src/Microsoft.Management.Deployment/MicrosoftEntraIdAuthenticationInfo.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "MicrosoftEntraIdAuthenticationInfo.h" +#include "MicrosoftEntraIdAuthenticationInfo.g.cpp" +#include <wil\cppwinrt_wrl.h> + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + void MicrosoftEntraIdAuthenticationInfo::Initialize(::AppInstaller::Authentication::MicrosoftEntraIdAuthenticationInfo authInfo) + { + m_authInfo = std::move(authInfo); + } + hstring MicrosoftEntraIdAuthenticationInfo::Resource() + { + return winrt::to_hstring(m_authInfo.Resource); + } + hstring MicrosoftEntraIdAuthenticationInfo::Scope() + { + return winrt::to_hstring(m_authInfo.Scope); + } +} diff --git a/src/Microsoft.Management.Deployment/MicrosoftEntraIdAuthenticationInfo.h b/src/Microsoft.Management.Deployment/MicrosoftEntraIdAuthenticationInfo.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "MicrosoftEntraIdAuthenticationInfo.g.h" +#include <winget/Authentication.h> + +namespace winrt::Microsoft::Management::Deployment::implementation +{ + struct MicrosoftEntraIdAuthenticationInfo : MicrosoftEntraIdAuthenticationInfoT<MicrosoftEntraIdAuthenticationInfo> + { + MicrosoftEntraIdAuthenticationInfo() = default; + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + void Initialize(::AppInstaller::Authentication::MicrosoftEntraIdAuthenticationInfo authInfo); +#endif + + hstring Resource(); + hstring Scope(); + +#if !defined(INCLUDE_ONLY_INTERFACE_METHODS) + private: + ::AppInstaller::Authentication::MicrosoftEntraIdAuthenticationInfo m_authInfo; +#endif + }; +} diff --git a/src/Microsoft.Management.Deployment/PackageCatalogReference.cpp b/src/Microsoft.Management.Deployment/PackageCatalogReference.cpp @@ -8,6 +8,7 @@ #include "PackageCatalog.h" #include "SourceAgreement.h" #include "ConnectResult.h" +#include "AuthenticationInfo.h" #include "Workflows/WorkflowBase.h" #include "Converters.h" #include "Microsoft/PredefinedInstalledSourceFactory.h" @@ -102,6 +103,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation copy.SetCaller(callerName); copy.SetBackgroundUpdateInterval(catalog.PackageCatalogBackgroundUpdateInterval()); copy.InstalledPackageInformationOnly(catalog.InstalledPackageInformationOnly()); + if (catalog.AuthenticationInfo().AuthenticationType() != winrt::Microsoft::Management::Deployment::AuthenticationType::None) + { + copy.SetAuthenticationArguments(GetAuthenticationArguments(catalog.AuthenticationArguments())); + } copy.Open(progress); remoteSources.emplace_back(std::move(copy)); } @@ -136,7 +141,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation } else { - if (!m_acceptSourceAgreements && SourceAgreements().Size() != 0) + if (!AcceptSourceAgreements() && SourceAgreements().Size() != 0) { return GetConnectSourceAgreementsNotAcceptedErrorResult(); } @@ -145,6 +150,10 @@ namespace winrt::Microsoft::Management::Deployment::implementation source.SetCaller(callerName); source.SetBackgroundUpdateInterval(PackageCatalogBackgroundUpdateInterval()); source.InstalledPackageInformationOnly(m_installedPackageInformationOnly); + if (AuthenticationInfo().AuthenticationType() != winrt::Microsoft::Management::Deployment::AuthenticationType::None) + { + source.SetAuthenticationArguments(GetAuthenticationArguments(m_authenticationArguments)); + } source.Open(progress); } @@ -224,7 +233,7 @@ namespace winrt::Microsoft::Management::Deployment::implementation { return m_acceptSourceAgreements; } - + void PackageCatalogReference::PackageCatalogBackgroundUpdateInterval(winrt::Windows::Foundation::TimeSpan const& value) { if (IsComposite()) @@ -253,4 +262,31 @@ namespace winrt::Microsoft::Management::Deployment::implementation m_installedPackageInformationOnly = value; } + winrt::Microsoft::Management::Deployment::AuthenticationArguments PackageCatalogReference::AuthenticationArguments() + { + return m_authenticationArguments; + } + void PackageCatalogReference::AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value) + { + if (IsComposite()) + { + throw winrt::hresult_illegal_state_change(); + } + + m_authenticationArguments = value; + } + winrt::Microsoft::Management::Deployment::AuthenticationInfo PackageCatalogReference::AuthenticationInfo() + { + std::call_once(m_authenticationInfoOnceFlag, + [&]() + { + if (!IsComposite()) + { + auto authenticationInfo = winrt::make_self<wil::details::module_count_wrapper<winrt::Microsoft::Management::Deployment::implementation::AuthenticationInfo>>(); + authenticationInfo->Initialize(m_sourceReference.GetInformation().Authentication); + m_authenticationInfo = *authenticationInfo; + } + }); + return m_authenticationInfo; + } } diff --git a/src/Microsoft.Management.Deployment/PackageCatalogReference.h b/src/Microsoft.Management.Deployment/PackageCatalogReference.h @@ -30,18 +30,24 @@ namespace winrt::Microsoft::Management::Deployment::implementation void PackageCatalogBackgroundUpdateInterval(winrt::Windows::Foundation::TimeSpan const& value); bool InstalledPackageInformationOnly(); void InstalledPackageInformationOnly(bool value); + winrt::Microsoft::Management::Deployment::AuthenticationArguments AuthenticationArguments(); + void AuthenticationArguments(winrt::Microsoft::Management::Deployment::AuthenticationArguments const& value); + winrt::Microsoft::Management::Deployment::AuthenticationInfo AuthenticationInfo(); #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) private: winrt::Microsoft::Management::Deployment::CreateCompositePackageCatalogOptions m_compositePackageCatalogOptions{ nullptr }; winrt::Microsoft::Management::Deployment::PackageCatalogInfo m_info{ nullptr }; + std::once_flag m_sourceAgreementsOnceFlag; winrt::Windows::Foundation::Collections::IVector<winrt::Microsoft::Management::Deployment::SourceAgreement> m_sourceAgreements{ winrt::single_threaded_vector<winrt::Microsoft::Management::Deployment::SourceAgreement>() }; ::AppInstaller::Repository::Source m_sourceReference; std::optional<std::string> m_additionalPackageCatalogArguments; bool m_acceptSourceAgreements = true; bool m_installedPackageInformationOnly = false; - std::once_flag m_sourceAgreementsOnceFlag; winrt::Windows::Foundation::TimeSpan m_packageCatalogBackgroundUpdateInterval = winrt::Windows::Foundation::TimeSpan::zero(); + winrt::Microsoft::Management::Deployment::AuthenticationArguments m_authenticationArguments{ nullptr }; + std::once_flag m_authenticationInfoOnceFlag; + winrt::Microsoft::Management::Deployment::AuthenticationInfo m_authenticationInfo{ nullptr }; #endif }; } diff --git a/src/Microsoft.Management.Deployment/PackageManager.idl b/src/Microsoft.Management.Deployment/PackageManager.idl @@ -2,7 +2,7 @@ // Licensed under the MIT License. namespace Microsoft.Management.Deployment { - [contractversion(9)] + [contractversion(10)] apicontract WindowsPackageManagerContract{}; /// State of the install @@ -634,7 +634,12 @@ namespace Microsoft.Management.Deployment BlockedByPolicy, CatalogError, InternalError, - InvalidOptions + InvalidOptions, + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] + { + AuthenticationError, + AccessDenied, + } }; /// IMPLEMENTATION NOTE: SearchResult from winget/RepositorySearch.h @@ -698,6 +703,59 @@ namespace Microsoft.Management.Deployment FindPackagesResult FindPackages(FindPackagesOptions options); } + /// Authentication mode + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] + enum AuthenticationMode + { + /// Always use interactive authentication flow on first authentication request, following requests may use cached result. + Interactive, + /// Try silent authentication flow first. If failed, use interactive authentication flow. + SilentPreferred, + /// Only use silent authentication flow. If failed, fail the authentication. + Silent, + }; + + /// Authentication related arguments + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] + runtimeclass AuthenticationArguments + { + AuthenticationArguments(); + + /// Choice of authentication flow behavior. + AuthenticationMode AuthenticationMode; + /// Optional. The authentication account to be used for authentication. + String AuthenticationAccount; + } + + /// Authentication method + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] + enum AuthenticationType + { + Unknown, + None, + MicrosoftEntraId, + }; + + /// Microsoft Entra Id related authentication info. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] + runtimeclass MicrosoftEntraIdAuthenticationInfo + { + /// The resource identifier or resource uri. + String Resource { get; }; + /// Requested scope. May be empty. + String Scope { get; }; + } + + /// Authentication info. + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] + runtimeclass AuthenticationInfo + { + /// The authentication type. + AuthenticationType AuthenticationType { get; }; + /// Microsoft Entra Id related authentication info. + MicrosoftEntraIdAuthenticationInfo MicrosoftEntraIdAuthenticationInfo { get; }; + } + /// Status of the Connect call [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 1)] enum ConnectResultStatus @@ -715,7 +773,7 @@ namespace Microsoft.Management.Deployment runtimeclass ConnectResult { /// Error codes - ConnectResultStatus Status{ get; }; + ConnectResultStatus Status { get; }; PackageCatalog PackageCatalog { get; }; } @@ -758,10 +816,22 @@ namespace Microsoft.Management.Deployment [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 9)] { - // When set to true, the opened catalog will only provide the information regarding packages installed from this catalog. - // In this mode, no external resources should be required. + /// When set to true, the opened catalog will only provide the information regarding packages installed from this catalog. + /// In this mode, no external resources should be required. Boolean InstalledPackageInformationOnly; } + + [contract(Microsoft.Management.Deployment.WindowsPackageManagerContract, 10)] + { + + /// Authentication arguments used in authentication flow during package catalog operations if applicable. + /// This is user or caller input. + AuthenticationArguments AuthenticationArguments; + + /// Authentication info from the package catalog. + /// This is defined by individual package catalog. + AuthenticationInfo AuthenticationInfo { get; }; + } } /// Catalogs with PackageCatalogOrigin Predefined @@ -1269,5 +1339,11 @@ namespace Microsoft.Management.Deployment interface Windows.Foundation.Collections.IVectorView<Icon>; interface Windows.Foundation.Collections.IVector<CatalogPackageMetadata>; interface Windows.Foundation.Collections.IVectorView<CatalogPackageMetadata>; + interface Windows.Foundation.Collections.IVector<AuthenticationArguments>; + interface Windows.Foundation.Collections.IVectorView<AuthenticationArguments>; + interface Windows.Foundation.Collections.IVector<MicrosoftEntraIdAuthenticationInfo>; + interface Windows.Foundation.Collections.IVectorView<MicrosoftEntraIdAuthenticationInfo>; + interface Windows.Foundation.Collections.IVector<AuthenticationInfo>; + interface Windows.Foundation.Collections.IVectorView<AuthenticationInfo>; } } diff --git a/src/Microsoft.Management.Deployment/Public/ComClsids.h b/src/Microsoft.Management.Deployment/Public/ComClsids.h @@ -13,6 +13,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_PackageMatchFilter "D02C9DAF-99DC-429C-B503-4E504E4AB000" #define WINGET_OUTOFPROC_COM_CLSID_ConfigurationStaticFunctions "73D763B7-2937-432F-A97A-D98A4A596126" #define WINGET_OUTOFPROC_COM_CLSID_DownloadOptions "4CBABE76-7322-4BE4-9CEA-2589A80682DC" +#define WINGET_OUTOFPROC_COM_CLSID_AuthenticationArguments "BA580786-BDE3-4F6C-B8F3-44698AC8711A" #else #define WINGET_OUTOFPROC_COM_CLSID_PackageManager "74CB3139-B7C5-4B9E-9388-E6616DEA288C" #define WINGET_OUTOFPROC_COM_CLSID_FindPackagesOptions "1BD8FF3A-EC50-4F69-AEEE-DF4C9D3BAA96" @@ -22,6 +23,7 @@ #define WINGET_OUTOFPROC_COM_CLSID_PackageMatchFilter "3F85B9F4-487A-4C48-9035-2903F8A6D9E8" #define WINGET_OUTOFPROC_COM_CLSID_ConfigurationStaticFunctions "C9ED7917-66AB-4E31-A92A-F65F18EF7933" #define WINGET_OUTOFPROC_COM_CLSID_DownloadOptions "8EF324ED-367C-4880-83E5-BB2ABD0B72F6" +#define WINGET_OUTOFPROC_COM_CLSID_AuthenticationArguments "6484A61D-50FA-41F0-B71E-F4370C6EB37C" #endif // Clsids only used in in-proc invocation @@ -38,6 +40,7 @@ namespace winrt::Microsoft::Management::Deployment const CLSID WINGET_INPROC_COM_CLSID_PackageMatchFilter = { 0x57DC8962, 0x7343, 0x42CD, 0xB9, 0x1C, 0x04, 0xF6, 0xA2, 0x5D, 0xB1, 0xD0 }; // 57DC8962-7343-42CD-B91C-04F6A25DB1D0 const CLSID WINGET_INPROC_COM_CLSID_PackageManagerSettings = { 0x80CF9D63, 0x5505, 0x4342, 0xB9, 0xB4, 0xBB, 0x87, 0x89, 0x5C, 0xA8, 0xBB }; // 80CF9D63-5505-4342-B9B4-BB87895CA8BB const CLSID WINGET_INPROC_COM_CLSID_DownloadOptions = { 0x4288DF96, 0xFDC9, 0x4B68, 0xB4, 0x03, 0x19, 0x3D, 0xBB, 0xF5, 0x6A, 0x24 }; // 4288DF96-FDC9-4B68-B403-193DBBF56A24 + const CLSID WINGET_INPROC_COM_CLSID_AuthenticationArguments = { 0x8D593114, 0x1CF1, 0x43B9, 0x87, 0x22, 0x4D, 0xBB, 0x30, 0x10, 0x32, 0x96 }; // 8D593114-1CF1-43B9-8722-4DBB30103296 CLSID GetRedirectedClsidFromInProcClsid(REFCLSID clsid); } \ No newline at end of file diff --git a/src/Microsoft.Management.Deployment/pch.h b/src/Microsoft.Management.Deployment/pch.h @@ -5,5 +5,7 @@ #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> +#include <ostream> +#include <string> #include <mutex> #include <random> \ No newline at end of file diff --git a/src/WindowsPackageManager/main.cpp b/src/WindowsPackageManager/main.cpp @@ -27,6 +27,7 @@ CoCreatableClassWrlCreatorMapInclude(InstallOptions); CoCreatableClassWrlCreatorMapInclude(UninstallOptions); CoCreatableClassWrlCreatorMapInclude(DownloadOptions); CoCreatableClassWrlCreatorMapInclude(PackageMatchFilter); +CoCreatableClassWrlCreatorMapInclude(AuthenticationArguments); CoCreatableClassWrlCreatorMapInclude(PackageManagerSettings); // Shim for configuration static functions