server/src/http_auth.h
Rama main · réplica de lectura del repositorio original.
#pragma once
/**
* @file src/http_auth.h
* @brief Declarations for HTTP authentication, API tokens, and session token management utilities.
*/
// standard includes
#include <chrono>
#include <exception>
#include <filesystem>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
// local includes
// platform includes
#include <boost/function.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>
#include <nlohmann/json.hpp>
#include <Simple-Web-Server/server_https.hpp>
namespace confighttp {
inline constexpr std::string_view session_cookie_name {"__Host-apollo_session"};
inline constexpr std::string_view refresh_cookie_name {"__Host-apollo_refresh"};
using StatusCode = SimpleWeb::StatusCode;
using resp_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Response>;
using req_https_t = std::shared_ptr<typename SimpleWeb::ServerBase<SimpleWeb::HTTPS>::Request>;
struct AuthResult {
bool ok;
StatusCode code;
std::string body;
SimpleWeb::CaseInsensitiveMultimap headers;
};
struct ApiTokenManagerDependencies {
boost::function<bool(const std::string &)> file_exists;
boost::function<void(const std::string &, boost::property_tree::ptree &)> read_json;
boost::function<void(const std::string &, const boost::property_tree::ptree &)> write_json;
boost::function<std::chrono::system_clock::time_point()> now;
boost::function<std::string(std::size_t)> rand_alphabet;
boost::function<std::string(const std::string &)> hash;
ApiTokenManagerDependencies() = default;
ApiTokenManagerDependencies(const ApiTokenManagerDependencies &) = default;
ApiTokenManagerDependencies &operator=(const ApiTokenManagerDependencies &) = default;
ApiTokenManagerDependencies(ApiTokenManagerDependencies &&) noexcept = default;
ApiTokenManagerDependencies &operator=(ApiTokenManagerDependencies &&) noexcept = default;
};
struct ApiTokenInfo {
std::string hash;
std::map<std::string, std::set<std::string, std::less<>>, std::less<>> path_methods;
std::string username;
std::chrono::system_clock::time_point created_at;
};
class InvalidScopeException: public std::exception {
public:
/**
* @brief Construct with a human readable message.
* @param msg Explanation of the invalid scope condition.
*/
explicit InvalidScopeException(const std::string &msg);
/**
* @brief Describe the error.
* @return Null terminated message string.
*/
const char *what() const noexcept override;
private:
std::string _message; ///< Stored explanatory message
};
class ApiTokenManager {
public:
/**
* @brief Construct a manager with injected dependencies.
* @param dependencies Dependency functors (defaults to production implementations).
*/
explicit ApiTokenManager(const ApiTokenManagerDependencies &dependencies = ApiTokenManager::make_default_dependencies());
/**
* @brief Authenticate a Bearer token for a given path and method.
* @param raw_auth Raw Authorization header value (e.g. "Bearer <token>").
* @param path Requested URI path.
* @param method HTTP method (e.g. GET / POST ...).
* @return `true` if authorized for the requested scope.
*/
bool authenticate_bearer(std::string_view raw_auth, const std::string &path, const std::string &method);
/**
* @brief Generate a new API token from a creation request body.
* @param request_body JSON string describing requested scopes.
* @param username User who owns the token.
* @return The plain token string (not hashed) or `std::nullopt` on failure.
*/
std::optional<std::string> generate_api_token(const std::string &request_body, const std::string &username);
/**
* @brief Serialize the loaded API tokens to JSON.
* @return JSON string listing tokens (hashed) and metadata.
*/
std::string list_api_tokens_json() const;
/**
* @brief Authenticate using a raw token (not hash) for path & method.
* @param token The un-hashed provided token.
* @param path Target path.
* @param method HTTP method.
* @return `true` if token exists and includes the scope.
*/
bool authenticate_token(const std::string &token, const std::string &path, const std::string &method);
/**
* @brief Create and store a token from structured JSON scopes.
* @param scopes_json JSON specifying scopes (path->methods mapping).
* @param username Owner username.
* @return The plain token value, or `std::nullopt` if validation fails.
*/
std::optional<std::string> create_api_token(const nlohmann::json &scopes_json, const std::string &username);
/**
* @brief Get a structured list of tokens.
* @return JSON array/object of tokens and properties.
*/
nlohmann::json get_api_tokens_list() const;
/**
* @brief Revoke a token by hash (mutable operation).
* @param hash Hash of the token.
* @return `true` if removed.
*/
bool revoke_api_token_by_hash(const std::string &hash);
/**
* @brief Persist tokens to backing storage.
*/
void save_api_tokens() const;
/**
* @brief Load tokens from backing storage.
*/
void load_api_tokens();
/**
* @brief Provide default dependency functors.
* @return Dependencies configured with production behaviors.
*/
static ApiTokenManagerDependencies make_default_dependencies();
/**
* @brief Access currently loaded tokens (thread-safe for reading via mutex).
* @return Map of token hash to token info.
*/
const std::map<std::string, ApiTokenInfo, std::less<>> &retrieve_loaded_api_tokens() const;
private:
/**
* @brief Parse scope JSON into internal map form.
* @param scopes_json Incoming JSON object.
* @return Scope map or `std::nullopt` on validation error.
*/
std::optional<std::map<std::string, std::set<std::string, std::less<>>, std::less<>>>
parse_json_scopes(const nlohmann::json &scopes_json) const;
/**
* @brief Parse a single scope entry from a property tree representation.
* @param scope_tree Scope node tree.
* @return Pair of path and method set or `std::nullopt` if invalid.
*/
std::optional<std::pair<std::string, std::set<std::string, std::less<>>>> parse_scope(const boost::property_tree::ptree &scope_tree) const;
/**
* @brief Build scope mapping from PT subtree.
* @param scopes_node Node containing scopes array/objects.
* @return Map path->allowed methods set.
*/
std::map<std::string, std::set<std::string, std::less<>>, std::less<>> build_scope_map(const boost::property_tree::ptree &scopes_node) const;
ApiTokenManagerDependencies _dependencies; ///< Injected dependencies
mutable std::mutex _mutex; ///< Guards token container
std::map<std::string, ApiTokenInfo, std::less<>> _api_tokens; ///< Token storage keyed by hash
};
struct SessionToken {
std::string username;
std::chrono::system_clock::time_point created_at;
std::chrono::system_clock::time_point expires_at;
std::chrono::system_clock::time_point refresh_expires_at;
std::string user_agent;
std::string remote_address;
std::chrono::system_clock::time_point last_seen;
bool remember_me;
std::string device_label;
std::string refresh_token_hash;
std::string rotation_id;
};
struct SessionTokenView {
std::string hash;
std::string username;
std::chrono::system_clock::time_point created_at;
std::chrono::system_clock::time_point expires_at;
std::chrono::system_clock::time_point refresh_expires_at;
std::chrono::system_clock::time_point last_seen;
bool remember_me;
std::string user_agent;
std::string remote_address;
std::string device_label;
};
struct SessionTokenBundle {
std::string session_token;
std::string refresh_token;
std::chrono::seconds session_ttl;
std::chrono::seconds refresh_ttl;
bool remember_me;
};
struct SessionTokenManagerDependencies {
boost::function<std::chrono::system_clock::time_point()> now;
boost::function<std::string(std::size_t)> rand_alphabet;
boost::function<std::string(const std::string &)> hash;
boost::function<bool(const std::string &)> file_exists;
boost::function<void(const std::string &, boost::property_tree::ptree &)> read_json;
boost::function<void(const std::string &, const boost::property_tree::ptree &)> write_json;
SessionTokenManagerDependencies() = default;
SessionTokenManagerDependencies(const SessionTokenManagerDependencies &) = default;
SessionTokenManagerDependencies &operator=(const SessionTokenManagerDependencies &) = default;
SessionTokenManagerDependencies(SessionTokenManagerDependencies &&) noexcept = default;
SessionTokenManagerDependencies &operator=(SessionTokenManagerDependencies &&) noexcept = default;
};
class SessionTokenManager {
public:
/**
* @brief Construct with injected dependencies.
* @param dependencies Dependency functors (defaults available).
*/
explicit SessionTokenManager(const SessionTokenManagerDependencies &dependencies = SessionTokenManager::make_default_dependencies());
/**
* @brief Create a new session token for a user.
* @param username Account name.
* @param lifetime Desired lifetime; defaults to config value when zero.
* @return Newly generated opaque token string.
*/
std::string generate_session_token(const std::string &username, std::chrono::seconds lifetime = std::chrono::seconds::zero(), const std::string &user_agent = std::string {}, const std::string &remote_address = std::string {}, bool remember_me = false);
/**
* @brief Issue a new access + refresh token pair.
* @param username Account to bind.
* @param session_ttl Optional access token lifetime (falls back to config when zero).
* @param refresh_ttl Optional refresh token lifetime (defaults to sliding/remember-me policy).
* @param user_agent Reported user agent string.
* @param remote_address Source address.
* @param remember_me Whether to extend refresh lifetime for long-lived device trust.
* @return Bundle containing raw tokens and effective TTLs.
*/
SessionTokenBundle issue_session_tokens(const std::string &username, std::chrono::seconds session_ttl = std::chrono::seconds::zero(), std::chrono::seconds refresh_ttl = std::chrono::seconds::zero(), const std::string &user_agent = std::string {}, const std::string &remote_address = std::string {}, bool remember_me = false);
/**
* @brief Refresh a session using a refresh token.
* @param refresh_token Raw refresh token value.
* @param user_agent Current user agent (for device labeling).
* @param remote_address Current client address.
* @return New token bundle if valid; std::nullopt otherwise.
*/
std::optional<SessionTokenBundle> refresh_session_tokens(const std::string &refresh_token, const std::string &user_agent = std::string {}, const std::string &remote_address = std::string {});
/**
* @brief Validate a session token (and update internal state if expired).
* @param token Opaque token string.
* @return `true` if exists and not expired.
*/
bool validate_session_token(const std::string &token);
/**
* @brief Revoke (delete) a session token.
* @param token Token to remove.
*/
void revoke_session_token(const std::string &token);
/**
* @brief Remove all expired tokens.
*/
bool cleanup_expired_session_tokens();
/**
* @brief Lookup username for a token.
* @param token Token string.
* @return Username or `std::nullopt` if not found/expired.
*/
std::optional<std::string> get_username_for_token(const std::string &token);
std::optional<std::string> get_hash_for_token(const std::string &token) const;
/**
* @brief Count active session tokens.
* @return Number of stored (possibly unexpired) sessions.
*/
size_t session_count() const;
/**
* @brief Create default dependency set.
* @return Dependency functors.
*/
static SessionTokenManagerDependencies make_default_dependencies();
/**
* @brief Persist session tokens to disk.
*/
void save_session_tokens() const;
/**
* @brief Load session tokens from disk, discarding expired entries.
*/
void load_session_tokens();
/**
* @brief List session tokens, optionally filtered by username.
* @param username_filter Case-insensitive user filter (empty for all).
* @return Snapshot list of sessions.
*/
std::vector<SessionTokenView> list_sessions(const std::string &username_filter = std::string {}) const;
/**
* @brief Revoke a session token by its stored hash.
* @param token_hash Hashed token identifier.
* @return `true` if a session was removed.
*/
bool revoke_session_by_hash(const std::string &token_hash);
/**
* @brief Revoke a session using its refresh token value.
* @param refresh_token Raw refresh token.
* @return `true` if a session was removed.
*/
bool revoke_refresh_token(const std::string &refresh_token);
private:
SessionTokenManagerDependencies _dependencies; ///< Injected dependencies
mutable std::mutex _mutex; ///< Guards session token map
struct TransparentStringHash {
using is_transparent = void;
std::size_t operator()(std::string_view txt) const noexcept {
return std::hash<std::string_view> {}(txt);
}
std::size_t operator()(const std::string &txt) const noexcept {
return std::hash<std::string_view> {}(txt);
}
std::size_t operator()(const char *txt) const noexcept {
return std::hash<std::string_view> {}(txt);
}
};
std::unordered_map<std::string, SessionToken, TransparentStringHash, std::equal_to<>> _session_tokens; ///< Active session tokens keyed by hash
std::unordered_map<std::string, std::string, TransparentStringHash, std::equal_to<>> _refresh_index; ///< Maps refresh token hash -> session hash
mutable bool _dirty = false; ///< Tracks whether persistence is required
mutable std::chrono::system_clock::time_point _last_persist; ///< Last persistence timestamp
};
struct APIResponse {
StatusCode status_code;
std::string body;
SimpleWeb::CaseInsensitiveMultimap headers;
/**
* @brief Construct a response object.
* @param code HTTP status code.
* @param response_body Body payload.
* @param response_headers Extra headers (case-insensitive multimap).
*/
APIResponse(StatusCode code, std::string response_body = "", SimpleWeb::CaseInsensitiveMultimap response_headers = {}):
status_code(code),
body(std::move(response_body)),
headers(std::move(response_headers)) {}
};
class SessionTokenAPI {
public:
/**
* @brief Construct API facade referencing a session manager.
* @param session_manager Session manager instance.
*/
explicit SessionTokenAPI(SessionTokenManager &session_manager);
/**
* @brief Handle login request validating credentials and returning session token.
* @param username User name.
* @param password Plain password candidate.
* @param redirect_url Optional redirect URL for HTML flows.
* @param remember_me Whether the session should persist beyond the browser session.
* @return API response containing token or error.
*/
APIResponse login(const std::string &username, const std::string &password, const std::string &redirect_url = "/", bool remember_me = false, const std::string &user_agent = std::string {}, const std::string &remote_address = std::string {});
/**
* @brief Invalidate the provided session token.
* @param session_token Token to revoke.
* @return Success or error response.
*/
APIResponse logout(const std::string &session_token, const std::string &refresh_token = std::string {});
/**
* @brief Validate a session token for an active session.
* @param session_token Token to validate.
* @return Response containing validity state.
*/
APIResponse validate_session(const std::string &session_token);
/**
* @brief Refresh a session using a refresh token.
* @param refresh_token Refresh token value from cookie/header/body.
* @param user_agent User agent to associate with the renewed session.
* @param remote_address Remote address for device labeling.
* @return API response with new cookies and token payload.
*/
APIResponse refresh_session(const std::string &refresh_token, const std::string &user_agent, const std::string &remote_address);
/**
* @brief List active sessions for the current user.
* @param username_filter Optional username filter.
* @param active_session_hash Hash of the session making the request.
* @return Response with session list payload.
*/
APIResponse list_sessions(const std::string &username_filter, const std::string &active_session_hash = std::string {}) const;
/**
* @brief Revoke a session identified by its hash.
* @param session_hash Target session hash.
* @return Success or error response.
*/
APIResponse revoke_session_by_hash(const std::string &session_hash);
private:
SessionTokenManager &_session_manager; ///< Managed session token store reference
/**
* @brief Validate supplied credentials (implementation-defined policy).
* @param username Name.
* @param password Password candidate.
* @return `true` if credentials accepted.
*/
bool validate_credentials(const std::string &username, const std::string &password) const;
/**
* @brief Build a success JSON response.
* @param data Additional JSON payload.
* @return Response with status `200` and JSON body.
*/
APIResponse create_success_response(const nlohmann::json &data = {}) const;
/**
* @brief Build an error JSON response.
* @param error_message Human readable error.
* @param status_code HTTP status code (default 400).
* @return Response with error payload.
*/
APIResponse create_error_response(const std::string &error_message, StatusCode status_code = StatusCode::client_error_bad_request) const;
};
/**
* @brief Construct an AuthResult representing an authentication error (no Basic auth support).
* @param code HTTP status to return.
* @param error Message body.
* @return Structured AuthResult.
*/
AuthResult make_auth_error(StatusCode code, const std::string &error);
/**
* @brief Check a Bearer token Authorization header.
* @param raw_auth Header value.
* @param path Requested path.
* @param method HTTP method.
* @return AuthResult with authorization outcome.
*/
AuthResult check_bearer_auth(const std::string &raw_auth, const std::string &path, const std::string &method);
/**
* @brief Check session cookie / header authentication.
* @param raw_auth Raw header or cookie string.
* @return AuthResult with outcome.
*/
AuthResult check_session_auth(const std::string &raw_auth);
/**
* @brief Determine whether a request path expects an HTML response.
* @param path Requested URI path.
* @return `true` if path indicates HTML content negotiation.
*/
bool is_html_request(const std::string &path);
/**
* @brief Perform layered auth checks (session, bearer, basic) for a request.
* @param remote_address Client IP / address.
* @param auth_header Authorization header value.
* @param path Request path.
* @param method HTTP method.
* @return AuthResult representing authentication decision.
*/
AuthResult check_auth(const std::string &remote_address, const std::string &auth_header, const std::string &path, const std::string &method);
/**
* @brief Extract session token from Cookie headers.
* @param headers Case-insensitive header multimap.
* @return Token string (may be empty if not present).
*/
std::string extract_session_token_from_cookie(const SimpleWeb::CaseInsensitiveMultimap &headers);
/**
* @brief Extract refresh token from Cookie headers.
* @param headers Case-insensitive header multimap.
* @return Token string (may be empty if not present).
*/
std::string extract_refresh_token_from_cookie(const SimpleWeb::CaseInsensitiveMultimap &headers);
extern ApiTokenManager api_token_manager;
extern SessionTokenManager session_token_manager;
extern SessionTokenAPI session_token_api;
} // namespace confighttp