#include "nocal/sync/oauth_tokens.hpp" #include #include #include #include #include #include #include #include #include #include namespace nocal::sync { namespace { constexpr std::size_t maximum_response_size = 1024U * 1024U; class DuplicateKey final : public std::exception {}; [[noreturn]] void invalid_response() { throw std::invalid_argument("invalid OAuth token response"); } [[nodiscard]] bool contains_ascii_control(std::string_view value) { return std::ranges::any_of(value, [](unsigned char character) { return character < 0x20U || character == 0x7fU; }); } [[nodiscard]] bool ascii_iequals(std::string_view left, std::string_view right) { if (left.size() != right.size()) { return false; } for (std::size_t index = 0; index < left.size(); ++index) { const auto lower = [](unsigned char value) { return value >= 'A' && value <= 'Z' ? static_cast(value + ('a' - 'A')) : value; }; if (lower(static_cast(left[index])) != lower(static_cast(right[index]))) { return false; } } return true; } [[nodiscard]] nlohmann::json parse_unique_object(std::string_view response_body) { if (response_body.empty() || response_body.size() > maximum_response_size) { invalid_response(); } std::vector> object_keys; const nlohmann::json::parser_callback_t callback = [&object_keys](int, nlohmann::json::parse_event_t event, nlohmann::json& parsed) { if (event == nlohmann::json::parse_event_t::object_start) { object_keys.emplace_back(); } else if (event == nlohmann::json::parse_event_t::key) { if (object_keys.empty() || !object_keys.back().insert(parsed.get()).second) { throw DuplicateKey{}; } } else if (event == nlohmann::json::parse_event_t::object_end) { if (object_keys.empty()) { throw DuplicateKey{}; } object_keys.pop_back(); } return true; }; try { nlohmann::json parsed = nlohmann::json::parse( response_body.begin(), response_body.end(), callback, true, false); if (!object_keys.empty() || !parsed.is_object()) { invalid_response(); } return parsed; } catch (const DuplicateKey&) { invalid_response(); } catch (const nlohmann::json::exception&) { invalid_response(); } } [[nodiscard]] std::string required_nonempty_string( const nlohmann::json& object, std::string_view name) { const auto found = object.find(std::string(name)); if (found == object.end() || !found->is_string()) { invalid_response(); } const std::string value = found->get(); if (value.empty() || contains_ascii_control(value)) { invalid_response(); } return value; } [[nodiscard]] std::string optional_nonempty_string( const nlohmann::json& object, std::string_view name) { const auto found = object.find(std::string(name)); if (found == object.end()) { return {}; } if (!found->is_string()) { invalid_response(); } const std::string value = found->get(); if (value.empty() || contains_ascii_control(value)) { invalid_response(); } return value; } [[nodiscard]] std::uint64_t positive_integral_seconds(const nlohmann::json& object) { const auto found = object.find("expires_in"); if (found == object.end()) { invalid_response(); } if (found->is_number_unsigned()) { const std::uint64_t value = found->get(); if (value == 0) { invalid_response(); } return value; } if (found->is_number_integer()) { const std::int64_t value = found->get(); if (value <= 0) { invalid_response(); } return static_cast(value); } invalid_response(); } [[nodiscard]] std::int64_t add_expiration( std::int64_t now_epoch_seconds, std::uint64_t expires_in) { constexpr std::int64_t maximum = std::numeric_limits::max(); if (now_epoch_seconds >= 0) { const auto room = static_cast(maximum - now_epoch_seconds); if (expires_in > room) { invalid_response(); } return now_epoch_seconds + static_cast(expires_in); } const std::uint64_t negative_magnitude = static_cast(-(now_epoch_seconds + 1)) + 1U; const std::uint64_t room = static_cast(maximum) + negative_magnitude; if (expires_in > room) { invalid_response(); } if (expires_in >= negative_magnitude) { return static_cast(expires_in - negative_magnitude); } const std::uint64_t remaining_magnitude = negative_magnitude - expires_in; if (remaining_magnitude == (std::uint64_t{1} << 63U)) { return std::numeric_limits::min(); } return -static_cast(remaining_magnitude); } } // namespace OAuthTokens parse_oauth_token_response(std::string_view response_body, std::int64_t now_epoch_seconds, std::string_view retained_refresh_token) { const nlohmann::json object = parse_unique_object(response_body); OAuthTokens result; result.access_token = required_nonempty_string(object, "access_token"); const std::string token_type = required_nonempty_string(object, "token_type"); if (!ascii_iequals(token_type, "Bearer")) { invalid_response(); } result.token_type = "Bearer"; result.scope = optional_nonempty_string(object, "scope"); result.refresh_token = optional_nonempty_string(object, "refresh_token"); if (result.refresh_token.empty()) { if (retained_refresh_token.empty() || contains_ascii_control(retained_refresh_token)) { invalid_response(); } result.refresh_token = std::string(retained_refresh_token); } result.expires_at_epoch_seconds = add_expiration(now_epoch_seconds, positive_integral_seconds(object)); return result; } } // namespace nocal::sync