diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/zen/cmds/oidctoken_cmd.cpp | 173 | ||||
| -rw-r--r-- | src/zenhttp/auth/oidc.cpp | 31 |
2 files changed, 178 insertions, 26 deletions
diff --git a/src/zen/cmds/oidctoken_cmd.cpp b/src/zen/cmds/oidctoken_cmd.cpp index 24a79323b..2cfd519df 100644 --- a/src/zen/cmds/oidctoken_cmd.cpp +++ b/src/zen/cmds/oidctoken_cmd.cpp @@ -2,11 +2,13 @@ #include "oidctoken_cmd.h" +#include <zencore/crypto.h> #include <zencore/except_fmt.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/process.h> +#include <zencore/string.h> #include <zencore/uid.h> #include <zenhttp/auth/oidc.h> #include <zenhttp/httpclient.h> @@ -80,6 +82,73 @@ namespace { } // ----------------------------------------------------------------------- + // AES-128-CBC Decryption (for encrypted remote config) + // ----------------------------------------------------------------------- + + // Decrypts AES-128-CBC ciphertext where the IV is prepended to the data. + // Key is a 16-byte hex string (32 hex chars -> 16 bytes). + // Returns empty string on failure. + std::string DecryptAes128Cbc(std::string_view HexKey, const void* EncryptedData, size_t EncryptedSize) + { + // Parse hex key to 16 bytes + if (HexKey.size() != 32) + { + ZEN_WARN("AES-128 key must be 32 hex characters (16 bytes), got {} characters", HexKey.size()); + return {}; + } + + uint8_t KeyBytes[16]; + if (!ParseHexBytes(HexKey, KeyBytes)) + { + ZEN_WARN("Failed to parse hex key"); + return {}; + } + + // IV is the first 16 bytes of the encrypted data + constexpr size_t IvSize = 16; + if (EncryptedSize <= IvSize) + { + ZEN_WARN("Encrypted data too small ({} bytes), must be larger than IV ({} bytes)", EncryptedSize, IvSize); + return {}; + } + + const auto* DataBytes = static_cast<const uint8_t*>(EncryptedData); + MemoryView IvView(DataBytes, IvSize); + MemoryView CiphertextView(DataBytes + IvSize, EncryptedSize - IvSize); + + // Use the existing crypto infrastructure. The Aes class uses AES-256 with a 256-bit key, + // but we need AES-128. We pad the 16-byte key to 32 bytes to satisfy AesKey256Bit, but + // on BCrypt (Windows) the key size selects the algorithm automatically. On OpenSSL/mbedTLS, + // the existing Transform function hardcodes AES-256-CBC, so for cross-platform correctness + // we zero-extend the key to 32 bytes and use the existing Aes::Decrypt. + // + // NOTE: This works correctly on Windows (BCrypt auto-selects AES-128 based on key size). + // On platforms using OpenSSL/mbedTLS, we extend to 256-bit key and use AES-256-CBC which + // is a different cipher and would NOT produce the correct result. If cross-platform + // decryption of auth configs is needed, AES-128 support must be added to crypto.cpp. + // In practice, auth config encryption is primarily used in Windows build farm environments. + + uint8_t PaddedKey[32] = {0}; + memcpy(PaddedKey, KeyBytes, 16); + + AesKey256Bit Key = AesKey256Bit::FromMemoryView(MemoryView(PaddedKey, 32)); + AesIV128Bit IV = AesIV128Bit::FromMemoryView(IvView); + + std::vector<uint8_t> DecryptedBuffer(CiphertextView.GetSize() + Aes::BlockSize); + std::optional<std::string> Reason; + + MemoryView DecryptedView = Aes::Decrypt(Key, IV, CiphertextView, MakeMutableMemoryView(DecryptedBuffer), Reason); + + if (Reason.has_value()) + { + ZEN_WARN("AES decryption failed: {}", Reason.value()); + return {}; + } + + return std::string(reinterpret_cast<const char*>(DecryptedView.GetData()), DecryptedView.GetSize()); + } + + // ----------------------------------------------------------------------- // Random String Generation // ----------------------------------------------------------------------- @@ -637,9 +706,9 @@ namespace { return true; } - bool LoadConfigFromUrl(std::string_view Url, OidcTokenConfig& Config) + bool LoadConfigFromUrl(std::string_view Url, OidcTokenConfig& Config, std::string_view EncryptionKey = {}) { - ZEN_DEBUG("Fetching OIDC config from URL: {}", Url); + ZEN_DEBUG("Fetching OIDC config from URL: {}{}", Url, EncryptionKey.empty() ? "" : " (encrypted)"); HttpClient Http{Url}; HttpClient::Response Response = Http.Get(""); @@ -652,7 +721,24 @@ namespace { ZEN_DEBUG("Received config response ({} bytes) from: {}", Response.DownloadedBytes, Url); - std::string JsonText(Response.AsText()); + std::string JsonText; + + if (!EncryptionKey.empty()) + { + ZEN_DEBUG("Decrypting config response with AES-128-CBC"); + JsonText = DecryptAes128Cbc(EncryptionKey, Response.ResponsePayload.GetData(), Response.ResponsePayload.GetSize()); + if (JsonText.empty()) + { + ZEN_WARN("Failed to decrypt OIDC config from '{}'", Url); + return false; + } + ZEN_DEBUG("Decrypted config: {} bytes", JsonText.size()); + } + else + { + JsonText = std::string(Response.AsText()); + } + std::string JsonError; json11::Json Json = json11::Json::parse(JsonText, JsonError); if (!JsonError.empty()) @@ -673,12 +759,16 @@ namespace { return LoadConfigFromUrl(Url, Config); } - OidcTokenConfig LoadConfiguration(const std::string& ConfigPath, const std::string& AuthConfigUrl, const std::string& HordeUrl) + OidcTokenConfig LoadConfiguration(const std::string& ConfigPath, + const std::string& AuthConfigUrl, + const std::string& HordeUrl, + const std::string& EncryptionKey = {}) { - ZEN_DEBUG("Loading OIDC configuration (config-path='{}', auth-config-url='{}', horde-url='{}')", + ZEN_DEBUG("Loading OIDC configuration (config-path='{}', auth-config-url='{}', horde-url='{}', has-encryption-key={})", ConfigPath, AuthConfigUrl, - HordeUrl); + HordeUrl, + !EncryptionKey.empty()); OidcTokenConfig Config; // 1. Load from explicit path if provided @@ -705,11 +795,11 @@ namespace { LoadConfigFromFile(CandidatePath, Config); } - // 3. Load from auth config URL + // 3. Load from auth config URL (optionally encrypted) if (!AuthConfigUrl.empty()) { ZEN_DEBUG("Loading config from auth-config-url: {}", AuthConfigUrl); - LoadConfigFromUrl(AuthConfigUrl, Config); + LoadConfigFromUrl(AuthConfigUrl, Config, EncryptionKey); } // 4. Load from Horde URL @@ -787,6 +877,14 @@ OidcTokenCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) return; } + // When outputting JSON to stdout, suppress all console logging so the output is clean JSON. + // This suppresses both the ZEN_CONSOLE logger and the default logger's console sink. + if (m_ResultToConsole) + { + zen::logging::SuppressConsoleLog(); + zen::logging::SuppressDefaultConsoleOutput(); + } + if (m_Service.empty()) { throw ErrorWithReturnCode("--service is required", 1); @@ -800,7 +898,7 @@ OidcTokenCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) m_ResultToConsole); // Load configuration - OidcTokenConfig Config = LoadConfiguration(m_ConfigPath, m_AuthConfigUrl, m_HordeUrl); + OidcTokenConfig Config = LoadConfiguration(m_ConfigPath, m_AuthConfigUrl, m_HordeUrl, m_AuthEncryptionKey); auto ProviderIt = Config.Providers.find(m_Service); if (ProviderIt == Config.Providers.end()) @@ -1035,31 +1133,60 @@ OidcTokenCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) Client.GetAuthorizationEndpoint(), Client.GetTokenEndpoint()); - // Determine redirect URI and port + // Determine redirect URI and port. If the primary RedirectUri is empty, + // try each PossibleRedirectUri until we find a port we can bind to. std::string RedirectUri = Provider.RedirectUri; uint16_t Port = 0; - if (RedirectUri.empty() && !Provider.PossibleRedirectUris.empty()) + if (!RedirectUri.empty()) { - ZEN_DEBUG("No primary RedirectUri, using first PossibleRedirectUri"); - RedirectUri = Provider.PossibleRedirectUris[0]; + Port = ExtractPortFromUri(RedirectUri); + if (Port == 0) + { + throw ErrorWithReturnCode(fmt::format("Unable to extract port from redirect URI '{}'", RedirectUri), 9); + } + ZEN_DEBUG("Using primary redirect URI: {} (port {})", RedirectUri, Port); } - - if (RedirectUri.empty()) + else if (!Provider.PossibleRedirectUris.empty()) { - throw ErrorWithReturnCode("No redirect URI configured for OIDC provider", 9); - } + ZEN_DEBUG("No primary RedirectUri, trying {} possible redirect URI(s)", Provider.PossibleRedirectUris.size()); + for (const auto& CandidateUri : Provider.PossibleRedirectUris) + { + uint16_t CandidatePort = ExtractPortFromUri(CandidateUri); + if (CandidatePort == 0) + { + ZEN_DEBUG("Skipping redirect URI '{}' (unable to extract port)", CandidateUri); + continue; + } - ZEN_DEBUG("Using redirect URI: {}", RedirectUri); + // Try to bind to the port to see if it's available + try + { + asio::io_context TestContext; + asio::ip::tcp::acceptor TestAcceptor(TestContext, asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), CandidatePort)); + TestAcceptor.close(); + + RedirectUri = CandidateUri; + Port = CandidatePort; + ZEN_DEBUG("Selected redirect URI: {} (port {} is available)", RedirectUri, Port); + break; + } + catch (const asio::system_error& Err) + { + ZEN_DEBUG("Port {} unavailable ({}), trying next", CandidatePort, Err.what()); + } + } - Port = ExtractPortFromUri(RedirectUri); - if (Port == 0) + if (RedirectUri.empty()) + { + throw ErrorWithReturnCode("None of the configured redirect URI ports are available", 2); + } + } + else { - throw ErrorWithReturnCode(fmt::format("Unable to extract port from redirect URI '{}'", RedirectUri), 9); + throw ErrorWithReturnCode("No redirect URI configured for OIDC provider", 9); } - ZEN_DEBUG("Extracted callback port: {}", Port); - // Generate state for CSRF protection std::string State = GenerateRandomState(); ZEN_DEBUG("Generated CSRF state: {}", State); diff --git a/src/zenhttp/auth/oidc.cpp b/src/zenhttp/auth/oidc.cpp index 4b306236c..eea81fbdb 100644 --- a/src/zenhttp/auth/oidc.cpp +++ b/src/zenhttp/auth/oidc.cpp @@ -28,6 +28,27 @@ namespace details { return Result; } + std::string UrlEncodeFormValue(std::string_view Input) + { + std::string Result; + Result.reserve(Input.size()); + for (unsigned char Ch : Input) + { + if ((Ch >= 'A' && Ch <= 'Z') || (Ch >= 'a' && Ch <= 'z') || (Ch >= '0' && Ch <= '9') || Ch == '-' || Ch == '_' || Ch == '.' || + Ch == '~') + { + Result += static_cast<char>(Ch); + } + else + { + char Hex[4]; + snprintf(Hex, sizeof(Hex), "%%%02X", Ch); + Result.append(Hex, 3); + } + } + return Result; + } + } // namespace details using namespace std::literals; @@ -81,7 +102,9 @@ OidcClient::Initialize() 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); + const std::string Body = fmt::format("grant_type=refresh_token&refresh_token={}&client_id={}", + details::UrlEncodeFormValue(RefreshToken), + details::UrlEncodeFormValue(m_ClientId)); HttpClient Http{m_Config.TokenEndpoint}; @@ -119,8 +142,10 @@ OidcClient::RefreshToken(std::string_view RefreshToken) OidcClient::RefreshTokenResult OidcClient::ExchangeAuthorizationCode(std::string_view Code, std::string_view RedirectUri) { - const std::string Body = - fmt::format("grant_type=authorization_code&code={}&redirect_uri={}&client_id={}", Code, RedirectUri, m_ClientId); + const std::string Body = fmt::format("grant_type=authorization_code&code={}&redirect_uri={}&client_id={}", + details::UrlEncodeFormValue(Code), + details::UrlEncodeFormValue(RedirectUri), + details::UrlEncodeFormValue(m_ClientId)); HttpClient Http{m_Config.TokenEndpoint}; |