This commit is contained in:
doomtube 2025-08-13 00:10:25 -04:00
parent e8864cc853
commit 1d42a9a623
9 changed files with 2122 additions and 542 deletions

View file

@ -1,6 +1,7 @@
#include "UserController.h"
#include "../services/DatabaseService.h"
#include <drogon/MultiPart.h>
#include <drogon/Cookie.h>
#include <fstream>
#include <random>
#include <sstream>
@ -51,18 +52,46 @@ namespace {
return false;
}
}
// Helper to set httpOnly auth cookie
void setAuthCookie(const HttpResponsePtr& resp, const std::string& token) {
Cookie authCookie("auth_token", token);
authCookie.setPath("/");
authCookie.setHttpOnly(true);
authCookie.setSecure(false); // Set to true in production with HTTPS
authCookie.setMaxAge(86400); // 24 hours
authCookie.setSameSite(Cookie::SameSite::kLax);
resp->addCookie(authCookie);
}
// Helper to clear auth cookie
void clearAuthCookie(const HttpResponsePtr& resp) {
Cookie authCookie("auth_token", "");
authCookie.setPath("/");
authCookie.setHttpOnly(true);
authCookie.setMaxAge(0); // Expire immediately
resp->addCookie(authCookie);
}
}
UserInfo UserController::getUserFromRequest(const HttpRequestPtr &req) {
UserInfo user;
std::string auth = req->getHeader("Authorization");
if (auth.empty() || auth.substr(0, 7) != "Bearer ") {
return user;
// First try to get from cookie
std::string token = req->getCookie("auth_token");
// Fallback to Authorization header for API clients
if (token.empty()) {
std::string auth = req->getHeader("Authorization");
if (!auth.empty() && auth.substr(0, 7) == "Bearer ") {
token = auth.substr(7);
}
}
if (!token.empty()) {
AuthService::getInstance().validateToken(token, user);
}
std::string token = auth.substr(7);
AuthService::getInstance().validateToken(token, user);
return user;
}
@ -168,9 +197,10 @@ void UserController::login(const HttpRequestPtr &req,
[callback, username](bool success, const std::string& token, const UserInfo& user) {
if (success) {
LOG_INFO << "Login successful for user: " << username;
Json::Value resp;
resp["success"] = true;
resp["token"] = token;
// Don't send token in body for cookie-based auth
resp["user"]["id"] = static_cast<Json::Int64>(user.id);
resp["user"]["username"] = user.username;
resp["user"]["isAdmin"] = user.isAdmin;
@ -179,8 +209,11 @@ void UserController::login(const HttpRequestPtr &req,
resp["user"]["bio"] = user.bio;
resp["user"]["avatarUrl"] = user.avatarUrl;
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
resp["user"]["colorCode"] = user.colorCode; // Use colorCode for consistency with database field name
callback(jsonResp(resp));
resp["user"]["colorCode"] = user.colorCode;
auto response = jsonResp(resp);
setAuthCookie(response, token);
callback(response);
} else {
LOG_WARN << "Login failed for user: " << username;
callback(jsonError(token.empty() ? "Invalid credentials" : token, k401Unauthorized));
@ -195,6 +228,22 @@ void UserController::login(const HttpRequestPtr &req,
}
}
void UserController::logout(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
try {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Logged out successfully";
auto response = jsonResp(resp);
clearAuthCookie(response);
callback(response);
} catch (const std::exception& e) {
LOG_ERROR << "Exception in logout: " << e.what();
callback(jsonError("Internal server error"));
}
}
void UserController::pgpChallenge(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
try {
@ -264,7 +313,7 @@ void UserController::pgpVerify(const HttpRequestPtr &req,
if (success) {
Json::Value resp;
resp["success"] = true;
resp["token"] = token;
// Don't send token in body for cookie-based auth
resp["user"]["id"] = static_cast<Json::Int64>(user.id);
resp["user"]["username"] = user.username;
resp["user"]["isAdmin"] = user.isAdmin;
@ -273,8 +322,11 @@ void UserController::pgpVerify(const HttpRequestPtr &req,
resp["user"]["bio"] = user.bio;
resp["user"]["avatarUrl"] = user.avatarUrl;
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
resp["user"]["colorCode"] = user.colorCode; // Add colorCode to PGP login response
callback(jsonResp(resp));
resp["user"]["colorCode"] = user.colorCode;
auto response = jsonResp(resp);
setAuthCookie(response, token);
callback(response);
} else {
callback(jsonError("Invalid signature", k401Unauthorized));
}
@ -782,7 +834,7 @@ void UserController::updateColor(const HttpRequestPtr &req,
AuthService::getInstance().updateUserColor(user.id, newColor,
[callback, user](bool success, const std::string& error, const std::string& finalColor) {
if (success) {
// Fetch updated user info and generate new token with updated color
// Fetch updated user info
AuthService::getInstance().fetchUserInfo(user.id,
[callback, finalColor](bool fetchSuccess, const UserInfo& updatedUser) {
if (fetchSuccess) {
@ -792,13 +844,16 @@ void UserController::updateColor(const HttpRequestPtr &req,
Json::Value resp;
resp["success"] = true;
resp["color"] = finalColor;
resp["token"] = newToken; // Return new token with updated color
resp["user"]["id"] = static_cast<Json::Int64>(updatedUser.id);
resp["user"]["username"] = updatedUser.username;
resp["user"]["isAdmin"] = updatedUser.isAdmin;
resp["user"]["isStreamer"] = updatedUser.isStreamer;
resp["user"]["colorCode"] = updatedUser.colorCode;
callback(jsonResp(resp));
auto response = jsonResp(resp);
// Update auth cookie with new token
setAuthCookie(response, newToken);
callback(response);
} else {
// Color was updated but couldn't fetch full user info
Json::Value resp;

View file

@ -9,6 +9,7 @@ public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(UserController::register_, "/api/auth/register", Post);
ADD_METHOD_TO(UserController::login, "/api/auth/login", Post);
ADD_METHOD_TO(UserController::logout, "/api/auth/logout", Post);
ADD_METHOD_TO(UserController::pgpChallenge, "/api/auth/pgp-challenge", Post);
ADD_METHOD_TO(UserController::pgpVerify, "/api/auth/pgp-verify", Post);
ADD_METHOD_TO(UserController::getCurrentUser, "/api/user/me", Get);
@ -30,6 +31,9 @@ public:
void login(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
void logout(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
void pgpChallenge(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);

View file

@ -6,6 +6,9 @@
#include <random>
#include <functional>
#include <memory>
#include <fstream>
#include <sstream>
#include <cstdlib>
using namespace drogon;
using namespace drogon::orm;
@ -29,6 +32,159 @@ bool AuthService::validatePassword(const std::string& password, std::string& err
return true;
}
// Helper function to execute GPG commands
std::string executeGpgCommand(const std::string& command) {
std::array<char, 128> buffer;
std::string result;
FILE* pipe = popen(command.c_str(), "r");
if (!pipe) {
LOG_ERROR << "Failed to execute GPG command: " << command;
return "";
}
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
result += buffer.data();
}
int exitCode = pclose(pipe);
// Exit code is returned as status << 8, so we need to extract the actual exit code
int actualExitCode = WEXITSTATUS(exitCode);
if (actualExitCode != 0) {
LOG_ERROR << "GPG command failed with exit code: " << actualExitCode
<< " for command: " << command
<< " output: " << result;
// Don't return empty string immediately - sometimes GPG returns non-zero but still works
}
return result;
}
// Server-side PGP signature verification
bool verifyPgpSignature(const std::string& message, const std::string& signature, const std::string& publicKey) {
try {
// Create temporary directory for GPG operations
std::string tmpDir = "/tmp/pgp_verify_" + drogon::utils::genRandomString(8);
std::string mkdirCmd = "mkdir -p " + tmpDir;
if (system(mkdirCmd.c_str()) != 0) {
LOG_ERROR << "Failed to create temporary directory: " << tmpDir;
return false;
}
// Create GPG home directory
std::string keyringDir = tmpDir + "/gnupg";
std::string mkdirGpgCmd = "mkdir -p " + keyringDir + " && chmod 700 " + keyringDir;
if (system(mkdirGpgCmd.c_str()) != 0) {
LOG_ERROR << "Failed to create GPG home directory: " << keyringDir;
std::string cleanupCmd = "rm -rf " + tmpDir;
system(cleanupCmd.c_str());
return false;
}
// Write files
std::string messageFile = tmpDir + "/message.txt";
std::string sigFile = tmpDir + "/signature.asc";
std::string pubkeyFile = tmpDir + "/pubkey.asc";
// Write message file
std::ofstream msgOut(messageFile);
if (!msgOut) {
LOG_ERROR << "Failed to create message file: " << messageFile;
std::string cleanupCmd = "rm -rf " + tmpDir;
system(cleanupCmd.c_str());
return false;
}
msgOut << message;
msgOut.close();
// Write signature file
std::ofstream sigOut(sigFile);
if (!sigOut) {
LOG_ERROR << "Failed to create signature file: " << sigFile;
std::string cleanupCmd = "rm -rf " + tmpDir;
system(cleanupCmd.c_str());
return false;
}
sigOut << signature;
sigOut.close();
// Write public key file
std::ofstream keyOut(pubkeyFile);
if (!keyOut) {
LOG_ERROR << "Failed to create public key file: " << pubkeyFile;
std::string cleanupCmd = "rm -rf " + tmpDir;
system(cleanupCmd.c_str());
return false;
}
keyOut << publicKey;
keyOut.close();
// Initialize GPG (create trustdb if needed)
std::string initCmd = "GNUPGHOME=" + keyringDir + " gpg --batch --yes --list-keys 2>&1";
executeGpgCommand(initCmd); // This will create the trustdb if it doesn't exist
// Import the public key to the temporary keyring
// Use --trust-model always to avoid trust issues
std::string importCmd = "GNUPGHOME=" + keyringDir +
" gpg --batch --yes --trust-model always --import " + pubkeyFile + " 2>&1";
std::string importResult = executeGpgCommand(importCmd);
LOG_DEBUG << "GPG import result: " << importResult;
// Check if import was successful (be more lenient with the check)
bool importSuccess = (importResult.find("imported") != std::string::npos) ||
(importResult.find("unchanged") != std::string::npos) ||
(importResult.find("processed: 1") != std::string::npos) ||
(importResult.find("public key") != std::string::npos);
if (!importSuccess) {
LOG_ERROR << "Failed to import public key. Import output: " << importResult;
// Try to get more information about what went wrong
std::string debugCmd = "GNUPGHOME=" + keyringDir + " gpg --list-keys 2>&1";
std::string debugResult = executeGpgCommand(debugCmd);
LOG_ERROR << "GPG keyring state: " << debugResult;
// Cleanup
std::string cleanupCmd = "rm -rf " + tmpDir;
system(cleanupCmd.c_str());
return false;
}
// Verify the signature
// Use --trust-model always to avoid trust issues
std::string verifyCmd = "GNUPGHOME=" + keyringDir +
" gpg --batch --yes --trust-model always --verify " +
sigFile + " " + messageFile + " 2>&1";
std::string verifyResult = executeGpgCommand(verifyCmd);
LOG_DEBUG << "GPG verify result: " << verifyResult;
// Check if verification succeeded (check both English and potential localized messages)
bool verified = (verifyResult.find("Good signature") != std::string::npos) ||
(verifyResult.find("gpg: Good signature") != std::string::npos) ||
(verifyResult.find("Signature made") != std::string::npos &&
verifyResult.find("BAD signature") == std::string::npos);
if (!verified) {
LOG_WARN << "Signature verification failed. Verify output: " << verifyResult;
} else {
LOG_INFO << "Signature verification successful for challenge";
}
// Cleanup temporary files
std::string cleanupCmd = "rm -rf " + tmpDir;
system(cleanupCmd.c_str());
return verified;
} catch (const std::exception& e) {
LOG_ERROR << "Exception during signature verification: " << e.what();
return false;
}
}
void AuthService::generateUniqueColor(std::function<void(const std::string& color)> callback) {
try {
auto dbClient = app().getDbClient();
@ -421,6 +577,7 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string&
[username, signature, challenge, callback, this](const std::string& storedChallenge) {
try {
if (storedChallenge.empty() || storedChallenge != challenge) {
LOG_WARN << "Challenge mismatch for user: " << username;
callback(false, "", UserInfo{});
return;
}
@ -428,9 +585,7 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string&
// Delete challenge after use
RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {});
// In a real implementation, you would verify the signature here
// For now, we'll trust the client-side verification
// Get user's public key and verify signature
auto dbClient = app().getDbClient();
if (!dbClient) {
LOG_ERROR << "Database client is null";
@ -438,16 +593,32 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string&
return;
}
*dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color "
"FROM users WHERE username = $1 LIMIT 1"
*dbClient << "SELECT pk.public_key, u.id, u.username, u.is_admin, u.is_streamer, "
"u.is_pgp_only, u.bio, u.avatar_url, u.pgp_only_enabled_at, u.user_color "
"FROM pgp_keys pk JOIN users u ON pk.user_id = u.id "
"WHERE u.username = $1 ORDER BY pk.created_at DESC LIMIT 1"
<< username
>> [callback, this](const Result& r) {
>> [callback, signature, challenge, this](const Result& r) {
try {
if (r.empty()) {
LOG_WARN << "No PGP key found for user";
callback(false, "", UserInfo{});
return;
}
std::string publicKey = r[0]["public_key"].as<std::string>();
// CRITICAL: Server-side signature verification
bool signatureValid = verifyPgpSignature(challenge, signature, publicKey);
if (!signatureValid) {
LOG_WARN << "Invalid PGP signature for user";
callback(false, "Invalid signature", UserInfo{});
return;
}
LOG_INFO << "PGP signature verified successfully for user";
UserInfo user;
user.id = r[0]["id"].as<int64_t>();
user.username = r[0]["username"].as<std::string>();