#include "nocal/sync/oauth_tokens.hpp" #include #include #include #include #include #include #include #include namespace { using nocal::sync::OAuthTokens; using nocal::sync::parse_oauth_token_response; constexpr std::size_t maximum_response_size = 1024U * 1024U; void require(bool condition, std::string_view message) { if (!condition) { throw std::runtime_error(std::string(message)); } } template std::string expect_failure(Action&& action) { try { action(); } catch (const std::exception& error) { return error.what(); } throw std::runtime_error("invalid OAuth token response was accepted"); } [[nodiscard]] std::string token_body(std::string_view extra = {}) { return std::string{"{\"access_token\":\"access\",\"refresh_token\":\"refresh\","} + "\"token_type\":\"Bearer\",\"expires_in\":3600" + std::string(extra) + "}"; } void test_success_and_unknown_fields() { const OAuthTokens tokens = parse_oauth_token_response( token_body(",\"scope\":\"openid offline_access\",\"unknown\":{\"nested\":true}"), 1'000); require(tokens == OAuthTokens{"access", "refresh", "Bearer", "openid offline_access", 4'600}, "valid token response was not parsed exactly"); const OAuthTokens lower = parse_oauth_token_response( "{\"access_token\":\"a\",\"refresh_token\":\"r\",\"token_type\":\"bEaReR\"," "\"expires_in\":1}", -2); require(lower.token_type == "Bearer" && lower.expires_at_epoch_seconds == -1, "Bearer comparison/canonicalization or negative epoch arithmetic failed"); } void test_retained_and_rotated_refresh_tokens() { const OAuthTokens retained = parse_oauth_token_response( "{\"access_token\":\"new-access\",\"token_type\":\"Bearer\",\"expires_in\":60}", 100, "old-refresh"); require(retained.refresh_token == "old-refresh" && retained.expires_at_epoch_seconds == 160, "refresh response did not retain the prior refresh token"); const OAuthTokens rotated = parse_oauth_token_response( "{\"access_token\":\"new-access\",\"refresh_token\":\"new-refresh\"," "\"token_type\":\"Bearer\",\"expires_in\":60}", 100, "old-refresh"); require(rotated.refresh_token == "new-refresh", "rotated refresh token did not replace the retained token"); expect_failure([&] { (void)parse_oauth_token_response( "{\"access_token\":\"a\",\"token_type\":\"Bearer\",\"expires_in\":1}", 0); }); expect_failure([&] { (void)parse_oauth_token_response( "{\"access_token\":\"a\",\"refresh_token\":\"\"," "\"token_type\":\"Bearer\",\"expires_in\":1}", 0, "retained"); }); } void test_expiration_boundaries_and_integral_types() { const auto maximum = std::numeric_limits::max(); const auto minimum = std::numeric_limits::min(); require(parse_oauth_token_response(token_body(",\"scope\":\"s\""), maximum - 3'600) .expires_at_epoch_seconds == maximum, "maximum signed expiration boundary was rejected"); require(parse_oauth_token_response( "{\"access_token\":\"a\",\"refresh_token\":\"r\"," "\"token_type\":\"Bearer\",\"expires_in\":18446744073709551615}", minimum) .expires_at_epoch_seconds == maximum, "full unsigned expiration boundary was not added safely"); require(parse_oauth_token_response( "{\"access_token\":\"a\",\"refresh_token\":\"r\"," "\"token_type\":\"Bearer\",\"expires_in\":1}", minimum) .expires_at_epoch_seconds == minimum + 1, "minimum signed epoch boundary was not added safely"); expect_failure([&] { (void)parse_oauth_token_response(token_body(), maximum); }); for (const std::string expires : {"0", "-1", "1.0", "1e3", "\"1\"", "true", "null", "18446744073709551616"}) { const std::string body = "{\"access_token\":\"a\",\"refresh_token\":\"r\"," "\"token_type\":\"Bearer\",\"expires_in\":" + expires + "}"; expect_failure([&] { (void)parse_oauth_token_response(body, 0); }); } } void test_wrong_types_and_required_values() { const std::vector invalid = {"{}", "[]", "null", "true", "{\"access_token\":1,\"refresh_token\":\"r\",\"token_type\":\"Bearer\"," "\"expires_in\":1}", "{\"access_token\":\"\",\"refresh_token\":\"r\",\"token_type\":\"Bearer\"," "\"expires_in\":1}", "{\"access_token\":\"a\",\"refresh_token\":1,\"token_type\":\"Bearer\"," "\"expires_in\":1}", "{\"access_token\":\"a\",\"refresh_token\":\"r\",\"token_type\":1," "\"expires_in\":1}", "{\"access_token\":\"a\",\"refresh_token\":\"r\",\"token_type\":\"Basic\"," "\"expires_in\":1}", "{\"access_token\":\"a\",\"refresh_token\":\"r\",\"token_type\":\"Bearer\"," "\"expires_in\":1,\"scope\":\"\"}", "{\"access_token\":\"a\",\"refresh_token\":\"r\",\"token_type\":\"Bearer\"," "\"expires_in\":1,\"scope\":[]}"}; for (const std::string& body : invalid) { expect_failure([&] { (void)parse_oauth_token_response(body, 0); }); } expect_failure([&] { (void)parse_oauth_token_response( "{\"access_token\":\"a\\n\",\"refresh_token\":\"r\"," "\"token_type\":\"Bearer\",\"expires_in\":1}", 0); }); } void test_duplicate_keys_at_every_depth() { const std::vector duplicates = { "{\"access_token\":\"first\",\"access_token\":\"second\"," "\"refresh_token\":\"r\",\"token_type\":\"Bearer\",\"expires_in\":1}", "{\"access_token\":\"a\",\"refresh_token\":\"r\"," "\"token_type\":\"Bearer\",\"expires_in\":1,\"unknown\":1,\"unknown\":2}", "{\"access_token\":\"a\",\"refresh_token\":\"r\"," "\"token_type\":\"Bearer\",\"expires_in\":1," "\"unknown\":{\"duplicate\":1,\"duplicate\":2}}"}; for (const std::string& body : duplicates) { expect_failure([&] { (void)parse_oauth_token_response(body, 0); }); } } void test_invalid_json_utf8_size_and_redaction() { std::vector invalid = {"{", "not-json"}; invalid.push_back(std::string("{\"access_token\":\"") + static_cast(0xff) + "\",\"refresh_token\":\"r\",\"token_type\":\"Bearer\",\"expires_in\":1}"); for (const std::string& body : invalid) { const std::string message = expect_failure( [&] { (void)parse_oauth_token_response(body, 0); }); require(message.find(body) == std::string::npos, "parser error leaked the invalid response body"); } const std::string prefix = "{\"access_token\":\"BOUNDARY_ACCESS_TOKEN\"," "\"refresh_token\":\"BOUNDARY_REFRESH_TOKEN\"," "\"token_type\":\"Bearer\",\"expires_in\":1,\"padding\":\""; const std::string suffix = "\"}"; std::string exact = prefix; exact.append(maximum_response_size - prefix.size() - suffix.size(), 'x'); exact += suffix; require(parse_oauth_token_response(exact, 0).access_token == "BOUNDARY_ACCESS_TOKEN", "exactly 1 MiB token response was rejected"); std::string oversized = exact; oversized.push_back(' '); const std::string oversized_message = expect_failure([&] { (void)parse_oauth_token_response(oversized, 0); }); require(oversized_message.find("BOUNDARY") == std::string::npos, "size parser error leaked token material"); const std::string malicious_body = "{\"access_token\":\"MALICIOUS_ACCESS_TOKEN\"," "\"refresh_token\":\"MALICIOUS_REFRESH_TOKEN\"," "\"token_type\":\"Basic\",\"expires_in\":1}"; const std::string malicious_message = expect_failure([&] { (void)parse_oauth_token_response(malicious_body, 0); }); require(malicious_message.find("MALICIOUS") == std::string::npos, "semantic parser error leaked token material"); const std::string retained_secret = "MALICIOUS_RETAINED_TOKEN"; const std::string retained_message = expect_failure([&] { (void)parse_oauth_token_response( "{\"access_token\":\"a\",\"token_type\":\"Basic\",\"expires_in\":1}", 0, retained_secret); }); require(retained_message.find(retained_secret) == std::string::npos, "parser error leaked retained refresh token"); } } // namespace int main() { try { test_success_and_unknown_fields(); test_retained_and_rotated_refresh_tokens(); test_expiration_boundaries_and_integral_types(); test_wrong_types_and_required_values(); test_duplicate_keys_at_every_depth(); test_invalid_json_utf8_size_and_redaction(); } catch (const std::exception& error) { std::cerr << "OAuth token tests failed: " << error.what() << '\n'; return 1; } std::cout << "OAuth token tests passed\n"; return 0; }