aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2023-05-02 12:31:53 +0200
committerGitHub <[email protected]>2023-05-02 12:31:53 +0200
commite3086573d2244def22ecbe1e6b4b3da8b47e0f14 (patch)
tree627066debdddf7474783893f6b9b6631bb9a4833 /src/zenhttp
parentmoved source directories into `/src` (#264) (diff)
downloadzen-e3086573d2244def22ecbe1e6b4b3da8b47e0f14.tar.xz
zen-e3086573d2244def22ecbe1e6b4b3da8b47e0f14.zip
move auth code from zenserver into zenhttp (#265)
this code should be usable outside of zenserver, so this moves it out into zenhttp where it can be used from lower level components
Diffstat (limited to 'src/zenhttp')
-rw-r--r--src/zenhttp/auth/authmgr.cpp506
-rw-r--r--src/zenhttp/auth/authservice.cpp90
-rw-r--r--src/zenhttp/auth/oidc.cpp127
-rw-r--r--src/zenhttp/include/zenhttp/auth/authmgr.h56
-rw-r--r--src/zenhttp/include/zenhttp/auth/authservice.h25
-rw-r--r--src/zenhttp/include/zenhttp/auth/oidc.h76
6 files changed, 880 insertions, 0 deletions
diff --git a/src/zenhttp/auth/authmgr.cpp b/src/zenhttp/auth/authmgr.cpp
new file mode 100644
index 000000000..d535d07a4
--- /dev/null
+++ b/src/zenhttp/auth/authmgr.cpp
@@ -0,0 +1,506 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zenhttp/auth/authmgr.h"
+
+#include <zencore/compactbinary.h>
+#include <zencore/compactbinarybuilder.h>
+#include <zencore/compactbinaryvalidation.h>
+#include <zencore/crypto.h>
+#include <zencore/filesystem.h>
+#include <zencore/logging.h>
+#include <zenhttp/auth/oidc.h>
+
+#include <condition_variable>
+#include <memory>
+#include <shared_mutex>
+#include <thread>
+#include <unordered_map>
+
+#include <fmt/format.h>
+
+namespace zen {
+
+using namespace std::literals;
+
+namespace details {
+ IoBuffer ReadEncryptedFile(std::filesystem::path Path,
+ const AesKey256Bit& Key,
+ const AesIV128Bit& IV,
+ std::optional<std::string>& Reason)
+ {
+ FileContents Result = ReadFile(Path);
+
+ if (Result.ErrorCode)
+ {
+ return IoBuffer();
+ }
+
+ IoBuffer EncryptedBuffer = Result.Flatten();
+
+ if (EncryptedBuffer.GetSize() == 0)
+ {
+ return IoBuffer();
+ }
+
+ std::vector<uint8_t> DecryptionBuffer;
+ DecryptionBuffer.resize(EncryptedBuffer.GetSize() + Aes::BlockSize);
+
+ MemoryView DecryptedView = Aes::Decrypt(Key, IV, EncryptedBuffer, MakeMutableMemoryView(DecryptionBuffer), Reason);
+
+ if (DecryptedView.IsEmpty())
+ {
+ return IoBuffer();
+ }
+
+ return IoBufferBuilder::MakeCloneFromMemory(DecryptedView);
+ }
+
+ void WriteEncryptedFile(std::filesystem::path Path,
+ IoBuffer FileData,
+ const AesKey256Bit& Key,
+ const AesIV128Bit& IV,
+ std::optional<std::string>& Reason)
+ {
+ if (FileData.GetSize() == 0)
+ {
+ return;
+ }
+
+ std::vector<uint8_t> EncryptionBuffer;
+ EncryptionBuffer.resize(FileData.GetSize() + Aes::BlockSize);
+
+ MemoryView EncryptedView = Aes::Encrypt(Key, IV, FileData, MakeMutableMemoryView(EncryptionBuffer), Reason);
+
+ if (EncryptedView.IsEmpty())
+ {
+ return;
+ }
+
+ WriteFile(Path, IoBuffer(IoBuffer::Wrap, EncryptedView.GetData(), EncryptedView.GetSize()));
+ }
+} // namespace details
+
+class AuthMgrImpl final : public AuthMgr
+{
+ using Clock = std::chrono::system_clock;
+ using TimePoint = Clock::time_point;
+ using Seconds = std::chrono::seconds;
+
+public:
+ AuthMgrImpl(const AuthConfig& Config) : m_Config(Config), m_Log(logging::Get("auth"))
+ {
+ LoadState();
+
+ m_BackgroundThread.Interval = Config.UpdateInterval;
+ m_BackgroundThread.Thread = std::thread(&AuthMgrImpl::BackgroundThreadEntry, this);
+ }
+
+ virtual ~AuthMgrImpl() { Shutdown(); }
+
+ virtual void AddOpenIdProvider(const AddOpenIdProviderParams& Params) final
+ {
+ if (OpenIdProviderExist(Params.Name))
+ {
+ ZEN_DEBUG("OpenID provider '{}' already exist", Params.Name);
+ return;
+ }
+
+ if (Params.Name.empty())
+ {
+ ZEN_WARN("add OpenID provider FAILED, reason 'invalid name'");
+ return;
+ }
+
+ std::unique_ptr<OidcClient> Client =
+ std::make_unique<OidcClient>(OidcClient::Options{.BaseUrl = Params.Url, .ClientId = Params.ClientId});
+
+ if (const auto InitResult = Client->Initialize(); InitResult.Ok == false)
+ {
+ ZEN_WARN("query OpenID provider FAILED, reason '{}'", InitResult.Reason);
+ return;
+ }
+
+ std::string NewProviderName = std::string(Params.Name);
+
+ OpenIdProvider* NewProvider = nullptr;
+
+ {
+ std::unique_lock _(m_ProviderMutex);
+
+ if (m_OpenIdProviders.contains(NewProviderName))
+ {
+ return;
+ }
+
+ auto InsertResult = m_OpenIdProviders.emplace(NewProviderName, std::make_unique<OpenIdProvider>());
+ NewProvider = InsertResult.first->second.get();
+ }
+
+ NewProvider->Name = std::string(Params.Name);
+ NewProvider->Url = std::string(Params.Url);
+ NewProvider->ClientId = std::string(Params.ClientId);
+ NewProvider->HttpClient = std::move(Client);
+
+ ZEN_INFO("added OpenID provider '{} - {}'", Params.Name, Params.Url);
+ }
+
+ virtual bool AddOpenIdToken(const AddOpenIdTokenParams& Params) final
+ {
+ if (Params.ProviderName.empty())
+ {
+ ZEN_WARN("trying add OpenID token with invalid provider name");
+ return false;
+ }
+
+ if (Params.RefreshToken.empty())
+ {
+ ZEN_WARN("add OpenID token FAILED, reason 'Token invalid'");
+ return false;
+ }
+
+ auto RefreshResult = RefreshOpenIdToken(Params.ProviderName, Params.RefreshToken);
+
+ if (RefreshResult.Ok == false)
+ {
+ ZEN_WARN("refresh OpenId token FAILED, reason '{}'", RefreshResult.Reason);
+ return false;
+ }
+
+ bool IsNew = false;
+
+ {
+ auto Token = OpenIdToken{.IdentityToken = RefreshResult.IdentityToken,
+ .RefreshToken = RefreshResult.RefreshToken,
+ .AccessToken = fmt::format("Bearer {}"sv, RefreshResult.AccessToken),
+ .ExpireTime = Clock::now() + Seconds(RefreshResult.ExpiresInSeconds)};
+
+ std::unique_lock _(m_TokenMutex);
+
+ const auto InsertResult = m_OpenIdTokens.insert_or_assign(std::string(Params.ProviderName), std::move(Token));
+
+ IsNew = InsertResult.second;
+ }
+
+ if (IsNew)
+ {
+ ZEN_INFO("added new OpenID token for provider '{}'", Params.ProviderName);
+ }
+ else
+ {
+ ZEN_INFO("updating OpenID token for provider '{}'", Params.ProviderName);
+ }
+
+ return true;
+ }
+
+ virtual OpenIdAccessToken GetOpenIdAccessToken(std::string_view ProviderName) final
+ {
+ std::unique_lock _(m_TokenMutex);
+
+ if (auto It = m_OpenIdTokens.find(std::string(ProviderName)); It != m_OpenIdTokens.end())
+ {
+ const OpenIdToken& Token = It->second;
+
+ return {.AccessToken = Token.AccessToken, .ExpireTime = Token.ExpireTime};
+ }
+
+ return {};
+ }
+
+private:
+ bool OpenIdProviderExist(std::string_view ProviderName)
+ {
+ std::unique_lock _(m_ProviderMutex);
+
+ return m_OpenIdProviders.contains(std::string(ProviderName));
+ }
+
+ OidcClient& GetOpenIdClient(std::string_view ProviderName)
+ {
+ std::unique_lock _(m_ProviderMutex);
+ return *m_OpenIdProviders[std::string(ProviderName)]->HttpClient.get();
+ }
+
+ OidcClient::RefreshTokenResult RefreshOpenIdToken(std::string_view ProviderName, std::string_view RefreshToken)
+ {
+ if (OpenIdProviderExist(ProviderName) == false)
+ {
+ return {.Reason = fmt::format("provider '{}' is missing", ProviderName)};
+ }
+
+ OidcClient& Client = GetOpenIdClient(ProviderName);
+
+ return Client.RefreshToken(RefreshToken);
+ }
+
+ void Shutdown()
+ {
+ BackgroundThread::Stop(m_BackgroundThread);
+ SaveState();
+ }
+
+ void LoadState()
+ {
+ try
+ {
+ std::optional<std::string> Reason;
+
+ IoBuffer Buffer =
+ details::ReadEncryptedFile(m_Config.RootDirectory / "authstate"sv, m_Config.EncryptionKey, m_Config.EncryptionIV, Reason);
+
+ if (!Buffer)
+ {
+ if (Reason)
+ {
+ ZEN_WARN("load auth state FAILED, reason '{}'", Reason.value());
+ }
+
+ return;
+ }
+
+ const CbValidateError ValidationError = ValidateCompactBinary(Buffer, CbValidateMode::All);
+
+ if (ValidationError != CbValidateError::None)
+ {
+ ZEN_WARN("load serialized state FAILED, reason 'Invalid compact binary'");
+ return;
+ }
+
+ if (CbObject AuthState = LoadCompactBinaryObject(Buffer))
+ {
+ for (CbFieldView ProviderView : AuthState["OpenIdProviders"sv])
+ {
+ CbObjectView ProviderObj = ProviderView.AsObjectView();
+
+ std::string_view ProviderName = ProviderObj["Name"].AsString();
+ std::string_view Url = ProviderObj["Url"].AsString();
+ std::string_view ClientId = ProviderObj["ClientId"].AsString();
+
+ AddOpenIdProvider({.Name = ProviderName, .Url = Url, .ClientId = ClientId});
+ }
+
+ for (CbFieldView TokenView : AuthState["OpenIdTokens"sv])
+ {
+ CbObjectView TokenObj = TokenView.AsObjectView();
+
+ std::string_view ProviderName = TokenObj["ProviderName"sv].AsString();
+ std::string_view RefreshToken = TokenObj["RefreshToken"sv].AsString();
+
+ const bool Ok = AddOpenIdToken({.ProviderName = ProviderName, .RefreshToken = RefreshToken});
+
+ if (!Ok)
+ {
+ ZEN_WARN("load serialized OpenId token for provider '{}' FAILED", ProviderName);
+ }
+ }
+ }
+ }
+ catch (std::exception& Err)
+ {
+ ZEN_ERROR("(de)serialize state FAILED, reason '{}'", Err.what());
+
+ {
+ std::unique_lock _(m_ProviderMutex);
+ m_OpenIdProviders.clear();
+ }
+
+ {
+ std::unique_lock _(m_TokenMutex);
+ m_OpenIdTokens.clear();
+ }
+ }
+ }
+
+ void SaveState()
+ {
+ try
+ {
+ CbObjectWriter AuthState;
+
+ {
+ std::unique_lock _(m_ProviderMutex);
+
+ if (m_OpenIdProviders.size() > 0)
+ {
+ AuthState.BeginArray("OpenIdProviders");
+ for (const auto& Kv : m_OpenIdProviders)
+ {
+ AuthState.BeginObject();
+ AuthState << "Name"sv << Kv.second->Name;
+ AuthState << "Url"sv << Kv.second->Url;
+ AuthState << "ClientId"sv << Kv.second->ClientId;
+ AuthState.EndObject();
+ }
+ AuthState.EndArray();
+ }
+ }
+
+ {
+ std::unique_lock _(m_TokenMutex);
+
+ AuthState.BeginArray("OpenIdTokens");
+ if (m_OpenIdTokens.size() > 0)
+ {
+ for (const auto& Kv : m_OpenIdTokens)
+ {
+ AuthState.BeginObject();
+ AuthState << "ProviderName"sv << Kv.first;
+ AuthState << "RefreshToken"sv << Kv.second.RefreshToken;
+ AuthState.EndObject();
+ }
+ }
+ AuthState.EndArray();
+ }
+
+ std::filesystem::create_directories(m_Config.RootDirectory);
+
+ std::optional<std::string> Reason;
+
+ details::WriteEncryptedFile(m_Config.RootDirectory / "authstate"sv,
+ AuthState.Save().GetBuffer().AsIoBuffer(),
+ m_Config.EncryptionKey,
+ m_Config.EncryptionIV,
+ Reason);
+
+ if (Reason)
+ {
+ ZEN_WARN("save auth state FAILED, reason '{}'", Reason.value());
+ }
+ }
+ catch (std::exception& Err)
+ {
+ ZEN_ERROR("serialize state FAILED, reason '{}'", Err.what());
+ }
+ }
+
+ void BackgroundThreadEntry()
+ {
+ for (;;)
+ {
+ std::cv_status SignalStatus = BackgroundThread::WaitForSignal(m_BackgroundThread);
+
+ if (m_BackgroundThread.Running.load() == false)
+ {
+ break;
+ }
+
+ if (SignalStatus != std::cv_status::timeout)
+ {
+ continue;
+ }
+
+ {
+ // Refresh Open ID token(s)
+
+ std::vector<OpenIdTokenMap::value_type> ExpiredTokens;
+
+ {
+ std::unique_lock _(m_TokenMutex);
+
+ for (const auto& Kv : m_OpenIdTokens)
+ {
+ const Seconds ExpiresIn = std::chrono::duration_cast<Seconds>(Kv.second.ExpireTime - Clock::now());
+ const bool Expired = ExpiresIn < Seconds(m_BackgroundThread.Interval * 2);
+
+ if (Expired)
+ {
+ ExpiredTokens.push_back(Kv);
+ }
+ }
+ }
+
+ ZEN_DEBUG("refreshing '{}' OpenID token(s)", ExpiredTokens.size());
+
+ for (const auto& Kv : ExpiredTokens)
+ {
+ OidcClient::RefreshTokenResult RefreshResult = RefreshOpenIdToken(Kv.first, Kv.second.RefreshToken);
+
+ if (RefreshResult.Ok)
+ {
+ ZEN_DEBUG("refresh access token from provider '{}' Ok", Kv.first);
+
+ auto Token = OpenIdToken{.IdentityToken = RefreshResult.IdentityToken,
+ .RefreshToken = RefreshResult.RefreshToken,
+ .AccessToken = fmt::format("Bearer {}"sv, RefreshResult.AccessToken),
+ .ExpireTime = Clock::now() + Seconds(RefreshResult.ExpiresInSeconds)};
+
+ {
+ std::unique_lock _(m_TokenMutex);
+ m_OpenIdTokens.insert_or_assign(Kv.first, std::move(Token));
+ }
+ }
+ else
+ {
+ ZEN_WARN("refresh access token from provider '{}' FAILED, reason '{}'", Kv.first, RefreshResult.Reason);
+ }
+ }
+ }
+ }
+ }
+
+ struct BackgroundThread
+ {
+ std::chrono::seconds Interval{10};
+ std::mutex Mutex;
+ std::condition_variable Signal;
+ std::atomic_bool Running{true};
+ std::thread Thread;
+
+ static void Stop(BackgroundThread& State)
+ {
+ if (State.Running.load())
+ {
+ State.Running.store(false);
+ State.Signal.notify_one();
+ }
+
+ if (State.Thread.joinable())
+ {
+ State.Thread.join();
+ }
+ }
+
+ static std::cv_status WaitForSignal(BackgroundThread& State)
+ {
+ std::unique_lock Lock(State.Mutex);
+ return State.Signal.wait_for(Lock, State.Interval);
+ }
+ };
+
+ struct OpenIdProvider
+ {
+ std::string Name;
+ std::string Url;
+ std::string ClientId;
+ std::unique_ptr<OidcClient> HttpClient;
+ };
+
+ struct OpenIdToken
+ {
+ std::string IdentityToken;
+ std::string RefreshToken;
+ std::string AccessToken;
+ TimePoint ExpireTime{};
+ };
+
+ using OpenIdProviderMap = std::unordered_map<std::string, std::unique_ptr<OpenIdProvider>>;
+ using OpenIdTokenMap = std::unordered_map<std::string, OpenIdToken>;
+
+ spdlog::logger& Log() { return m_Log; }
+
+ AuthConfig m_Config;
+ spdlog::logger& m_Log;
+ BackgroundThread m_BackgroundThread;
+ OpenIdProviderMap m_OpenIdProviders;
+ OpenIdTokenMap m_OpenIdTokens;
+ std::mutex m_ProviderMutex;
+ std::shared_mutex m_TokenMutex;
+};
+
+std::unique_ptr<AuthMgr>
+AuthMgr::Create(const AuthConfig& Config)
+{
+ return std::make_unique<AuthMgrImpl>(Config);
+}
+
+} // namespace zen
diff --git a/src/zenhttp/auth/authservice.cpp b/src/zenhttp/auth/authservice.cpp
new file mode 100644
index 000000000..6ed587770
--- /dev/null
+++ b/src/zenhttp/auth/authservice.cpp
@@ -0,0 +1,90 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zenhttp/auth/authservice.h"
+
+#include <zencore/compactbinarybuilder.h>
+#include <zencore/string.h>
+#include <zenhttp/auth/authmgr.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <json11.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+using namespace std::literals;
+
+HttpAuthService::HttpAuthService(AuthMgr& AuthMgr) : m_AuthMgr(AuthMgr)
+{
+ m_Router.RegisterRoute(
+ "oidc/refreshtoken",
+ [this](HttpRouterRequest& RouterRequest) {
+ HttpServerRequest& ServerRequest = RouterRequest.ServerRequest();
+
+ const HttpContentType ContentType = ServerRequest.RequestContentType();
+
+ if ((ContentType == HttpContentType::kUnknownContentType || ContentType == HttpContentType::kJSON) == false)
+ {
+ return ServerRequest.WriteResponse(HttpResponseCode::BadRequest);
+ }
+
+ const IoBuffer Body = ServerRequest.ReadPayload();
+
+ std::string JsonText(reinterpret_cast<const char*>(Body.GetData()), Body.GetSize());
+ std::string JsonError;
+ json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError);
+
+ if (!JsonError.empty())
+ {
+ CbObjectWriter Response;
+ Response << "Result"sv << false;
+ Response << "Error"sv << JsonError;
+
+ return ServerRequest.WriteResponse(HttpResponseCode::BadRequest, Response.Save());
+ }
+
+ const std::string RefreshToken = TokenInfo["RefreshToken"].string_value();
+ std::string ProviderName = TokenInfo["ProviderName"].string_value();
+
+ if (ProviderName.empty())
+ {
+ ProviderName = "Default"sv;
+ }
+
+ const bool Ok =
+ m_AuthMgr.AddOpenIdToken(AuthMgr::AddOpenIdTokenParams{.ProviderName = ProviderName, .RefreshToken = RefreshToken});
+
+ if (Ok)
+ {
+ ServerRequest.WriteResponse(Ok ? HttpResponseCode::OK : HttpResponseCode::BadRequest);
+ }
+ else
+ {
+ CbObjectWriter Response;
+ Response << "Result"sv << false;
+ Response << "Error"sv
+ << "Invalid token"sv;
+
+ ServerRequest.WriteResponse(HttpResponseCode::BadRequest, Response.Save());
+ }
+ },
+ HttpVerb::kPost);
+}
+
+HttpAuthService::~HttpAuthService()
+{
+}
+
+const char*
+HttpAuthService::BaseUri() const
+{
+ return "/auth/";
+}
+
+void
+HttpAuthService::HandleRequest(zen::HttpServerRequest& Request)
+{
+ m_Router.HandleRequest(Request);
+}
+
+} // namespace zen
diff --git a/src/zenhttp/auth/oidc.cpp b/src/zenhttp/auth/oidc.cpp
new file mode 100644
index 000000000..318110c7d
--- /dev/null
+++ b/src/zenhttp/auth/oidc.cpp
@@ -0,0 +1,127 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zenhttp/auth/oidc.h"
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <cpr/cpr.h>
+#include <fmt/format.h>
+#include <json11.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+namespace details {
+
+ using StringArray = std::vector<std::string>;
+
+ StringArray ToStringArray(const json11::Json JsonArray)
+ {
+ StringArray Result;
+
+ const auto& Items = JsonArray.array_items();
+
+ for (const auto& Item : Items)
+ {
+ Result.push_back(Item.string_value());
+ }
+
+ return Result;
+ }
+
+} // namespace details
+
+using namespace std::literals;
+
+OidcClient::OidcClient(const OidcClient::Options& Options)
+{
+ m_BaseUrl = std::string(Options.BaseUrl);
+ m_ClientId = std::string(Options.ClientId);
+}
+
+OidcClient::InitResult
+OidcClient::Initialize()
+{
+ ExtendableStringBuilder<256> Uri;
+ Uri << m_BaseUrl << "/.well-known/openid-configuration"sv;
+
+ cpr::Session Session;
+
+ Session.SetOption(cpr::Url{Uri.c_str()});
+
+ cpr::Response Response = Session.Get();
+
+ if (Response.error)
+ {
+ return {.Reason = std::move(Response.error.message)};
+ }
+
+ if (Response.status_code != 200)
+ {
+ return {.Reason = std::move(Response.reason)};
+ }
+
+ std::string JsonError;
+ json11::Json Json = json11::Json::parse(Response.text, JsonError);
+
+ if (JsonError.empty() == false)
+ {
+ return {.Reason = std::move(JsonError)};
+ }
+
+ m_Config = {.Issuer = Json["issuer"].string_value(),
+ .AuthorizationEndpoint = Json["authorization_endpoint"].string_value(),
+ .TokenEndpoint = Json["token_endpoint"].string_value(),
+ .UserInfoEndpoint = Json["userinfo_endpoint"].string_value(),
+ .RegistrationEndpoint = Json["registration_endpoint"].string_value(),
+ .JwksUri = Json["jwks_uri"].string_value(),
+ .SupportedResponseTypes = details::ToStringArray(Json["response_types_supported"]),
+ .SupportedResponseModes = details::ToStringArray(Json["response_modes_supported"]),
+ .SupportedGrantTypes = details::ToStringArray(Json["grant_types_supported"]),
+ .SupportedScopes = details::ToStringArray(Json["scopes_supported"]),
+ .SupportedTokenEndpointAuthMethods = details::ToStringArray(Json["token_endpoint_auth_methods_supported"]),
+ .SupportedClaims = details::ToStringArray(Json["claims_supported"])};
+
+ return {.Ok = true};
+}
+
+OidcClient::RefreshTokenResult
+OidcClient::RefreshToken(std::string_view RefreshToken)
+{
+ const std::string Body = fmt::format("grant_type=refresh_token&refresh_token={}&client_id={}", RefreshToken, m_ClientId);
+
+ cpr::Session Session;
+
+ Session.SetOption(cpr::Url{m_Config.TokenEndpoint.c_str()});
+ Session.SetOption(cpr::Header{{"Content-Type", "application/x-www-form-urlencoded"}});
+ Session.SetBody(cpr::Body{Body.data(), Body.size()});
+
+ cpr::Response Response = Session.Post();
+
+ if (Response.error)
+ {
+ return {.Reason = std::move(Response.error.message)};
+ }
+
+ if (Response.status_code != 200)
+ {
+ return {.Reason = fmt::format("{} ({})", Response.reason, Response.text)};
+ }
+
+ std::string JsonError;
+ json11::Json Json = json11::Json::parse(Response.text, JsonError);
+
+ if (JsonError.empty() == false)
+ {
+ return {.Reason = std::move(JsonError)};
+ }
+
+ return {.TokenType = Json["token_type"].string_value(),
+ .AccessToken = Json["access_token"].string_value(),
+ .RefreshToken = Json["refresh_token"].string_value(),
+ .IdentityToken = Json["id_token"].string_value(),
+ .Scope = Json["scope"].string_value(),
+ .ExpiresInSeconds = static_cast<int64_t>(Json["expires_in"].int_value()),
+ .Ok = true};
+}
+
+} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/auth/authmgr.h b/src/zenhttp/include/zenhttp/auth/authmgr.h
new file mode 100644
index 000000000..054588ab9
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/auth/authmgr.h
@@ -0,0 +1,56 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/crypto.h>
+#include <zencore/iobuffer.h>
+#include <zencore/string.h>
+
+#include <chrono>
+#include <filesystem>
+#include <memory>
+
+namespace zen {
+
+struct AuthConfig
+{
+ std::filesystem::path RootDirectory;
+ std::chrono::seconds UpdateInterval{30};
+ AesKey256Bit EncryptionKey;
+ AesIV128Bit EncryptionIV;
+};
+
+class AuthMgr
+{
+public:
+ virtual ~AuthMgr() = default;
+
+ struct AddOpenIdProviderParams
+ {
+ std::string_view Name;
+ std::string_view Url;
+ std::string_view ClientId;
+ };
+
+ virtual void AddOpenIdProvider(const AddOpenIdProviderParams& Params) = 0;
+
+ struct AddOpenIdTokenParams
+ {
+ std::string_view ProviderName;
+ std::string_view RefreshToken;
+ };
+
+ virtual bool AddOpenIdToken(const AddOpenIdTokenParams& Params) = 0;
+
+ struct OpenIdAccessToken
+ {
+ std::string AccessToken;
+ std::chrono::system_clock::time_point ExpireTime{};
+ };
+
+ virtual OpenIdAccessToken GetOpenIdAccessToken(std::string_view ProviderName) = 0;
+
+ static std::unique_ptr<AuthMgr> Create(const AuthConfig& Config);
+};
+
+} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/auth/authservice.h b/src/zenhttp/include/zenhttp/auth/authservice.h
new file mode 100644
index 000000000..64b86e21f
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/auth/authservice.h
@@ -0,0 +1,25 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zenhttp/httpserver.h>
+
+namespace zen {
+
+class AuthMgr;
+
+class HttpAuthService final : public zen::HttpService
+{
+public:
+ HttpAuthService(AuthMgr& AuthMgr);
+ virtual ~HttpAuthService();
+
+ virtual const char* BaseUri() const override;
+ virtual void HandleRequest(zen::HttpServerRequest& Request) override;
+
+private:
+ AuthMgr& m_AuthMgr;
+ HttpRequestRouter m_Router;
+};
+
+} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/auth/oidc.h b/src/zenhttp/include/zenhttp/auth/oidc.h
new file mode 100644
index 000000000..f43ae3cd7
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/auth/oidc.h
@@ -0,0 +1,76 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zencore/string.h>
+
+#include <vector>
+
+namespace zen {
+
+class OidcClient
+{
+public:
+ struct Options
+ {
+ std::string_view BaseUrl;
+ std::string_view ClientId;
+ };
+
+ OidcClient(const Options& Options);
+ ~OidcClient() = default;
+
+ OidcClient(const OidcClient&) = delete;
+ OidcClient& operator=(const OidcClient&) = delete;
+
+ struct Result
+ {
+ std::string Reason;
+ bool Ok = false;
+ };
+
+ using InitResult = Result;
+
+ InitResult Initialize();
+
+ struct RefreshTokenResult
+ {
+ std::string TokenType;
+ std::string AccessToken;
+ std::string RefreshToken;
+ std::string IdentityToken;
+ std::string Scope;
+ std::string Reason;
+ int64_t ExpiresInSeconds{};
+ bool Ok = false;
+ };
+
+ RefreshTokenResult RefreshToken(std::string_view RefreshToken);
+
+private:
+ using StringArray = std::vector<std::string>;
+
+ struct OpenIdConfiguration
+ {
+ std::string Issuer;
+ std::string AuthorizationEndpoint;
+ std::string TokenEndpoint;
+ std::string UserInfoEndpoint;
+ std::string RegistrationEndpoint;
+ std::string EndSessionEndpoint;
+ std::string DeviceAuthorizationEndpoint;
+ std::string JwksUri;
+ StringArray SupportedResponseTypes;
+ StringArray SupportedResponseModes;
+ StringArray SupportedGrantTypes;
+ StringArray SupportedScopes;
+ StringArray SupportedTokenEndpointAuthMethods;
+ StringArray SupportedClaims;
+ };
+
+ std::string m_BaseUrl;
+ std::string m_ClientId;
+ OpenIdConfiguration m_Config;
+};
+
+} // namespace zen