feat: add PKCE OAuth boundary

Introduce provider-neutral HTTP contracts and public-client authorization-code orchestration with PKCE S256, strict HTTPS and loopback validation, constant-time state checks, secure OpenSSL entropy, and single-send token exchange semantics.

Add independent protocol and adversarial suites covering RFC 7636, RFC3986 encoding, callback failures, response redaction, and no-retry behavior. Concrete networking, browser/listener, token parsing, Secret Service, and Graph integration remain separate follow-up slices.
This commit is contained in:
2026-07-18 11:11:28 +01:00
parent 787daf00dd
commit dd6e02cb55
11 changed files with 1572 additions and 25 deletions

View File

@@ -0,0 +1,43 @@
#pragma once
#include <string>
#include <vector>
namespace nocal::sync {
enum class HttpMethod { get, post, put, patch, delete_ };
struct HttpHeader {
std::string name;
std::string value;
bool operator==(const HttpHeader&) const = default;
};
struct HttpRequest {
HttpMethod method{HttpMethod::get};
std::string url;
std::vector<HttpHeader> headers;
std::string body;
bool operator==(const HttpRequest&) const = default;
};
struct HttpResponse {
int status{0};
std::vector<HttpHeader> headers;
std::string body;
bool operator==(const HttpResponse&) const = default;
};
class HttpTransport {
public:
virtual ~HttpTransport() = default;
// Performs exactly one request. Retry policy belongs to the caller because
// only the caller knows whether replay is safe.
virtual HttpResponse send(const HttpRequest& request) = 0;
};
} // namespace nocal::sync

View File

@@ -0,0 +1,82 @@
#pragma once
#include "nocal/sync/http.hpp"
#include <cstddef>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
namespace nocal::sync {
struct OAuthClientConfig {
std::string client_id;
std::string authorization_endpoint;
std::string token_endpoint;
std::vector<std::string> scopes;
};
struct AuthorizationCallback {
std::string code;
std::string state;
std::string error;
std::string error_description;
};
class OAuthError : public std::runtime_error {
public:
enum class Kind {
invalid_configuration,
invalid_redirect,
invalid_callback,
state_mismatch,
authorization_rejected,
token_endpoint_rejected,
cryptographic_failure,
};
OAuthError(Kind kind, std::string message);
[[nodiscard]] Kind kind() const noexcept;
private:
Kind kind_;
};
class EntropySource {
public:
virtual ~EntropySource() = default;
virtual void fill(std::span<std::byte> output) = 0;
};
class SystemEntropySource final : public EntropySource {
public:
void fill(std::span<std::byte> output) override;
};
class BrowserLauncher {
public:
virtual ~BrowserLauncher() = default;
virtual void open(const std::string& url) = 0;
};
class AuthorizationCallbackReceiver {
public:
virtual ~AuthorizationCallbackReceiver() = default;
// The receiver chooses and starts its loopback endpoint before returning
// this URI. receive() owns timeout and socket lifecycle behavior.
[[nodiscard]] virtual std::string redirect_uri() const = 0;
virtual AuthorizationCallback receive() = 0;
};
// Runs one public-client authorization-code attempt with PKCE S256. It opens
// one browser URL, accepts one callback, and performs at most one token request.
// The opaque response body is intentionally left for a provider adapter to
// parse. This function does not persist credentials or retry requests.
[[nodiscard]] HttpResponse authorize_with_pkce(const OAuthClientConfig& config,
BrowserLauncher& browser, AuthorizationCallbackReceiver& receiver,
HttpTransport& transport, EntropySource& entropy);
} // namespace nocal::sync