// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include #include ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_PLATFORM_WINDOWS # define timegm _mkgmtime #endif // ZEN_PLATFORM_WINDOWS namespace zen { namespace httpclientauth { using namespace std::literals; std::function CreateFromStaticToken(HttpClientAccessToken Token) { return [Token]() { return Token; }; } std::function CreateFromStaticToken(std::string_view Token) { return CreateFromStaticToken( HttpClientAccessToken{.Value = fmt::format("Bearer {}"sv, Token), .ExpireTime = HttpClientAccessToken::TimePoint::max()}); } std::function CreateFromOAuthClientCredentials(const OAuthClientCredentialsParams& Params) { OAuthClientCredentialsParams OAuthParams(Params); return [OAuthParams]() { using namespace std::chrono; std::string Body = fmt::format("client_id={}&scope=cache_access&grant_type=client_credentials&client_secret={}"sv, OAuthParams.ClientId, OAuthParams.ClientSecret); HttpClient Http{OAuthParams.Url}; IoBuffer Payload{IoBuffer::Wrap, Body.data(), Body.size()}; // TODO: ensure this gets the right Content-Type passed along HttpClient::Response Response = Http.Post("", Payload, {{"Content-Type", "application/x-www-form-urlencoded"}}); if (!Response || Response.StatusCode != HttpResponseCode::OK) { ZEN_WARN("Failed fetching OAuth access token {}. Reason: '{}'", OAuthParams.Url, Response.ErrorMessage("")); return HttpClientAccessToken{}; } std::string JsonError; json11::Json Json = json11::Json::parse(std::string{Response.AsText()}, JsonError); if (JsonError.empty() == false) { ZEN_WARN("Unable to parse OAuth json response from {}. Reason: '{}'", OAuthParams.Url, JsonError); return HttpClientAccessToken{}; } std::string Token = Json["access_token"].string_value(); int64_t ExpiresInSeconds = static_cast(Json["expires_in"].int_value()); HttpClientAccessToken::TimePoint ExpireTime = HttpClientAccessToken::Clock::now() + seconds(ExpiresInSeconds); return HttpClientAccessToken{.Value = fmt::format("Bearer {}"sv, Token), .ExpireTime = ExpireTime}; }; } std::function CreateFromOpenIdProvider(AuthMgr& AuthManager, std::string_view OpenIdProvider) { return [&AuthManager = AuthManager, OpenIdProvider = std::string(OpenIdProvider)]() { AuthMgr::OpenIdAccessToken Token = AuthManager.GetOpenIdAccessToken(OpenIdProvider); return HttpClientAccessToken{.Value = Token.AccessToken, .ExpireTime = Token.ExpireTime}; }; } std::function CreateFromDefaultOpenIdProvider(AuthMgr& AuthManager) { return CreateFromOpenIdProvider(AuthManager, "Default"sv); } static HttpClientAccessToken GetOidcTokenFromExe(const std::filesystem::path& OidcExecutablePath, std::string_view CloudHost, bool Unattended, bool Quiet, bool Hidden) { Stopwatch Timer; CreateProcOptions ProcOptions; if (Quiet) { ProcOptions.StdoutFile = std::filesystem::temp_directory_path() / fmt::format(".zen-auth-output-{}", Oid::NewOid()); } if (Hidden) { ProcOptions.Flags |= CreateProcOptions::Flag_NoConsole; } const std::filesystem::path AuthTokenPath(std::filesystem::temp_directory_path() / fmt::format(".zen-auth-{}", Oid::NewOid())); auto _ = MakeGuard([AuthTokenPath, &ProcOptions]() { RemoveFile(AuthTokenPath); if (!ProcOptions.StdoutFile.empty()) { RemoveFile(ProcOptions.StdoutFile); } }); const std::string ProcArgs = fmt::format("{} --AuthConfigUrl {} --OutFile {} --Unattended={}", OidcExecutablePath, CloudHost, AuthTokenPath, Unattended ? "true"sv : "false"sv); ZEN_DEBUG("Running: {}", ProcArgs); ProcessHandle Proc; Proc.Initialize(CreateProc(OidcExecutablePath, ProcArgs, ProcOptions)); if (!Proc.IsValid()) { throw std::runtime_error(fmt::format("failed to launch '{}'", OidcExecutablePath)); } int ExitCode = Proc.WaitExitCode(); if (ExitCode == 0) { IoBuffer Body = IoBufferBuilder::MakeFromFile(AuthTokenPath); std::string JsonText(reinterpret_cast(Body.GetData()), Body.GetSize()); std::string JsonError; json11::Json Json = json11::Json::parse(JsonText, JsonError); if (JsonError.empty() == false) { ZEN_WARN("Unable to parse Oidcs json response from {}. Reason: '{}'", AuthTokenPath, JsonError); return HttpClientAccessToken{}; } std::string Token = Json["Token"].string_value(); std::string ExpiresAtUTCString = Json["ExpiresAtUtc"].string_value(); ZEN_ASSERT(!ExpiresAtUTCString.empty()); int Year = 0; int Month = 0; int Day = 0; int Hour = 0; int Minute = 0; int Second = 0; int Millisecond = 0; sscanf(ExpiresAtUTCString.c_str(), "%d-%d-%dT%d:%d:%d.%dZ", &Year, &Month, &Day, &Hour, &Minute, &Second, &Millisecond); std::tm Time = { Second, Minute, Hour, Day, Month - 1, Year - 1900, }; time_t UTCTime = timegm(&Time); HttpClientAccessToken::TimePoint ExpireTime = std::chrono::system_clock::from_time_t(UTCTime); ExpireTime += std::chrono::microseconds(Millisecond); return HttpClientAccessToken{.Value = fmt::format("Bearer {}"sv, Token), .ExpireTime = ExpireTime}; } else { ZEN_WARN("Failed running {} to get auth token, error code {}", OidcExecutablePath, ExitCode); } return HttpClientAccessToken{}; } std::optional> CreateFromOidcTokenExecutable(const std::filesystem::path& OidcExecutablePath, std::string_view CloudHost, bool Quiet, bool Unattended, bool Hidden) { HttpClientAccessToken InitialToken = GetOidcTokenFromExe(OidcExecutablePath, CloudHost, Unattended, Quiet, Hidden); if (InitialToken.IsValid()) { return [OidcExecutablePath = std::filesystem::path(OidcExecutablePath), CloudHost = std::string(CloudHost), Quiet, Hidden, InitialToken]() mutable { if (InitialToken.IsValid()) { HttpClientAccessToken Result = InitialToken; InitialToken = {}; return Result; } return GetOidcTokenFromExe(OidcExecutablePath, CloudHost, /* Unattended */ true, Quiet, Hidden); }; } return {}; } }} // namespace zen::httpclientauth