diff --git a/msix/storeapi/StoreApi.cpp b/msix/storeapi/StoreApi.cpp index 4cb550090..378327ce8 100644 --- a/msix/storeapi/StoreApi.cpp +++ b/msix/storeapi/StoreApi.cpp @@ -1,11 +1,21 @@ +#include "StoreApi.hpp" + +#include +#include + +#include +#include +#include // For strnlen +#include +#include + +#include "framework.hpp" + #ifndef DNDEBUG #include #include #endif -#include "StoreApi.hpp" -#include "framework.hpp" - // Syntactic sugar to convert the enum [value] into a Int. constexpr Int toInt(StoreApi::ErrorCode value) { return static_cast(value); @@ -24,9 +34,9 @@ static constexpr std::size_t MaxProductIdLen = 129; StoreApi::ErrorCode validateArg(const char* input, std::size_t maxLength); void logError(std::string_view functionName, std::string_view errMsg) { - #ifndef DNDEBUG - std::cerr << std::format("storeapi: {}: {}\n", functionName , errMsg); - #endif +#ifndef DNDEBUG + std::cerr << std::format("storeapi: {}: {}\n", functionName, errMsg); +#endif } #define LOG_ERROR(msg) \ @@ -75,10 +85,9 @@ Int GenerateUserJWT(const char* accessToken, char** userJWT, } try { - auto user = StoreApi::UserInfo::Current().get(); - StoreApi::ServerStoreService service{}; - const std::string jwt = service.GenerateUserJwt(accessToken, user).get(); + auto user = service.CurrentUserInfo(); + const std::string jwt = service.GenerateUserJwt(accessToken, user); // Allocates memory using some OS API so we can free this buffer on the // other side of the ABI without assumptions on specifics of the programming diff --git a/msix/storeapi/StoreApi.hpp b/msix/storeapi/StoreApi.hpp index d863af91b..4261447b1 100644 --- a/msix/storeapi/StoreApi.hpp +++ b/msix/storeapi/StoreApi.hpp @@ -5,7 +5,6 @@ // the ABI. Zero or positive values have no special meaning other than success. #pragma once -#include #include extern "C" { @@ -37,5 +36,4 @@ DLL_EXPORT Int GetSubscriptionExpirationDate(const char* productID, DLL_EXPORT Int GenerateUserJWT(const char* accessToken, // output char** userJWT, std::uint64_t* userJWTLen); - } diff --git a/msix/storeapi/storeapi.vcxproj b/msix/storeapi/storeapi.vcxproj index 5e5e17d9e..8c063c59d 100644 --- a/msix/storeapi/storeapi.vcxproj +++ b/msix/storeapi/storeapi.vcxproj @@ -43,9 +43,11 @@ - - UP4W_TEST_WITH_MS_STORE_MOCK - + + + UP4W_TEST_WITH_MS_STORE_MOCK;%(PreProcessorDefinitions) + + Level3 @@ -97,7 +99,6 @@ - diff --git a/storeapi/agent/ServerStoreService.cpp b/storeapi/agent/ServerStoreService.cpp deleted file mode 100644 index 33905a6c1..000000000 --- a/storeapi/agent/ServerStoreService.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "ServerStoreService.hpp" - -#include -#include -#include -#include - -namespace StoreApi { - -using winrt::Windows::Foundation::IInspectable; -using winrt::Windows::Security::Cryptography::BinaryStringEncoding; -using winrt::Windows::Security::Cryptography::CryptographicBuffer; -using winrt::Windows::Security::Cryptography::Core::HashAlgorithmNames; -using winrt::Windows::Security::Cryptography::Core::HashAlgorithmProvider; -using winrt::Windows::System::KnownUserProperties; -using winrt::Windows::System::User; -using winrt::Windows::System::UserAuthenticationStatus; -using winrt::Windows::System::UserType; - -using concurrency::task; - -winrt::hstring sha256(winrt::hstring input) { - auto inputUtf8 = CryptographicBuffer::ConvertStringToBinary( - input, BinaryStringEncoding::Utf8); - auto hasher = - HashAlgorithmProvider::OpenAlgorithm(HashAlgorithmNames::Sha256()); - return CryptographicBuffer::EncodeToHexString(hasher.HashData(inputUtf8)); -} - -task UserInfo::Current() { - // The preferred strategy (defined in the spec) is a plain SHA256 hash of the - // user email acquired from the runtime API, which provides the AccountName - // property of the current user. - auto users = co_await User::FindAllAsync( - UserType::LocalUser, UserAuthenticationStatus::LocallyAuthenticated); - - auto howManyUsers = users.Size(); - if (howManyUsers < 1) { - throw Exception(ErrorCode::NoLocalUser); - } - - if (howManyUsers > 1) { - throw Exception(ErrorCode::TooManyLocalUsers, - std::format("Expected one but found {}", howManyUsers)); - } - - IInspectable accountName = co_await users.GetAt(0).GetPropertyAsync( - KnownUserProperties::AccountName()); - auto name = winrt::unbox_value(accountName); - if (name.size() == 0) { - co_return {}; - } - - co_return UserInfo{.id = sha256(name)}; -} - -} // namespace StoreApi diff --git a/storeapi/agent/ServerStoreService.hpp b/storeapi/agent/ServerStoreService.hpp index 050f01894..0a2e46093 100644 --- a/storeapi/agent/ServerStoreService.hpp +++ b/storeapi/agent/ServerStoreService.hpp @@ -1,13 +1,12 @@ #pragma once -#include - -#include "base/DefaultContext.hpp" -#include "base/Exception.hpp" -#include "base/StoreService.hpp" - +#include +#include +#include #include #include +#include #include +#include namespace StoreApi { @@ -15,10 +14,7 @@ namespace StoreApi { // when talking to external business servers about the subscription. struct UserInfo { // The user ID that should be tracked in the Contract Server. - winrt::hstring id; - - // An asynchronous factory returning [UserInfo] of the current user. - static concurrency::task Current(); + std::string id; }; // Adds functionality on top of the [StoreService] interesting to background @@ -28,20 +24,18 @@ class ServerStoreService : public StoreService { public: // Generates the user ID key (a.k.a the JWT) provided the server AAD [token] // and the [user] info whose ID the caller wants to have encoded in the JWT. - concurrency::task GenerateUserJwt(std::string token, - UserInfo user) { + std::string GenerateUserJwt(std::string token, UserInfo user) const { if (user.id.empty()) { throw Exception(StoreApi::ErrorCode::NoLocalUser); } - auto hToken = winrt::to_hstring(token); - auto jwt = co_await this->context.GenerateUserJwt(hToken, user.id); + auto jwt = this->context.GenerateUserJwt(token, user.id); if (jwt.empty()) { throw Exception(ErrorCode::EmptyJwt, std::format("access token: {}", token)); } - co_return winrt::to_string(jwt); + return jwt; } // Returns the expiration time as the number of seconds since Unix epoch of @@ -60,6 +54,23 @@ class ServerStoreService : public StoreService { // just need to convert the duration to seconds. return duration_cast(dur).count(); } + + // A factory returning the current user's [UserInfo]. + UserInfo CurrentUserInfo() const { + auto hashes = this->context.AllLocallyAuthenticatedUserHashes(); + + auto howManyUsers = hashes.size(); + if (howManyUsers < 1) { + throw Exception(ErrorCode::NoLocalUser); + } + + if (howManyUsers > 1) { + throw Exception(ErrorCode::TooManyLocalUsers, + std::format("Expected one but found {}", howManyUsers)); + } + + return UserInfo{.id = hashes[0]}; + } }; } // namespace StoreApi diff --git a/storeapi/base/impl/StoreContext.cpp b/storeapi/base/impl/StoreContext.cpp index c942d76f4..3a5b35bee 100644 --- a/storeapi/base/impl/StoreContext.cpp +++ b/storeapi/base/impl/StoreContext.cpp @@ -3,6 +3,12 @@ #include "StoreContext.hpp" #include +#include +#include +#include +#include +#include +#include #include #include @@ -29,6 +35,9 @@ std::vector to_hstrings(std::span input); // Translates a [StorePurchaseStatus] into the [PurchaseStatus] enum. PurchaseStatus translate(StorePurchaseStatus purchaseStatus) noexcept; + +// Returns a hstring representation of a SHA256 sum of the input hstring. +winrt::hstring sha256(winrt::hstring input); } // namespace std::chrono::system_clock::time_point @@ -85,6 +94,15 @@ std::vector StoreContext::GetProducts( return products; } +std::string StoreContext::GenerateUserJwt(std::string token, + std::string userId) const { + assert(!token.empty() && "Azure AD token is required"); + auto hJwt = self.GetCustomerPurchaseIdAsync(winrt::to_hstring(token), + winrt::to_hstring(userId)) + .get(); + return winrt::to_string(hJwt); +} + void StoreContext::InitDialogs(Window parentWindow) { // Apps that do not feature a [CoreWindow] must inform the runtime the parent // window handle in order to render runtime provided UI elements, such as @@ -93,6 +111,34 @@ void StoreContext::InitDialogs(Window parentWindow) { iiw->Initialize(parentWindow); } +std::vector StoreContext::AllLocallyAuthenticatedUserHashes() { + using winrt::Windows::Foundation::IInspectable; + using winrt::Windows::System::KnownUserProperties; + using winrt::Windows::System::User; + using winrt::Windows::System::UserAuthenticationStatus; + using winrt::Windows::System::UserType; + + // This should really return a single user, but the API is specified in terms + // of a collection, so let's not assume too much. + auto users = + User::FindAllAsync(UserType::LocalUser, + UserAuthenticationStatus::LocallyAuthenticated) + .get(); + + std::vector allHashes; + allHashes.reserve(users.Size()); + for (auto user : users) { + IInspectable accountName = + user.GetPropertyAsync(KnownUserProperties::AccountName()).get(); + auto name = winrt::unbox_value(accountName); + if (!name.empty()) { + allHashes.push_back(winrt::to_string(sha256(name))); + } + } + + return allHashes; +} + namespace { std::vector to_hstrings(std::span input) { std::vector hStrs; @@ -119,8 +165,22 @@ PurchaseStatus translate(StorePurchaseStatus purchaseStatus) noexcept { assert(false && "Missing enum elements to translate StorePurchaseStatus."); return StoreApi::PurchaseStatus::Unknown; // To be future proof. } + +winrt::hstring sha256(winrt::hstring input) { + using winrt::Windows::Security::Cryptography::BinaryStringEncoding; + using winrt::Windows::Security::Cryptography::CryptographicBuffer; + using winrt::Windows::Security::Cryptography::Core::HashAlgorithmNames; + using winrt::Windows::Security::Cryptography::Core::HashAlgorithmProvider; + + auto inputUtf8 = CryptographicBuffer::ConvertStringToBinary( + winrt::to_hstring(input), BinaryStringEncoding::Utf8); + auto hasher = + HashAlgorithmProvider::OpenAlgorithm(HashAlgorithmNames::Sha256()); + return CryptographicBuffer::EncodeToHexString(hasher.HashData(inputUtf8)); +} + } // namespace } // namespace StoreApi::impl -#endif // UP4W_TEST_WITH_MS_STORE_MOCK +#endif // UP4W_TEST_WITH_MS_STORE_MOCK diff --git a/storeapi/base/impl/StoreContext.hpp b/storeapi/base/impl/StoreContext.hpp index b16b824bf..acfcb0447 100644 --- a/storeapi/base/impl/StoreContext.hpp +++ b/storeapi/base/impl/StoreContext.hpp @@ -11,9 +11,6 @@ // For the underlying Store API #include -// To provide the WinRT coroutine types. -#include - // For HWND and GUI-related Windows types. #include @@ -70,17 +67,18 @@ class StoreContext { std::vector GetProducts(std::span kinds, std::span ids) const; - // Generates the user ID key (a.k.a the JWT) provided the server AAD [hToken] - // and the [hUserId] the caller wants to have encoded in the JWT. - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - return self.GetCustomerPurchaseIdAsync(hToken, hUserId); - } + // Generates the user ID key (a.k.a the JWT) provided the server AAD [token] + // and the [userId] the caller wants to have encoded in the JWT. + std::string GenerateUserJwt(std::string token, std::string userId) const; // Initializes the GUI "subsystem" with the [parentWindow] handle so we can // render native dialogs, such as when purchase or other kinds of // authorization are required. void InitDialogs(Window parentWindow); + + // Returns a collection of hashes of all locally authenticated users running + // in this session. Most likely the collection will contain a single element. + static std::vector AllLocallyAuthenticatedUserHashes(); }; } // namespace StoreApi::impl diff --git a/storeapi/test/CMakeLists.txt b/storeapi/test/CMakeLists.txt index 6acefe58e..e3cd0a0b7 100644 --- a/storeapi/test/CMakeLists.txt +++ b/storeapi/test/CMakeLists.txt @@ -15,7 +15,7 @@ set(StoreApi_SRCS "../base/Exception.hpp" "../base/Purchase.hpp" "../base/StoreService.hpp" - "../agent/ServerStoreService.cpp" + "../agent/ServerStoreService.hpp" "../gui/ClientStoreService.hpp" ) diff --git a/storeapi/test/ServerStoreServiceTest.cpp b/storeapi/test/ServerStoreServiceTest.cpp index a2e154405..e822b17c6 100644 --- a/storeapi/test/ServerStoreServiceTest.cpp +++ b/storeapi/test/ServerStoreServiceTest.cpp @@ -8,41 +8,42 @@ namespace StoreApi { using namespace ::testing; -TEST(UserInfo, PredictableSizes) { - auto user = UserInfo::Current().get(); - auto size = user.id.size(); - EXPECT_TRUE(size == 0 || size == 64) - << "User ID of unexpected size: " << size << " <" - << winrt::to_string(user.id) << '\n'; +TEST(ServerStoreService, NoUsersLikeInCI) { + auto service = ServerStoreService{}; + EXPECT_THROW({auto user = service.CurrentUserInfo();}, Exception); +} + +TEST(ServerStoreService, TooManyUsers) { + auto service = ServerStoreService{}; + EXPECT_THROW({auto user = service.CurrentUserInfo();}, Exception); +} + +TEST(ServerStoreService, FindOneUser) { + static constexpr char goodHash[] = "goodHash"; + auto service = ServerStoreService{}; + FindOneUserContext::goodHash = goodHash; + auto user = service.CurrentUserInfo(); + EXPECT_EQ(user.id, goodHash); } TEST(ServerStoreService, EmptyJwtThrows) { auto service = ServerStoreService{}; - UserInfo user{.id = L"my@name.com"}; + UserInfo user{.id = "my@name.com"}; EXPECT_THROW( { - auto jwt = service.GenerateUserJwt("this-is-a-web-token", user).get(); + auto jwt = service.GenerateUserJwt("this-is-a-web-token", user); }, Exception); } TEST(ServerStoreService, NonEmptyJwtNeverThrows) { auto service = ServerStoreService{}; - UserInfo user{.id = L"my@name.com"}; + UserInfo user{.id = "my@name.com"}; std::string token{"this-is-a-web-token"}; - auto jwt = service.GenerateUserJwt(token, user).get(); + auto jwt = service.GenerateUserJwt(token, user); EXPECT_EQ(token, jwt); } -TEST(ServerStoreService, RealServerFailsUnderTest) { - auto service = ServerStoreService{}; - UserInfo user{.id = L"my@name.com"}; - std::string token{"this-is-a-web-token"}; - // This fails because the test is not an app deployed through the store. - EXPECT_THROW({ auto jwt = service.GenerateUserJwt(token, user).get(); }, - Exception); -} - TEST(ServerStoreService, ExpirationDateUnsubscribed) { auto service = ServerStoreService{}; diff --git a/storeapi/test/stubs.hpp b/storeapi/test/stubs.hpp index 2a101fda4..c2f46aa36 100644 --- a/storeapi/test/stubs.hpp +++ b/storeapi/test/stubs.hpp @@ -4,23 +4,16 @@ #include #include -// For WinRT basic types and coroutines. -#include -// For non-WinRT coroutines -#include - -// Win32 APIs, such as the Timezone -#include - #include #include #include #include #include #include +// For timegm +#include #if defined _MSC_VER -#include #include #define timegm _mkgmtime #endif @@ -89,9 +82,8 @@ struct EmptyJwtContext { return {Product{.kind = kinds[0], .id = ids[0]}}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return {}; + std::string GenerateUserJwt(std::string hToken, std::string hUserId) const { + return {}; } }; @@ -107,9 +99,8 @@ struct IdentityJwtContext { return {Product{.kind = kinds[0], .id = ids[0]}}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; + std::string GenerateUserJwt(std::string hToken, std::string hUserId) const { + return hToken; } }; @@ -132,9 +123,8 @@ struct NeverSubscribedContext { return {Product{.kind = kinds[0], .id = ids[0]}}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; + std::string GenerateUserJwt(std::string hToken, std::string hUserId) const { + return hToken; } }; @@ -158,9 +148,8 @@ struct UnixEpochContext { return {Product{.kind = kinds[0], .id = ids[0]}}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; + std::string GenerateUserJwt(std::string hToken, std::string hUserId) const { + return hToken; } }; @@ -187,9 +176,8 @@ struct AlreadyPurchasedContext { return {Product{.kind = kinds[0], .id = ids[0]}}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; + std::string GenerateUserJwt(std::string hToken, std::string hUserId) const { + return hToken; } // noop @@ -219,11 +207,32 @@ struct PurchaseSuccessContext { return {Product{.kind = kinds[0], .id = ids[0]}}; } - winrt::Windows::Foundation::IAsyncOperation GenerateUserJwt( - winrt::hstring hToken, winrt::hstring hUserId) { - co_return hToken; + std::string GenerateUserJwt(std::string hToken, std::string hUserId) const { + return hToken; } // noop void InitDialogs(Window window) {} }; + +struct TooManyUsersContext { + struct Product {}; + std::vector AllLocallyAuthenticatedUserHashes() const { + return {"first-user", "second-user"}; + } +}; + +struct NoUsersContext { + struct Product {}; + std::vector AllLocallyAuthenticatedUserHashes() const { + return {}; + } +}; + +struct FindOneUserContext { + struct Product {}; + static inline std::string goodHash{}; + std::vector AllLocallyAuthenticatedUserHashes() const { + return {goodHash}; + } +};