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:
43
include/nocal/sync/http.hpp
Normal file
43
include/nocal/sync/http.hpp
Normal 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
|
||||
82
include/nocal/sync/oauth.hpp
Normal file
82
include/nocal/sync/oauth.hpp
Normal 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
|
||||
Reference in New Issue
Block a user