diff --git a/WindowsAppRuntime.sln b/WindowsAppRuntime.sln index 6e50801299..b512fc6ab5 100644 --- a/WindowsAppRuntime.sln +++ b/WindowsAppRuntime.sln @@ -534,6 +534,7 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OAuth", "dev\OAuth\OAuth.vcxitems", "{3E7FD510-8B66-40E7-A80B-780CB8972F83}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Security.Authentication.OAuth.Projection", "dev\Projections\CS\Microsoft.Security.Authentication.OAuth\Microsoft.Security.Authentication.OAuth.Projection.csproj", "{1D24CC70-85B1-4864-B847-3328F40AF01E}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{3B706C5C-55E0-4B76-BF59-89E20FE46795}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CameraCaptureUI", "CameraCaptureUI", "{0833D8EF-6E11-4133-B0EE-9B7625CD615E}" @@ -546,6 +547,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Windows.Media.Cap EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ProjectReunion.InteractiveExperiences.TransportPackage.PackageReference", "eng\PackageReference\ProjectReunion.InteractiveExperiences\Microsoft.ProjectReunion.InteractiveExperiences.TransportPackage.PackageReference.csproj", "{EDD6D3BF-EBD9-4952-A9B7-76171031139B}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OAuthTestApp", "test\TestApps\OAuthTestApp\OAuthTestApp.vcxproj", "{4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}" +EndProject +Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "OAuthTestAppPackage", "test\TestApps\OAuthTestAppPackage\OAuthTestAppPackage.wapproj", "{455C01F8-0A3E-42C4-9F22-13992EB909EC}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "OAuth2ManagerTests", "test\OAuth2ManagerTests\OAuth2ManagerTests.vcxproj", "{0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1952,6 +1959,62 @@ Global {EDD6D3BF-EBD9-4952-A9B7-76171031139B}.Release|x64.Build.0 = Release|x64 {EDD6D3BF-EBD9-4952-A9B7-76171031139B}.Release|x86.ActiveCfg = Release|x86 {EDD6D3BF-EBD9-4952-A9B7-76171031139B}.Release|x86.Build.0 = Release|x86 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|Any CPU.ActiveCfg = Debug|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|Any CPU.Build.0 = Debug|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|ARM64.ActiveCfg = Debug|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|ARM64.Build.0 = Debug|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|x64.ActiveCfg = Debug|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|x64.Build.0 = Debug|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|x86.ActiveCfg = Debug|Win32 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Debug|x86.Build.0 = Debug|Win32 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|Any CPU.ActiveCfg = Release|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|Any CPU.Build.0 = Release|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|ARM64.ActiveCfg = Release|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|ARM64.Build.0 = Release|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|x64.ActiveCfg = Release|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|x64.Build.0 = Release|x64 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|x86.ActiveCfg = Release|Win32 + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991}.Release|x86.Build.0 = Release|Win32 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|ARM64.Build.0 = Debug|ARM64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|x64.ActiveCfg = Debug|x64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|x64.Build.0 = Debug|x64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|x64.Deploy.0 = Debug|x64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|x86.ActiveCfg = Debug|x86 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|x86.Build.0 = Debug|x86 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Debug|x86.Deploy.0 = Debug|x86 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|Any CPU.Build.0 = Release|Any CPU + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|Any CPU.Deploy.0 = Release|Any CPU + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|ARM64.ActiveCfg = Release|ARM64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|ARM64.Build.0 = Release|ARM64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|ARM64.Deploy.0 = Release|ARM64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|x64.ActiveCfg = Release|x64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|x64.Build.0 = Release|x64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|x64.Deploy.0 = Release|x64 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|x86.ActiveCfg = Release|x86 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|x86.Build.0 = Release|x86 + {455C01F8-0A3E-42C4-9F22-13992EB909EC}.Release|x86.Deploy.0 = Release|x86 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|Any CPU.Build.0 = Debug|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|ARM64.ActiveCfg = Debug|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|ARM64.Build.0 = Debug|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|x64.ActiveCfg = Debug|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|x64.Build.0 = Debug|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|x86.ActiveCfg = Debug|Win32 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Debug|x86.Build.0 = Debug|Win32 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|Any CPU.ActiveCfg = Release|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|Any CPU.Build.0 = Release|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|ARM64.ActiveCfg = Release|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|ARM64.Build.0 = Release|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|x64.ActiveCfg = Release|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|x64.Build.0 = Release|x64 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|x86.ActiveCfg = Release|Win32 + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2122,12 +2185,16 @@ Global {95409D1E-843F-4316-8D8E-471B3E203F94} = {0833D8EF-6E11-4133-B0EE-9B7625CD615E} {1DAA2342-CF55-48E5-B49C-982FA5C07014} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} {97AB4F8D-DF7E-4BA8-9B06-E7B79AF616D6} = {716C26A0-E6B0-4981-8412-D14A4D410531} + {4CAA3052-7FAE-4C5B-A1CB-02D7F910C991} = {AC5FFC80-92FE-4933-BED2-EC5519AC4440} + {455C01F8-0A3E-42C4-9F22-13992EB909EC} = {AC5FFC80-92FE-4933-BED2-EC5519AC4440} + {0FF6A68F-6C7F-4E66-8CB8-C0B9501060CA} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4B3D7591-CFEC-4762-9A07-ABE99938FB77} EndGlobalSection GlobalSection(SharedMSBuildProjectFiles) = preSolution test\inc\inc.vcxitems*{08bc78e0-63c6-49a7-81b3-6afc3deac4de}*SharedItemsImports = 4 + test\inc\inc.vcxitems*{0ff6a68f-6c7f-4e66-8cb8-c0b9501060ca}*SharedItemsImports = 4 dev\PushNotifications\PushNotifications.vcxitems*{103c0c23-7ba8-4d44-a63c-83488e2e3a81}*SharedItemsImports = 9 dev\EnvironmentManager\API\Microsoft.Process.Environment.vcxitems*{2f3fad1b-d3df-4866-a3a3-c2c777d55638}*SharedItemsImports = 9 dev\OAuth\OAuth.vcxitems*{3e7fd510-8b66-40e7-a80b-780cb8972f83}*SharedItemsImports = 9 diff --git a/test/DynamicDependency/data/Microsoft.WindowsAppRuntime.Framework/appxmanifest.xml b/test/DynamicDependency/data/Microsoft.WindowsAppRuntime.Framework/appxmanifest.xml index aed4d038b8..cc3940259b 100644 --- a/test/DynamicDependency/data/Microsoft.WindowsAppRuntime.Framework/appxmanifest.xml +++ b/test/DynamicDependency/data/Microsoft.WindowsAppRuntime.Framework/appxmanifest.xml @@ -34,6 +34,21 @@ in the same manifest with the same ActivatableClassId (regardless in same or different s). --> + + + Microsoft.WindowsAppRuntime.dll + + + + + + + + + + + + Microsoft.WindowsAppRuntime.dll diff --git a/test/OAuth2ManagerTests/OAuth2APITests.cpp b/test/OAuth2ManagerTests/OAuth2APITests.cpp new file mode 100644 index 0000000000..f4582d9c2c --- /dev/null +++ b/test/OAuth2ManagerTests/OAuth2APITests.cpp @@ -0,0 +1,1278 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "OAuth2APITests.h" + +// NOTE: Thise files don't include everything they need, hence they are here last +#include +#include + +#include // Included last to enable the most features + +using namespace std::literals; +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace winrt::Microsoft::Security::Authentication::OAuth; +using namespace winrt::Windows::Data::Json; +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Windows::Security::Cryptography; +using namespace winrt::Windows::Security::Cryptography::Core; +using namespace winrt::Windows::Storage::Streams; + +namespace TB = ::Test::Bootstrap; +namespace TP = ::Test::Packages; +EXTERN_C IMAGE_DOS_HEADER __ImageBase; + +namespace OAuth2ManagerTests +{ + class OAuth2APITests + { + public: + BEGIN_TEST_CLASS(OAuth2APITests) + TEST_CLASS_PROPERTY(L"ThreadingModel", L"MTA") + END_TEST_CLASS() + + TEST_METHOD_SETUP(MethodInit) + { + VERIFY_IS_TRUE(TP::IsPackageRegistered_WindowsAppRuntimeFramework()); + return true; + } + + TEST_METHOD_CLEANUP(MethodUninit) + { + VERIFY_IS_TRUE(TP::IsPackageRegistered_WindowsAppRuntimeFramework()); + return true; + } + + TEST_CLASS_SETUP(Setup) + { + // Initialize the Windows Runtime + winrt::init_apartment(); + Test::Bootstrap::Setup(); + Test::Packages::WapProj::AddPackage(Test::TAEF::GetDeploymentDir(), L"OAuthTestAppPackage", L".msix"); + + // Detour ShellExecuteW + ::DetourTransactionBegin(); + ::DetourUpdateThread(::GetCurrentThread()); + if (auto err = ::DetourAttach(reinterpret_cast(&RealShellExecuteW), &DetouredShellExecuteW)) + { + Log::Error(WEX::Common::String().Format(L"DetourAttach failed: %d", err)); + ::DetourTransactionAbort(); + return false; + } + ::DetourTransactionCommit(); + + // Initialize the HTTP server we use for token requests + VERIFY_WIN32_SUCCEEDED(::HttpInitialize(HTTPAPI_VERSION_2, HTTP_INITIALIZE_SERVER, nullptr)); + VERIFY_WIN32_SUCCEEDED(::HttpCreateRequestQueue(HTTPAPI_VERSION_2, nullptr, nullptr, 0, &m_requestQueue)); + VERIFY_WIN32_SUCCEEDED(::HttpCreateServerSession(HTTPAPI_VERSION_2, &m_serverSessionId, 0)); + VERIFY_WIN32_SUCCEEDED(::HttpCreateUrlGroup(m_serverSessionId, &m_urlGroup, 0)); + + HTTP_BINDING_INFO bindingInfo = {}; + bindingInfo.Flags.Present = 1; + bindingInfo.RequestQueueHandle = m_requestQueue; + VERIFY_WIN32_SUCCEEDED(::HttpSetUrlGroupProperty(m_urlGroup, HttpServerBindingProperty, &bindingInfo, + static_cast(sizeof(bindingInfo)))); + + // Find an open port; note that ports in the low 50000s are frequently claimed, hence the large iteration bounds + ULONG err = 0; + for (std::uint16_t i = 0; i < 500; ++i) + { + wchar_t buffer[18 + 5 + 1]; + std::swprintf(buffer, std::size(buffer), L"http://127.0.0.1:%d/", m_serverPort); + + err = ::HttpAddUrlToUrlGroup(m_urlGroup, buffer, 0, 0); + if (err == NO_ERROR) + { + m_serverUrlBase = buffer; + break; + } + + ++m_serverPort; + } + + VERIFY_WIN32_SUCCEEDED(err, L"Looking for an open port"); + m_httpServerThread = std::thread([this] + { + RunHttpServer(); + }); + + return true; + } + + TEST_CLASS_CLEANUP(Cleanup) + { + ::Test::Bootstrap::CleanupPackages(); + Test::Packages::RemovePackage(L"OAuthTestAppPackage_1.0.0.0_" WINDOWSAPPRUNTIME_TEST_PACKAGE_DDLM_ARCHITECTURE L"__8wekyb3d8bbwe"); + + // Tear down the HTTP server + m_serverShutdownEvent.SetEvent(); + m_httpServerThread.join(); + + if (m_urlGroup) + { + ::HttpCloseUrlGroup(m_urlGroup); + m_urlGroup = 0; + } + + if (m_serverSessionId) + { + ::HttpCloseServerSession(m_serverSessionId); + m_serverSessionId = 0; + } + + if (m_requestQueue) + { + ::HttpCloseRequestQueue(m_requestQueue); + m_requestQueue = nullptr; + } + + ::HttpTerminate(HTTP_INITIALIZE_SERVER, nullptr); + + // Clean up our detours + ::DetourTransactionBegin(); + ::DetourUpdateThread(::GetCurrentThread()); + ::DetourDetach(reinterpret_cast(&RealShellExecuteW), &DetouredShellExecuteW); + ::DetourTransactionCommit(); + + Test::Bootstrap::Cleanup(); + + return true; + } + + template + static void WaitWithTimeout(const IAsyncOperation& op, AsyncStatus expectedStatus) + { + wil::unique_event event(wil::EventOptions::None); + op.Completed([event = event.get()](const IAsyncOperation&, AsyncStatus) + { + ::SetEvent(event); + }); + + // 10 seconds is beyond + if (::WaitForSingleObject(event.get(), 10000) == WAIT_OBJECT_0) + { + VERIFY_ARE_EQUAL(expectedStatus, op.Status()); + return; + } + + Log::Warning(L"Timed out waiting for IAsyncOperation to complete; cancelling..."); + op.Cancel(); + + // Cancel should cause the operation to complete with the cancellation + if (::WaitForSingleObject(event.get(), 1000) != WAIT_OBJECT_0) + { + // Lambda holds a reference to the event. Best just to leak it here + Log::Warning(L"Failed to cancel IAsyncOperation; leaking event"); + event.release(); + } + + VERIFY_FAIL(L"IAsyncOperation did not complete in a reasonable amount of time"); + } + + template + static void VerifyErrorNull(const ErrorT& error) + { + if (error) + { + Log::Error(WEX::Common::String().Format(L"Error object expected to be null! Message: %ls", + error.ErrorDescription().c_str())); + } + VERIFY_IS_NULL(error); + } + + AuthResponse InitiateAndWaitForSuccessfulAuthResponse(const AuthRequestParams& params) + { + auto op = OAuth2Manager::RequestAuthWithParamsAsync(winrt::Microsoft::UI::WindowId(123456789), Uri{ auth_url }, params); + auto par = params.CodeChallenge(); + WaitWithTimeout(op, AsyncStatus::Completed); + + auto result = op.GetResults(); + VerifyErrorNull(result.Failure()); + + auto response = result.Response(); + VERIFY_IS_NOT_NULL(response); + VERIFY_ARE_EQUAL(params.State(), response.State()); + + return response; + } + + TokenResponse RequestTokenAndWaitForSuccessfulResponse(const TokenRequestParams& params, const ClientAuthentication& auth = { nullptr }) + { + IAsyncOperation op{ nullptr }; + if (auth) + { + op = OAuth2Manager::RequestTokenAsync(Uri{ m_serverUrlBase + L"token" }, params, auth); + } + else + { + op = OAuth2Manager::RequestTokenAsync(Uri{ m_serverUrlBase + L"token" }, params); + } + WaitWithTimeout(op, AsyncStatus::Completed); + + auto result = op.GetResults(); + VerifyErrorNull(result.Failure()); + + auto response = result.Response(); + VERIFY_IS_NOT_NULL(response); + VERIFY_ARE_EQUAL(token, response.AccessToken()); + VERIFY_ARE_EQUAL(L"Bearer", response.TokenType()); + VERIFY_ARE_EQUAL(3600, response.ExpiresIn()); + VERIFY_ARE_EQUAL(refresh_token, response.RefreshToken()); + VERIFY_ARE_EQUAL(L"all", response.Scope()); + + return response; + } + + static inline constexpr std::wstring_view auth_url = L"http://oauthtests.com/oauth"sv; + + // Redirect URIs + static inline constexpr std::wstring_view localhost_redirect_uri = L"http://127.0.0.1/oauth"sv; + static inline constexpr std::wstring_view protocol_redirect_uri = L"oauthtestapp:oauth"sv; + + void DoEndToEndAuthCodeTest(const AuthRequestParams& requestParams) + { + auto authResponse = InitiateAndWaitForSuccessfulAuthResponse(requestParams); + VERIFY_IS_FALSE(authResponse.Code().empty()); + + auto tokenParams = TokenRequestParams::CreateForAuthorizationCodeRequest(authResponse); + RequestTokenAndWaitForSuccessfulResponse(tokenParams); + } + + void DoBasicEndToEndAuthCodeTest(std::wstring_view clientId, std::wstring_view redirectUri) + { + auto requestParams = AuthRequestParams::CreateForAuthorizationCodeRequest(clientId, Uri{ redirectUri }); + DoEndToEndAuthCodeTest(requestParams); + } + + TEST_METHOD(Protocol_AuthorizationCode_BasicEndToEnd) + { + static constexpr std::wstring_view client_id = GRANT_TYPE_CODE REDIRECT_TYPE_PROTOCOL; + DoBasicEndToEndAuthCodeTest(client_id, protocol_redirect_uri); + } + + TEST_METHOD(AuthorizationCodeWithClientAuth) + { + // NOTE: This is testing client auth, which is a token request only thing, hence only using a single redirection type + static constexpr std::wstring_view client_id = GRANT_TYPE_CODE REDIRECT_TYPE_LOCALHOST AUTH_TYPE_HEADER; + auto requestParams = AuthRequestParams::CreateForAuthorizationCodeRequest(client_id, Uri{ localhost_redirect_uri }); + auto authResponse = InitiateAndWaitForSuccessfulAuthResponse(requestParams); + + auto tokenParams = TokenRequestParams::CreateForAuthorizationCodeRequest(authResponse); + auto auth = ClientAuthentication::CreateForBasicAuthorization(client_id, L"password"); + auto tokenAsyncOp = OAuth2Manager::RequestTokenAsync(Uri{ m_serverUrlBase + L"token" }, tokenParams, auth); + WaitWithTimeout(tokenAsyncOp, AsyncStatus::Completed); + + auto tokenResult = tokenAsyncOp.GetResults(); + auto tokenResponse = tokenResult.Response(); + VerifyErrorNull(tokenResult.Failure()); + VERIFY_IS_NOT_NULL(tokenResponse); + VERIFY_ARE_EQUAL(token, tokenResponse.AccessToken()); + } + + TEST_METHOD(UserCredentialsTokenRequest) + { + static constexpr std::wstring_view client_id = GRANT_TYPE_PASSWORD AUTH_TYPE_HEADER; + auto tokenParams = TokenRequestParams::CreateForResourceOwnerPasswordCredentials(L"username", L"password"); + auto auth = ClientAuthentication::CreateForBasicAuthorization(client_id, L"password"); + RequestTokenAndWaitForSuccessfulResponse(tokenParams, auth); + } + + TEST_METHOD(ClientCredentialsTokenRequest) + { + static constexpr std::wstring_view client_id = GRANT_TYPE_CLIENT AUTH_TYPE_HEADER; + auto tokenParams = TokenRequestParams::CreateForClientCredentials(); + auto auth = ClientAuthentication::CreateForBasicAuthorization(client_id, L"password"); + RequestTokenAndWaitForSuccessfulResponse(tokenParams, auth); + } + + TEST_METHOD(RefreshTokenRequest) + { + static constexpr std::wstring_view client_id = GRANT_TYPE_REFRESH AUTH_TYPE_HEADER; + auto tokenParams = TokenRequestParams::CreateForRefreshToken(refresh_token_old); + auto auth = ClientAuthentication::CreateForBasicAuthorization(client_id, L"password"); + RequestTokenAndWaitForSuccessfulResponse(tokenParams, auth); + } + + TEST_METHOD(ExtensionTokenRequest) + { + static constexpr std::wstring_view client_id = GRANT_TYPE_EXTENSION AUTH_TYPE_HEADER; + auto tokenParams = TokenRequestParams::CreateForExtension(Uri{ extension_grant_uri }); + auto auth = ClientAuthentication::CreateForBasicAuthorization(client_id, L"password"); + RequestTokenAndWaitForSuccessfulResponse(tokenParams, auth); + } + + TEST_METHOD(TokenRequestErrorResponse) + { + static constexpr std::wstring_view client_id = GRANT_TYPE_PASSWORD TOKEN_ERROR_RESPONSE; + auto tokenParams = TokenRequestParams::CreateForResourceOwnerPasswordCredentials(L"username", L"password"); + auto auth = ClientAuthentication::CreateForBasicAuthorization(client_id, L"password"); + auto tokenAsyncOp = OAuth2Manager::RequestTokenAsync(Uri{ m_serverUrlBase + L"token" }, tokenParams, auth); + WaitWithTimeout(tokenAsyncOp, AsyncStatus::Completed); + + auto tokenResult = tokenAsyncOp.GetResults(); + auto tokenError = tokenResult.Failure(); + VERIFY_IS_NULL(tokenResult.Response()); + VERIFY_IS_NOT_NULL(tokenError); + auto additionalParams = tokenError.AdditionalParams(); + VERIFY_IS_NOT_NULL(additionalParams); + + VERIFY_ARE_EQUAL(E_FAIL, tokenError.ErrorCode().value); + VERIFY_ARE_EQUAL(L"server_error", tokenError.Error()); + VERIFY_ARE_EQUAL(error_description, tokenError.ErrorDescription()); + VERIFY_IS_NOT_NULL(tokenError.ErrorUri()); + VERIFY_ARE_EQUAL(error_uri, tokenError.ErrorUri().RawUri()); + + VERIFY_IS_TRUE(additionalParams.HasKey(additional_param_key)); + auto jsonValue = additionalParams.Lookup(additional_param_key); + VERIFY_ARE_EQUAL(JsonValueType::String, jsonValue.ValueType()); + VERIFY_ARE_EQUAL(additional_param_value, jsonValue.GetString()); + } + TEST_METHOD(AdditionalParams) + { + static constexpr std::wstring_view client_id = GRANT_TYPE_CODE REDIRECT_TYPE_LOCALHOST ADDITIONAL_PARAMS; + auto requestParams = AuthRequestParams::CreateForAuthorizationCodeRequest(client_id, Uri{ localhost_redirect_uri }); + auto additionalRequestParams = requestParams.AdditionalParams(); + additionalRequestParams.Insert(additional_param_key, additional_param_value); + auto authResponse = InitiateAndWaitForSuccessfulAuthResponse(requestParams); + + auto tokenParams = TokenRequestParams::CreateForAuthorizationCodeRequest(authResponse); + auto additionalTokenParams = tokenParams.AdditionalParams(); + additionalTokenParams.Insert(additional_param_key, additional_param_value); + + auto tokenAsyncOp = OAuth2Manager::RequestTokenAsync(Uri{ m_serverUrlBase + L"token" }, tokenParams); + WaitWithTimeout(tokenAsyncOp, AsyncStatus::Completed); + + auto tokenResult = tokenAsyncOp.GetResults(); + auto tokenResponse = tokenResult.Response(); + VerifyErrorNull(tokenResult.Failure()); + VERIFY_IS_NOT_NULL(tokenResponse); + } + + TEST_METHOD(CompleteInvalidState) + { + VERIFY_IS_FALSE(OAuth2Manager::CompleteAuthRequest(Uri{ L"unknown-protocol:" })); // No query parameters at all + VERIFY_IS_FALSE(OAuth2Manager::CompleteAuthRequest(Uri{ L"http://127.0.0.1/oauth?code=abc123" })); // Missing state + VERIFY_IS_FALSE(OAuth2Manager::CompleteAuthRequest(Uri{ L"oauthtestapp:oauth?code=abc&state=invalid" })); + VERIFY_IS_FALSE(OAuth2Manager::CompleteAuthRequest(Uri{ L"http://127.0.0.1/oauth?code=abc123&state=invalid" })); + } + + // Detoured Functions + static BOOL __stdcall DetouredShellExecuteW(SHELLEXECUTEINFO* sei) try + { + std::wstring_view fileStr = sei->lpFile; + if (fileStr.substr(0, auth_url.size()) == auth_url) + { + winrt::hstring errorString; + winrt::hstring errorMessage; + auto assignInvalidRequestError = [&](std::wstring_view msg) + { + if (errorString.empty()) + { + errorString = L"invalid_request"; + errorMessage = msg; + } + }; + auto assignMismatchedArgsError = [&](std::wstring_view name, std::wstring_view expected, std::wstring_view actual) + { + if (errorString.empty()) + { + std::wstring msg = L"Unexpected value for '"; + msg.append(name); + msg += L"'. Expected '"; + msg.append(expected); + msg += L"' but got '"; + msg.append(actual); + msg += L"'"; + errorString = L"invalid_request"; + errorMessage = msg; + } + }; + + // There's no point in launching the browser and trying to fake an authorization flow as that would do + // nothing to test the API. Instead, perform the logic of the browser and authorization flow here in-proc + winrt::hstring responseType; + winrt::hstring clientId; + Uri redirectUri{ nullptr }; + winrt::hstring scope; + winrt::hstring state; + winrt::hstring codeChallenge; + winrt::hstring codeChallengeMethod; + winrt::hstring additionalParam; + winrt::hstring foo; + for (auto&& entry : Uri(fileStr).QueryParsed()) + { + auto name = entry.Name(); + auto value = entry.Value(); + if (name == L"response_type") + { + responseType = value; + } + else if (name == L"client_id") + { + clientId = value; + } + else if (name == L"redirect_uri") + { + redirectUri = Uri{ value }; + } + else if (name == L"scope") + { + scope = value; + } + else if (name == L"state") + { + state = value; + } + else if (name == L"code_challenge") + { + codeChallenge = value; + } + else if (name == L"code_challenge_method") + { + codeChallengeMethod = value; + } + else if (name == additional_param_key) + { + additionalParam = value; + } + else if (name == L"foo") + { + foo = value; + } + else + { + assignInvalidRequestError(L"Unrecognized query parameter '"s + name + L"'"); + } + } + + // Some behavior is encoded in the client id + winrt::hstring expectedGrantType; + winrt::hstring expectedRedirectType; + winrt::hstring expectedPkceType = L"S256"; + winrt::hstring expectedScopeType = L"none"; + winrt::hstring expectedError = L"none"; + bool completeRequest = true; + bool expectAdditionalParams = false; + for (auto&& entry : WwwFormUrlDecoder{ clientId }) + { + auto name = entry.Name(); + auto value = entry.Value(); + if (name == L"grant") + { + expectedGrantType = value; + } + else if (name == L"redirect") + { + expectedRedirectType = value; + } + else if (name == L"pkce") + { + expectedPkceType = value; + } + else if (name == L"scope") + { + expectedScopeType = value; + } + else if (name == L"error") + { + expectedError = value; + } + else if (name == L"complete") + { + completeRequest = (value == L"true"); + } + else if (name == L"additional_params") + { + expectAdditionalParams = (value == L"true"); + } + // Ignore other values as these are specific to the token request + } + + if (state.empty()) + { + // If no state is provided, we'll be unable to correlate the response to the request. The best we can + // really do here is to fail the launch which will fail the test early and reliably + Log::Error(L"No 'state' value provided in the URI"); + ::SetLastError(ERROR_INVALID_PARAMETER); + return false; + } + else if (responseType.empty()) + { + assignInvalidRequestError(L"Missing 'response_type'"); + } + else if (clientId.empty()) + { + assignInvalidRequestError(L"Missing 'client_id'"); + } + else if (expectedGrantType.empty()) + { + assignInvalidRequestError(L"Client id is missing the expected grant type"); + } + else if (expectedRedirectType.empty()) + { + assignInvalidRequestError(L"Client id is missing the expected redirect type"); + } + + if (responseType != expectedGrantType) + { + assignMismatchedArgsError(L"response_type", expectedGrantType, responseType); + } + + auto expectedUri = (expectedRedirectType == L"localhost") ? localhost_redirect_uri : protocol_redirect_uri; + if (redirectUri.RawUri() != expectedUri) + { + assignMismatchedArgsError(L"redirect_uri", expectedUri, redirectUri.RawUri()); + } + + if (expectedPkceType == L"none") + { + if (!codeChallengeMethod.empty()) + { + assignMismatchedArgsError(L"code_challenge_method", L"", codeChallengeMethod); + } + } + else if (expectedPkceType != codeChallengeMethod) + { + assignMismatchedArgsError(L"code_challenge_method", expectedPkceType, codeChallengeMethod); + } + + if (expectedScopeType == L"none") + { + if (!scope.empty()) + { + assignMismatchedArgsError(L"scope", L"", scope); + } + } + else if (scope.empty()) + { + assignInvalidRequestError(L"Expected a 'scope' parameter, but none provided"); + } + else if (expectedScopeType == L"single") + { + if (scope != single_scope) + { + assignMismatchedArgsError(L"scope", single_scope, scope); + } + } + else if (expectedScopeType == L"multiple") + { + if (scope != multiple_scope) + { + assignMismatchedArgsError(L"scope", multiple_scope, scope); + } + } + + if (expectAdditionalParams) + { + if (additionalParam.empty()) + { + assignInvalidRequestError(L"Expected additional params, but none provided"); + } + else if (additionalParam != additional_param_value) + { + assignMismatchedArgsError(L"additional param", additional_param_value, additionalParam); + } + } + else if (!additionalParam.empty()) + { + assignInvalidRequestError(L"Expected no additional params, but one was provided"); + } + + Uri responseUri{ nullptr }; + if (expectedError == L"auth") + { + uri_builder builder(redirectUri, responseType != L"token"); + builder.add(L"state", state); + builder.add(L"error", L"server_error"); + builder.add(L"error_description", error_description); + builder.add(L"error_uri", error_uri); + builder.add(additional_param_key, additional_param_value); + responseUri = builder.get(); + } + else if (responseType == L"code") + { + // For simplicity, encode the client id and PKCE info in the code + std::wstring code = L"client="; + code += Uri::EscapeComponent(clientId); + if (codeChallengeMethod.empty()) + { + code += L"&challenge_method=none"; + } + else + { + code += L"&challenge_method="; + code += codeChallengeMethod; + code += L"&challenge="; + code += codeChallenge; + } + + // NOTE: The 'scope' should be empty, but we should never indicate an expected 'scope' other than 'none' + // for tests that use the auth code grant type + + uri_builder builder{ redirectUri }; + builder.add(L"code", code); + builder.add(L"state", state); + responseUri = builder.get(); + } + else if (responseType == L"token") + { + if (!codeChallengeMethod.empty()) + { + assignInvalidRequestError(L"Use of PKCE is not valid for implicit requests"); + } + + uri_builder builder{ redirectUri, false }; + builder.add(L"state", state); + builder.add(L"access_token", token); + builder.add(L"token_type", L"Bearer"); + builder.add(L"expires_in", L"3600"); + if (scope.empty()) + { + builder.add(L"scope", L"all"); + } + else + { + builder.add(L"scope", scope); + } + + responseUri = builder.get(); + } + else + { + assignInvalidRequestError(L"Unknown response type '"s + responseType + L"'"); + } + + if (!errorString.empty()) + { + // NOTE: We may have created a response URI already, in which case we want to overwrite it here + uri_builder builder(redirectUri, responseType != L"token"); + builder.add(L"state", state); + builder.add(L"error", errorString); + builder.add_optional(L"error_description", errorMessage); + responseUri = builder.get(); + } + SHELLEXECUTEINFO sei = { sizeof(sei) }; + sei.fMask = SEE_MASK_NOCLOSEPROCESS; + sei.hwnd = nullptr; + sei.lpVerb = L"open"; + sei.lpFile = responseUri.RawUri().c_str(); + sei.lpParameters = nullptr; + sei.lpDirectory = nullptr; + sei.nShow = SW_SHOWDEFAULT; + sei.hInstApp = nullptr; + if (responseUri.SchemeName() != L"http") + { + // Protocol activation + return RealShellExecuteW(&sei); + } + + // Simulating a localhost server. This would give the response back in-proc so we can just go ahead and + // do that directly. Note that we do this in the same callstack as that will test more interesting code + // paths. TODO: Async completion as a parameter? Or just let protocol activation test that path + if (completeRequest && !OAuth2Manager::CompleteAuthRequest(responseUri)) + { + Log::Warning(L"Failed to complete auth request"); + } + + return TRUE; + } + + // Not intercepting. Let this "fall through" to the implementation + return RealShellExecuteW(sei); + } + catch (...) + { + ::SetLastError(ERROR_FILE_NOT_FOUND); + return FALSE; + } + + // HTTP Server Thread Callback + void RunHttpServer() + { + wil::unique_event event{ wil::EventOptions::None }; + OVERLAPPED overlapped = {}; + overlapped.hEvent = event.get(); + + ULONG bufferSize = 0x1000; // 4 KB + auto buffer = std::make_unique(bufferSize); + auto request = reinterpret_cast(buffer.get()); + while (true) + { + auto err = ::HttpReceiveHttpRequest(m_requestQueue, HTTP_NULL_ID, HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY, + request, bufferSize, nullptr, &overlapped); + if (err == ERROR_IO_PENDING) + { + // Wait for either shutdown or a request to come in + HANDLE handles[] = { event.get(), m_serverShutdownEvent.get() }; + auto waitResult = ::WaitForMultipleObjects(2, handles, false, INFINITE); + if (waitResult == (WAIT_OBJECT_0 + 1)) + { + // Shutdown + ::CancelIo(m_requestQueue); + break; + } + else if (waitResult != WAIT_OBJECT_0) + { + Log::Warning(WEX::Common::String().Format( + L"WaitForMultipleObjects failed in the HTTP server thread: %d", ::GetLastError())); + ::CancelIo(m_requestQueue); + break; + } + } + + // We have a request; we'll block here until we have all data, if needed + DWORD bytes; + ::GetOverlappedResult(m_requestQueue, &overlapped, &bytes, false); + err = ::GetLastError(); + if (err == ERROR_MORE_DATA) + { + bufferSize = bytes; + buffer = std::make_unique(bufferSize); + request = reinterpret_cast(buffer.get()); + err = ::HttpReceiveHttpRequest(m_requestQueue, request->RequestId, HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY, + request, bufferSize, &bytes, nullptr); + } + + if (err == ERROR_CONNECTION_INVALID) + { + // Connection corrupted by peer + continue; + } + else if (err != ERROR_SUCCESS) + { + Log::Warning(WEX::Common::String().Format(L"HttpReceiveHttpRequest failed: %d", err)); + break; + } + + switch (request->Verb) + { + case HttpVerbPOST: + HandlePostRequest(request); + break; + + default: + Log::Warning(L"Received an HTTP request with an unexpected verb"); + break; + } + } + } + + void HandlePostRequest(HTTP_REQUEST* request) + { + std::string body; + for (USHORT i = 0; i < request->EntityChunkCount; ++i) + { + auto& chunk = request->pEntityChunks[i]; + WINRT_ASSERT(chunk.DataChunkType == HttpDataChunkFromMemory); + auto& data = chunk.FromMemory; + body.append(static_cast(data.pBuffer), data.BufferLength); + } + + if (request->Flags & HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS) + { + ULONG bufferLength = 2048; + auto buffer = std::make_unique(bufferLength); + while (true) + { + ULONG bytes = 0; + auto result = ::HttpReceiveRequestEntityBody(m_requestQueue, request->RequestId, 0, buffer.get(), + bufferLength, &bytes, nullptr); + if ((result == NO_ERROR) || (result == ERROR_HANDLE_EOF)) + { + body.append(buffer.get(), bytes); + } + else + { + Log::Warning(WEX::Common::String().Format(L"HttpReceiveRequestEntityBody failed: %d", result)); + return; // TODO: Should we send a response here? Getting an error probably means we shouldn't? + } + + if (result == ERROR_HANDLE_EOF) break; + } + } + + winrt::hstring errorString; + winrt::hstring errorMessage; + auto assignInvalidRequestError = [&](std::wstring_view msg) + { + if (errorString.empty()) + { + errorString = L"invalid_request"; + errorMessage = msg; + } + }; + auto assignMismatchedArgsError = [&](std::wstring_view name, std::wstring_view expected, std::wstring_view actual) + { + if (errorString.empty()) + { + std::wstring msg = L"Unexpected value for '"; + msg.append(name); + msg += L"'. Expected '"; + msg.append(expected); + msg += L"' but got '"; + msg.append(actual); + msg += L"'"; + errorString = L"invalid_request"; + errorMessage = msg; + } + }; + + winrt::hstring grantType; + winrt::hstring code; + Uri redirectUri{ nullptr }; + winrt::hstring clientId; + winrt::hstring codeVerifier; + winrt::hstring username; + winrt::hstring password; + winrt::hstring scope; + winrt::hstring refreshToken; + winrt::hstring additionalParam; + for (auto&& entry : WwwFormUrlDecoder(winrt::to_hstring(body))) + { + auto name = entry.Name(); + auto value = entry.Value(); + if (name == L"grant_type") + { + grantType = value; + } + else if (name == L"code") + { + code = value; + } + else if (name == L"redirect_uri") + { + redirectUri = Uri{ value }; + } + else if (name == L"client_id") + { + clientId = value; + } + else if (name == L"code_verifier") + { + codeVerifier = value; + } + else if (name == L"username") + { + username = value; + } + else if (name == L"password") + { + password = value; + } + else if (name == L"scope") + { + scope = value; + } + else if (name == L"refresh_token") + { + refreshToken = value; + } + else if (name == additional_param_key) + { + additionalParam = value; + } + else + { + assignInvalidRequestError(L"Unrecognized query parameter '"s + name + L"'"); + } + } + + auto& authHeader = request->Headers.KnownHeaders[HttpHeaderAuthorization]; + if (authHeader.RawValueLength > 0) + { + // Should be of the form ' ' + std::string_view authHeaderStr(authHeader.pRawValue, authHeader.RawValueLength); + auto firstSpace = authHeaderStr.find_first_of(' '); + if (firstSpace == authHeaderStr.npos) + { + assignInvalidRequestError(L"Bad Authorization hedaer"); + } + else + { + auto scheme = authHeaderStr.substr(0, firstSpace); + auto value = authHeaderStr.substr(firstSpace + 1); + if (scheme != "Basic") + { + assignInvalidRequestError(L"Authorization must use 'Basic' type"); + } + else + { + // 'value' is 'client_id:client_crednetials' base64urlencoded + auto credsBuffer = CryptographicBuffer::DecodeFromBase64String(winrt::to_hstring(value)); + auto fullCreds = CryptographicBuffer::ConvertBinaryToString(BinaryStringEncoding::Utf8, credsBuffer); + std::wstring_view fullCredsStr = fullCreds; + auto colonPos = fullCredsStr.find_first_of(':'); + if (colonPos == fullCredsStr.npos) + { + assignInvalidRequestError(L"Bad Authorization header"); + } + else + { + auto credsClientId = fullCredsStr.substr(0, colonPos); + auto credsClientSecret = fullCredsStr.substr(colonPos + 1); + if (credsClientSecret != L"password") + { + assignMismatchedArgsError(L"Authorization client secret", L"password", credsClientSecret); + } + else if (clientId.empty()) + { + clientId = credsClientId; + } + else if (credsClientId != clientId) + { + assignMismatchedArgsError(L"Authorization client id", clientId, credsClientId); + } + } + } + } + } + + if (clientId.empty()) + { + assignInvalidRequestError(L"Client id not provided"); + } + + winrt::hstring expectedGrantType; + winrt::hstring expectedRedirectType; + winrt::hstring expectedPkceType = L"S256"; + winrt::hstring expectedScopeType = L"none"; + winrt::hstring expectedAuthType = L"none"; + winrt::hstring expectedError = L"none"; + bool expectAdditionalParams = false; + for (auto&& entry : WwwFormUrlDecoder{ clientId }) + { + auto name = entry.Name(); + auto value = entry.Value(); + if (name == L"grant") + { + expectedGrantType = value; + } + else if (name == L"redirect") + { + expectedRedirectType = value; + } + else if (name == L"pkce") + { + expectedPkceType = value; + } + else if (name == L"scope") + { + expectedScopeType = value; + } + else if (name == L"auth") + { + expectedAuthType = value; + } + else if (name == L"error") + { + expectedError = value; + } + else if (name == L"additional_params") + { + expectAdditionalParams = (value == L"true"); + } + // Ignore other values as these are specific to the authorization request + } + + auto checkUnexpectedArg = [&](std::wstring_view name, const winrt::hstring& value) + { + if (!value.empty()) + { + assignMismatchedArgsError(name, L"", value); + } + }; + + if (expectAdditionalParams) + { + if (additionalParam.empty()) + { + assignInvalidRequestError(L"Expected additional params, but none provided"); + } + else if (additionalParam != additional_param_value) + { + assignMismatchedArgsError(L"additional param", additional_param_value, additionalParam); + } + } + else if (!additionalParam.empty()) + { + assignInvalidRequestError(L"Expected no additional params, but one was provided"); + } + + if ((expectedAuthType == L"header") && (authHeader.RawValueLength == 0)) + { + assignInvalidRequestError(L"Authorization header expected, but not provided"); + } + + std::wstring responseJson; + if (expectedError == L"token") + { + errorString = L"server_error"; + errorMessage = json_escaped_error_description; + } + else if (grantType == L"authorization_code") + { + if (expectedGrantType != L"code") + { + assignMismatchedArgsError(L"grant_type", expectedGrantType, grantType); + } + else if (code.empty()) + { + assignInvalidRequestError(L"Authorization code not provided"); + } + + if (redirectUri) + { + auto expectedUri = (expectedRedirectType == L"protocol") ? protocol_redirect_uri : localhost_redirect_uri; + if (redirectUri.RawUri() != expectedUri) + { + assignMismatchedArgsError(L"redirect_uri", expectedUri, redirectUri.RawUri()); + } + } + else if (expectedRedirectType != L"inferred") + { + assignInvalidRequestError(L"Expected a 'redirect_uri', but none provided"); + } + + checkUnexpectedArg(L"username", username); + checkUnexpectedArg(L"password", password); + checkUnexpectedArg(L"scope", scope); // Only expected during auth request + checkUnexpectedArg(L"refresh_token", refreshToken); + + winrt::hstring codeClientId; + winrt::hstring codeChallengeMethod; + winrt::hstring codeChallenge; + for (auto&& entry : WwwFormUrlDecoder{ code }) + { + auto name = entry.Name(); + auto value = entry.Value(); + if (name == L"client") + { + codeClientId = value; + } + else if (name == L"challenge_method") + { + codeChallengeMethod = value; + } + else if (name == L"challenge") + { + codeChallenge = value; + } + else + { + assignInvalidRequestError(L"Unrecognized query parameter '" + name + L"' in code"); + } + } + + if (clientId != codeClientId) + { + assignMismatchedArgsError(L"client_id", codeClientId, clientId); + } + + if (expectedPkceType != codeChallengeMethod) + { + assignMismatchedArgsError(L"code challenge method", expectedPkceType, codeChallengeMethod); + } + else if (codeChallengeMethod == L"none") + { + if (!codeVerifier.empty()) + { + assignMismatchedArgsError(L"code_verifier", L"", codeVerifier); + } + } + else if (codeVerifier.empty()) + { + assignInvalidRequestError(L"Expected 'code_verifier', but none provided"); + } + + if (codeChallengeMethod == L"S256") + { + // We can't "unhash" the code challenge, so hash the code verifier and base64urlencode it + auto algo = HashAlgorithmProvider::OpenAlgorithm(HashAlgorithmNames::Sha256()); + auto hash = algo.HashData(CryptographicBuffer::ConvertStringToBinary(codeVerifier, BinaryStringEncoding::Utf8)); + auto base64Hash = CryptographicBuffer::EncodeToBase64String(hash); + std::wstring base64urlencodedHash; + base64urlencodedHash.reserve(base64Hash.size()); + for (auto ch : base64Hash) + { + switch (ch) + { + case '+': base64urlencodedHash.push_back('-'); break; + case '/': base64urlencodedHash.push_back('_'); break; + case '=': break; // No padding + default: base64urlencodedHash.push_back(ch); break; + } + } + + if (codeChallenge != base64urlencodedHash) + { + assignInvalidRequestError(L"The code verifier does not match the original code challenge"); + } + } + else if (codeChallengeMethod == L"plain") + { + if (codeChallenge != codeVerifier) + { + assignInvalidRequestError(L"Code verifier does not match the expected value"); + } + } + } + else if (grantType == L"password") + { + if (expectedGrantType != L"password") + { + assignMismatchedArgsError(L"grant_type", expectedGrantType, grantType); + } + else if (username.empty()) + { + assignMismatchedArgsError(L"username", L"username", L""); + } + else if (username != L"username") + { + assignMismatchedArgsError(L"username", L"username", username); + } + else if (password.empty()) + { + assignMismatchedArgsError(L"password", L"password", L""); + } + else if (password != L"password") + { + assignMismatchedArgsError(L"password", L"password", password); + } + + checkUnexpectedArg(L"code", code); + checkUnexpectedArg(L"code_verifier", codeVerifier); + checkUnexpectedArg(L"refresh_token", refreshToken); + + } + else if (grantType == L"client_credentials") + { + if (expectedGrantType != L"client") + { + assignMismatchedArgsError(L"grant_type", expectedGrantType, grantType); + } + + checkUnexpectedArg(L"code", code); + checkUnexpectedArg(L"code_verifier", codeVerifier); + checkUnexpectedArg(L"username", username); + checkUnexpectedArg(L"password", password); + checkUnexpectedArg(L"refresh_token", refreshToken); + } + else if (grantType == L"refresh_token") + { + if (expectedGrantType != L"refresh") + { + assignMismatchedArgsError(L"grant_type", expectedGrantType, grantType); + } + else if (refreshToken != refresh_token_old) + { + assignMismatchedArgsError(L"refresh_token", refresh_token_old, refreshToken); + } + + checkUnexpectedArg(L"code", code); + checkUnexpectedArg(L"code_verifier", codeVerifier); + checkUnexpectedArg(L"username", username); + checkUnexpectedArg(L"password", password); + } + else if (grantType == extension_grant_uri) + { + if (expectedGrantType != L"extension") + { + assignMismatchedArgsError(L"grant_type", expectedGrantType, grantType); + } + + checkUnexpectedArg(L"code", code); + checkUnexpectedArg(L"code_verifier", codeVerifier); + checkUnexpectedArg(L"username", username); + checkUnexpectedArg(L"password", password); + checkUnexpectedArg(L"refresh_token", refreshToken); + } + else + { + assignInvalidRequestError(L"Unrecognized grant type '"s + grantType + L"'"); + } + + if (errorString.empty()) + { + // NOTE: All responses are the same + responseJson = L"{\"access_token\":\""; + responseJson += json_escaped_token; + responseJson += L"\",\"token_type\":\"Bearer\",\"expires_in\":3600,\"refresh_token\":\""; + responseJson += json_escaped_refresh_token; + responseJson += L"\""; + if (scope.empty()) + { + responseJson += L",\"scope\":\"all\""; + } + responseJson += L"}"; + } + else + { + responseJson = L"{\"error\":\"" + errorString + L"\",\"error_description\":\"" + errorMessage + + L"\",\"error_uri\":\"" + error_uri + L"\",\"" + additional_param_key + L"\":\"" + + additional_param_value + L"\"}"; + } + + WINRT_ASSERT(!responseJson.empty()); + + HTTP_RESPONSE response = {}; + response.StatusCode = 200; + response.pReason = "OK"; + response.ReasonLength = 2; + + auto& contentTypeHeader = response.Headers.KnownHeaders[HttpHeaderContentType]; + contentTypeHeader.pRawValue = "application/json; charset=UTF-8"; + contentTypeHeader.RawValueLength = static_cast(::strlen(contentTypeHeader.pRawValue)); + + auto responseJsonUtf8 = winrt::to_string(responseJson); + HTTP_DATA_CHUNK dataChunk = {}; + dataChunk.DataChunkType = HttpDataChunkFromMemory; + dataChunk.FromMemory.pBuffer = responseJsonUtf8.data(); + dataChunk.FromMemory.BufferLength = static_cast(responseJsonUtf8.size()); + + response.EntityChunkCount = 1; + response.pEntityChunks = &dataChunk; + + ULONG bytesSent; + auto sendResult = ::HttpSendHttpResponse(m_requestQueue, request->RequestId, 0, &response, nullptr, &bytesSent, + nullptr, 0, nullptr, nullptr); + if (sendResult != NO_ERROR) + { + Log::Warning(WEX::Common::String().Format(L"HttpSendHttpResponse failed: %d", sendResult)); + } + } + + // Detours Information + static inline decltype(&ShellExecuteExW) RealShellExecuteW = &ShellExecuteExW; + + // Local server for performing the token exchange + wil::unique_event m_serverShutdownEvent{ wil::EventOptions::None }; + std::thread m_httpServerThread; + HANDLE m_requestQueue = nullptr; + HTTP_SERVER_SESSION_ID m_serverSessionId = 0; + HTTP_URL_GROUP_ID m_urlGroup = 0; + std::uint16_t m_serverPort = 50001; + std::wstring m_serverUrlBase; + }; +} diff --git a/test/OAuth2ManagerTests/OAuth2APITests.h b/test/OAuth2ManagerTests/OAuth2APITests.h new file mode 100644 index 0000000000..f1296c8363 --- /dev/null +++ b/test/OAuth2ManagerTests/OAuth2APITests.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + + +// The 'client_id' describes the behavior and expectations of our mocked authorization server +// Specifying grant type is required +#define GRANT_TYPE_CODE L"grant=code" +#define GRANT_TYPE_TOKEN L"grant=token" +#define GRANT_TYPE_PASSWORD L"grant=password" +#define GRANT_TYPE_CLIENT L"grant=client" +#define GRANT_TYPE_REFRESH L"grant=refresh" +#define GRANT_TYPE_EXTENSION L"grant=extension" + +// Specifying redirect type is required +#define REDIRECT_TYPE_LOCALHOST "&redirect=localhost" +#define REDIRECT_TYPE_PROTOCOL "&redirect=protocol" + +// 'none' is the default if not specified +#define AUTH_TYPE_HEADER "&auth=header" + +// 'none' is the default if not specified +#define TOKEN_ERROR_RESPONSE "&error=token" + +// 'false' is the default if not specified +#define ADDITIONAL_PARAMS "&additional_params=true" + +// Constants to validate expectations. The strings are specifically chosen to validate proper escaping of special characters +inline constexpr std::wstring_view error_description = L"This is an error & it contains characters like \"=\""; +inline constexpr std::wstring_view json_escaped_error_description = L"This is an error & it contains characters like \\\"=\\\""; +inline constexpr std::wstring_view error_uri = L"https://contoso.com/errors?foo=bar"; + +inline constexpr std::wstring_view additional_param_key = L"use=key&name=foo"; +inline constexpr std::wstring_view additional_param_value = L"use=value&name=bar"; + +inline constexpr std::wstring_view extension_grant_uri = L"oauth:test:extension"; + +inline constexpr std::wstring_view single_scope = L"foo=bar?"; +inline constexpr std::wstring_view multiple_scope = L"foo=bar? &\"foobar\""; + +inline constexpr std::wstring_view token = L"tacos=yummy&location=\"my tummy\""; +inline constexpr std::wstring_view json_escaped_token = L"tacos=yummy&location=\\\"my tummy\\\""; +inline constexpr std::wstring_view refresh_token_old = L"~!@#$%^&*()_+`-=[]\\{};':\",./<>?-old"; +inline constexpr std::wstring_view refresh_token = L"~!@#$%^&*()_+`-=[]\\{};':\",./<>?"; +inline constexpr std::wstring_view json_escaped_refresh_token = L"~!@#$%^&*()_+`-=[]\\\\{};':\\\",./<>?"; + +struct uri_builder +{ + std::wstring uri; + wchar_t prefix; + + uri_builder(const winrt::Windows::Foundation::Uri& uri, bool useQuery = true) : + uri(uri.RawUri()) + { + if (useQuery) + { + prefix = uri.Query().empty() ? L'?' : '&'; + } + else + { + prefix = '#'; + } + } + + void add(std::wstring_view name, std::wstring_view value) + { + assert(!name.empty() && !value.empty()); + + uri.push_back(prefix); + prefix = L'&'; + uri.append(winrt::Windows::Foundation::Uri::EscapeComponent(name)); + uri.push_back(L'='); + uri.append(winrt::Windows::Foundation::Uri::EscapeComponent(value)); + } + + void add_optional(std::wstring_view name, std::wstring_view value) + { + if (!value.empty()) + { + add(name, value); + } + } + + winrt::Windows::Foundation::Uri get() + { + return winrt::Windows::Foundation::Uri{ uri }; + } +}; diff --git a/test/OAuth2ManagerTests/OAuth2ManagerTests.vcxproj b/test/OAuth2ManagerTests/OAuth2ManagerTests.vcxproj new file mode 100644 index 0000000000..d687253210 --- /dev/null +++ b/test/OAuth2ManagerTests/OAuth2ManagerTests.vcxproj @@ -0,0 +1,292 @@ + + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM64 + + + Release + ARM64 + + + + 17.0 + Win32Proj + {0ff6a68f-6c7f-4e66-8cb8-c0b9501060ca} + OAuth2ManagerTests + 10.0 + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Level3 + true + WIN32;_DEBUG;OAUTH2MANAGERTESTS_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir)..\inc;$(ProjectDir)..\..\dev\WindowsAppRuntime_BootstrapDLL\;"D:\WindowsAppSDKInternal\WindowsAppSDK\dev\Detours" + + + Windows + true + false + + + + + Level3 + true + true + true + WIN32;NDEBUG;OAUTH2MANAGERTESTS_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + + + Windows + true + true + true + false + + + + + httpapi.lib;%(AdditionalDependencies) + + + + + Level3 + true + _DEBUG;OAUTH2MANAGERTESTS_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + + ..\..\dev\Detours; + ..\..\dev\Common; + $(OutDir)\..\WindowsAppRuntime_DLL; + $(OutDir)\..\WindowsAppRuntime_BootstrapDLL; + %(AdditionalIncludeDirectories) + + + + Windows + true + false + Microsoft.WindowsAppRuntime.dll;%(DelayLoadDLLs) + + + + + Level3 + true + true + true + NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir)..\inc;$(ProjectDir)..\..\dev\WindowsAppRuntime_BootstrapDLL\ + + + Windows + true + true + true + false + + + + + Level3 + true + _DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir)..\inc;$(ProjectDir)..\..\dev\WindowsAppRuntime_BootstrapDLL\ + + + Windows + true + false + + + + + Level3 + true + true + true + NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + $(ProjectDir)..\inc;$(ProjectDir)..\..\dev\WindowsAppRuntime_BootstrapDLL\ + + + Windows + true + true + true + false + + + + + + + + + + Create + Create + Create + Create + Create + Create + + + + + + + + + + + .Debug + _Debug + $(AppxPackageDir)\OAuthTestAppPackage_1.0.0.0_$(PlatformTarget)$(TestPkgDebugConfigName)_Test + D:\WindowsAppSDKInternal\WindowsAppSDK\BuildOutput\Debug\x86\AppxPackages\OAuthTestAppPackage_1.0.0.0_x86_Debug_Test\OAuthTestAppPackage_1.0.0.0_x86_Debug.msix + + + + + + + + $(OutDir)\..\WindowsAppRuntime_DLL\Microsoft.Security.Authentication.OAuth.winmd + true + $(OutDir)\..\WindowsAppRuntime_DLL\Microsoft.WindowsAppRuntime.dll + + + + + {d6bc25c5-1aa7-4c4a-a02c-b42dedbfea33} + + + {f76b776e-86f5-48c5-8fc7-d2795ecc9746} + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/test/OAuth2ManagerTests/OAuth2ManagerTests.vcxproj.filters b/test/OAuth2ManagerTests/OAuth2ManagerTests.vcxproj.filters new file mode 100644 index 0000000000..1e57c7b1be --- /dev/null +++ b/test/OAuth2ManagerTests/OAuth2ManagerTests.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/test/OAuth2ManagerTests/packages.config b/test/OAuth2ManagerTests/packages.config new file mode 100644 index 0000000000..0184e4db4f --- /dev/null +++ b/test/OAuth2ManagerTests/packages.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/OAuth2ManagerTests/pch.cpp b/test/OAuth2ManagerTests/pch.cpp new file mode 100644 index 0000000000..64b7eef6d6 --- /dev/null +++ b/test/OAuth2ManagerTests/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/test/OAuth2ManagerTests/pch.h b/test/OAuth2ManagerTests/pch.h new file mode 100644 index 0000000000..81b481856c --- /dev/null +++ b/test/OAuth2ManagerTests/pch.h @@ -0,0 +1,25 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +// add headers that you want to pre-compile here +#ifndef INLINE_TEST_METHOD_MARKUP +#define INLINE_TEST_METHOD_MARKUP +#endif + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#include + +#include "WexTestClass.h" +#include +#include +#include +#include + +#include +#endif //PCH_H diff --git a/test/TestApps/OAuthTestApp/OAuthTestApp.vcxproj b/test/TestApps/OAuthTestApp/OAuthTestApp.vcxproj new file mode 100644 index 0000000000..e8268cf612 --- /dev/null +++ b/test/TestApps/OAuthTestApp/OAuthTestApp.vcxproj @@ -0,0 +1,152 @@ + + + + + true + true + true + true + 15.0 + {4caa3052-7fae-4c5b-a1cb-02d7f910c991} + Win32Proj + OAuthTestApp + 10.0.26100.0 + 10.0.17134.0 + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + Application + v143 + v142 + v141 + v140 + Unicode + + + true + true + + + false + true + false + + + + + + + + + + + + + + + + Use + pch.h + $(IntDir)pch.pch + _CONSOLE;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + %(AdditionalOptions) /permissive- /bigobj + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + Console + false + + + + + WIN32;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + Console + true + true + false + + + + + + + + + Create + + + + + + + false + + + + + {f76b776e-86f5-48c5-8fc7-d2795ecc9746} + + + + + $(BaseOutputPath)\WindowsAppRuntime_DLL\Microsoft.Windows.AppLifecycle.winmd + true + $(OutDir)\..\WindowsAppRuntime_DLL\Microsoft.WindowsAppRuntime.dll + + + $(OutDir)\..\WindowsAppRuntime_DLL\Microsoft.Security.Authentication.OAuth.winmd + true + $(OutDir)\..\WindowsAppRuntime_DLL\Microsoft.WindowsAppRuntime.dll + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + \ No newline at end of file diff --git a/test/TestApps/OAuthTestApp/OAuthTestApp.vcxproj.filters b/test/TestApps/OAuthTestApp/OAuthTestApp.vcxproj.filters new file mode 100644 index 0000000000..388ab2e234 --- /dev/null +++ b/test/TestApps/OAuthTestApp/OAuthTestApp.vcxproj.filters @@ -0,0 +1,37 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + + + Source Files + + + Source Files + + + + + + + + + + \ No newline at end of file diff --git a/test/TestApps/OAuthTestApp/PropertySheet.props b/test/TestApps/OAuthTestApp/PropertySheet.props new file mode 100644 index 0000000000..b0c622690f --- /dev/null +++ b/test/TestApps/OAuthTestApp/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/test/TestApps/OAuthTestApp/main.cpp b/test/TestApps/OAuthTestApp/main.cpp new file mode 100644 index 0000000000..06e2bc385a --- /dev/null +++ b/test/TestApps/OAuthTestApp/main.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include +#include +#include +#include +#include +#include +using namespace winrt::Microsoft::Windows::AppLifecycle; +using namespace winrt::Microsoft::Security::Authentication::OAuth; +using namespace winrt::Windows::ApplicationModel::Activation; + +int main() +{ + try + { + auto args = AppInstance::GetCurrent().GetActivatedEventArgs(); + auto kind = args.Kind(); + if (kind == ExtendedActivationKind::Protocol) + { + auto uri = args.Data().as().Uri(); + if (!OAuth2Manager::CompleteAuthRequest(uri)) + { + std::printf("WARNING: Failed to complete auth request with uri '%ls'.\n", uri.RawUri().c_str()); + std::printf("WARNING: This may or may not be expected depending on which test is running.\n"); + } + } + else + { + std::printf("WARNING: Application was launched with something other than protocol activation!\n"); + }; + } + catch (const std::exception& ex) + { + std::printf("Standard exception: %s\n", ex.what()); + } +} diff --git a/test/TestApps/OAuthTestApp/packages.config b/test/TestApps/OAuthTestApp/packages.config new file mode 100644 index 0000000000..cbf6205e62 --- /dev/null +++ b/test/TestApps/OAuthTestApp/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/TestApps/OAuthTestApp/pch.cpp b/test/TestApps/OAuthTestApp/pch.cpp new file mode 100644 index 0000000000..bcb5590be1 --- /dev/null +++ b/test/TestApps/OAuthTestApp/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/test/TestApps/OAuthTestApp/pch.h b/test/TestApps/OAuthTestApp/pch.h new file mode 100644 index 0000000000..2eec5b9225 --- /dev/null +++ b/test/TestApps/OAuthTestApp/pch.h @@ -0,0 +1,3 @@ +#pragma once +#include +#include diff --git a/test/TestApps/OAuthTestApp/readme.txt b/test/TestApps/OAuthTestApp/readme.txt new file mode 100644 index 0000000000..17d67ecada --- /dev/null +++ b/test/TestApps/OAuthTestApp/readme.txt @@ -0,0 +1,30 @@ +======================================================================== + C++/WinRT OAuthTestApp Project Overview +======================================================================== + +This project demonstrates how to get started consuming Windows Runtime +classes directly from standard C++, using platform projection headers +generated from Windows SDK metadata files. + +Steps to generate and consume SDK platform projection: +1. Build project initially to generate platform projection headers into + your Generated Files folder. +2. Include a projection namespace header in your pch.h, such as + . +3. Consume winrt namespace and any Windows Runtime namespaces, such as + winrt::Windows::Foundation, from source code. +4. Initialize apartment via init_apartment() and consume winrt classes. + +Steps to generate and consume a projection from third party metadata: +1. Add a WinMD reference by right-clicking the References project node + and selecting "Add Reference...". In the Add References dialog, + browse to the component WinMD you want to consume and add it. +2. Build the project once to generate projection headers for the + referenced WinMD file under the "Generated Files" subfolder. +3. As above, include projection headers in pch or source code + to consume projected Windows Runtime classes. + +======================================================================== +Learn more about C++/WinRT here: +http://aka.ms/cppwinrt/ +======================================================================== diff --git a/test/TestApps/OAuthTestAppPackage/Images/LockScreenLogo.scale-200.png b/test/TestApps/OAuthTestAppPackage/Images/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..735f57adb5 Binary files /dev/null and b/test/TestApps/OAuthTestAppPackage/Images/LockScreenLogo.scale-200.png differ diff --git a/test/TestApps/OAuthTestAppPackage/Images/SplashScreen.scale-200.png b/test/TestApps/OAuthTestAppPackage/Images/SplashScreen.scale-200.png new file mode 100644 index 0000000000..023e7f1fed Binary files /dev/null and b/test/TestApps/OAuthTestAppPackage/Images/SplashScreen.scale-200.png differ diff --git a/test/TestApps/OAuthTestAppPackage/Images/Square150x150Logo.scale-200.png b/test/TestApps/OAuthTestAppPackage/Images/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..af49fec1a5 Binary files /dev/null and b/test/TestApps/OAuthTestAppPackage/Images/Square150x150Logo.scale-200.png differ diff --git a/test/TestApps/OAuthTestAppPackage/Images/Square44x44Logo.scale-200.png b/test/TestApps/OAuthTestAppPackage/Images/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..ce342a2ec8 Binary files /dev/null and b/test/TestApps/OAuthTestAppPackage/Images/Square44x44Logo.scale-200.png differ diff --git a/test/TestApps/OAuthTestAppPackage/Images/Square44x44Logo.targetsize-24_altform-unplated.png b/test/TestApps/OAuthTestAppPackage/Images/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..f6c02ce97e Binary files /dev/null and b/test/TestApps/OAuthTestAppPackage/Images/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/test/TestApps/OAuthTestAppPackage/Images/StoreLogo.png b/test/TestApps/OAuthTestAppPackage/Images/StoreLogo.png new file mode 100644 index 0000000000..7385b56c0e Binary files /dev/null and b/test/TestApps/OAuthTestAppPackage/Images/StoreLogo.png differ diff --git a/test/TestApps/OAuthTestAppPackage/Images/Wide310x150Logo.scale-200.png b/test/TestApps/OAuthTestAppPackage/Images/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..288995b397 Binary files /dev/null and b/test/TestApps/OAuthTestAppPackage/Images/Wide310x150Logo.scale-200.png differ diff --git a/test/TestApps/OAuthTestAppPackage/OAuthTestAppPackage.wapproj b/test/TestApps/OAuthTestAppPackage/OAuthTestAppPackage.wapproj new file mode 100644 index 0000000000..3402cf21de --- /dev/null +++ b/test/TestApps/OAuthTestAppPackage/OAuthTestAppPackage.wapproj @@ -0,0 +1,98 @@ + + + + + 15.0 + + + + + Debug + x86 + + + Release + x86 + + + Debug + x64 + + + Release + x64 + + + Debug + ARM + + + Release + ARM + + + Debug + ARM64 + + + Release + ARM64 + + + Debug + AnyCPU + + + Release + AnyCPU + + + + + $(MSBuildExtensionsPath)\Microsoft\DesktopBridge\ + + + + + + 455c01f8-0a3e-42c4-9f22-13992eb909ec + 10.0.26100.0 + 10.0.17763.0 + en-US + True + ..\OAuthTestApp\OAuthTestApp.vcxproj + true + False + $(RepoTestCertificatePFX) + $(RepoTestCertificatePassword) + false + SHA256 + True + True + $(Platform) + 0 + + + + + Designer + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/TestApps/OAuthTestAppPackage/Package.appxmanifest b/test/TestApps/OAuthTestAppPackage/Package.appxmanifest new file mode 100644 index 0000000000..df55447d89 --- /dev/null +++ b/test/TestApps/OAuthTestAppPackage/Package.appxmanifest @@ -0,0 +1,65 @@ + + + + + + + + OAuthTestAppPackage + Microsoft Corporation + Images\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + OAuth Test App + + + + + + + + + + + + + + + + + +