#include "nocal/sync/token_broker.hpp" #include "nocal/sync/oauth.hpp" #include "nocal/sync/oauth_tokens.hpp" #include "nocal/sync/secret_store.hpp" #include #include #include #include #include namespace nocal::sync { namespace { [[nodiscard]] std::string percent_encode(std::string_view value) { static constexpr char hex[] = "0123456789ABCDEF"; std::string result; result.reserve(value.size()); for (const unsigned char character : value) { const bool ascii_alphanumeric = (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z') || (character >= '0' && character <= '9'); if (ascii_alphanumeric || character == '-' || character == '.' || character == '_' || character == '~') { result.push_back(static_cast(character)); } else { result.push_back('%'); result.push_back(hex[character >> 4U]); result.push_back(hex[character & 0x0fU]); } } return result; } [[nodiscard]] std::string join_scopes(const std::vector& scopes) { std::string result; for (const std::string& scope : scopes) { if (!result.empty()) { result.push_back(' '); } result += scope; } return result; } void append_parameter( std::string& output, bool& first, std::string_view name, std::string_view value) { if (!first) { output.push_back('&'); } first = false; output += name; output.push_back('='); output += percent_encode(value); } [[nodiscard]] std::string make_refresh_body(const OAuthClientConfig& config, std::string_view refresh_token, const std::string& scope) { std::string body; bool first = true; append_parameter(body, first, "grant_type", "refresh_token"); append_parameter(body, first, "client_id", config.client_id); append_parameter(body, first, "refresh_token", refresh_token); if (!scope.empty()) { append_parameter(body, first, "scope", scope); } return body; } } // namespace OAuthTokenBroker::OAuthTokenBroker(const OAuthClientConfig& config, HttpTransport& transport, OAuthSecretStore& secret_store, std::function clock) : config_(config), transport_(transport), secret_store_(secret_store), clock_(clock) {} OAuthTokens OAuthTokenBroker::valid_tokens(std::string_view account_id) { // 1. Load stored tokens std::optional stored_opt; try { stored_opt = secret_store_.load(std::string(account_id)); } catch (const SecretStoreError&) { throw; } catch (const std::exception& e) { throw std::runtime_error(std::string("failed to load tokens: ") + e.what()); } if (!stored_opt.has_value()) { throw std::runtime_error("account not connected: " + std::string(account_id)); } const OAuthTokens& stored = *stored_opt; const std::int64_t now = clock_(); // 2. Check if token is still valid (beyond now + 300s skew) if (stored.expires_at_epoch_seconds > now + refresh_skew_seconds) { return stored; } // 3. Refresh token const std::string scope = join_scopes(config_.scopes); const std::string body = make_refresh_body(config_, stored.refresh_token, scope); HttpRequest request; request.method = HttpMethod::post; request.url = config_.token_endpoint; request.headers = {{"Content-Type", "application/x-www-form-urlencoded"}}; request.body = body; HttpResponse response; try { response = transport_.send(request); } catch (const std::exception& e) { throw std::runtime_error(std::string("transport error: ") + e.what()); } // 4. Handle response if (response.status == 200) { OAuthTokens parsed; try { parsed = parse_oauth_token_response(response.body, now, stored.refresh_token); } catch (const std::exception& e) { throw std::runtime_error(std::string("parse error: ") + e.what()); } // Persist first, then return try { secret_store_.store(std::string(account_id), parsed); } catch (const std::exception& e) { throw std::runtime_error(std::string("store error: ") + e.what()); } return parsed; } else if (response.status == 400 || response.status == 401) { try { secret_store_.erase(std::string(account_id)); } catch (const std::exception& e) { throw std::runtime_error(std::string("erase error: ") + e.what()); } throw AccountDisconnected("account " + std::string(account_id) + " disconnected"); } else { throw std::runtime_error("unexpected status code: " + std::to_string(response.status)); } } } // namespace nocal::sync