#include "AuthService.h" #include "DatabaseService.h" #include "RedisHelper.h" #include #include #include #include #include #include #include #include #include #include using namespace drogon; using namespace drogon::orm; bool AuthService::validatePassword(const std::string& password, std::string& error) { if (password.length() < 8) { error = "Password must be at least 8 characters long"; return false; } if (!std::regex_search(password, std::regex("[0-9]"))) { error = "Password must contain at least one number"; return false; } if (!std::regex_search(password, std::regex("[!@#$%^&*(),.?\":{}|<>]"))) { error = "Password must contain at least one symbol"; return false; } return true; } // GPGME-based PGP signature verification (no shell commands for security) namespace { // RAII wrapper for gpgme_data_t (named to avoid conflict with deprecated GpgmeData typedef) class GpgmeDataWrapper { public: GpgmeDataWrapper() : data_(nullptr) {} ~GpgmeDataWrapper() { if (data_) gpgme_data_release(data_); } gpgme_data_t* ptr() { return &data_; } gpgme_data_t get() { return data_; } bool valid() const { return data_ != nullptr; } private: gpgme_data_t data_; }; // RAII wrapper for gpgme_ctx_t class GpgmeContextWrapper { public: GpgmeContextWrapper() : ctx_(nullptr) {} ~GpgmeContextWrapper() { if (ctx_) gpgme_release(ctx_); } gpgme_ctx_t* ptr() { return &ctx_; } gpgme_ctx_t get() { return ctx_; } bool valid() const { return ctx_ != nullptr; } private: gpgme_ctx_t ctx_; }; } bool verifyPgpSignature(const std::string& message, const std::string& signature, const std::string& publicKey) { try { // Initialize GPGME gpgme_check_version(nullptr); // Create temporary directory for isolated keyring using filesystem std::string tmpDir = "/tmp/pgp_verify_" + drogon::utils::genRandomString(16); std::filesystem::create_directories(tmpDir); std::filesystem::permissions(tmpDir, std::filesystem::perms::owner_all); // Set GNUPGHOME environment for this context std::string gnupgHome = tmpDir; // Create GPGME context GpgmeContextWrapper ctx; gpgme_error_t err = gpgme_new(ctx.ptr()); if (err) { LOG_ERROR << "Failed to create GPGME context: " << gpgme_strerror(err); std::filesystem::remove_all(tmpDir); return false; } // Set the engine info to use our temporary directory err = gpgme_ctx_set_engine_info(ctx.get(), GPGME_PROTOCOL_OpenPGP, nullptr, gnupgHome.c_str()); if (err) { LOG_ERROR << "Failed to set GPGME engine info: " << gpgme_strerror(err); std::filesystem::remove_all(tmpDir); return false; } // Set protocol gpgme_set_protocol(ctx.get(), GPGME_PROTOCOL_OpenPGP); // Import the public key GpgmeDataWrapper keyData; err = gpgme_data_new_from_mem(keyData.ptr(), publicKey.c_str(), publicKey.size(), 1); if (err) { LOG_ERROR << "Failed to create key data: " << gpgme_strerror(err); std::filesystem::remove_all(tmpDir); return false; } err = gpgme_op_import(ctx.get(), keyData.get()); if (err) { LOG_ERROR << "Failed to import public key: " << gpgme_strerror(err); std::filesystem::remove_all(tmpDir); return false; } gpgme_import_result_t importResult = gpgme_op_import_result(ctx.get()); if (!importResult || (importResult->imported == 0 && importResult->unchanged == 0)) { LOG_ERROR << "No keys were imported"; std::filesystem::remove_all(tmpDir); return false; } LOG_DEBUG << "GPGME imported " << importResult->imported << " keys, " << importResult->unchanged << " unchanged"; // Create data objects for signature and message GpgmeDataWrapper sigData; err = gpgme_data_new_from_mem(sigData.ptr(), signature.c_str(), signature.size(), 1); if (err) { LOG_ERROR << "Failed to create signature data: " << gpgme_strerror(err); std::filesystem::remove_all(tmpDir); return false; } GpgmeDataWrapper msgData; err = gpgme_data_new_from_mem(msgData.ptr(), message.c_str(), message.size(), 1); if (err) { LOG_ERROR << "Failed to create message data: " << gpgme_strerror(err); std::filesystem::remove_all(tmpDir); return false; } // Verify the signature err = gpgme_op_verify(ctx.get(), sigData.get(), msgData.get(), nullptr); if (err) { LOG_WARN << "Signature verification failed: " << gpgme_strerror(err); std::filesystem::remove_all(tmpDir); return false; } // Check verification result gpgme_verify_result_t verifyResult = gpgme_op_verify_result(ctx.get()); if (!verifyResult || !verifyResult->signatures) { LOG_WARN << "No signatures found in verification result"; std::filesystem::remove_all(tmpDir); return false; } // Check if signature is valid gpgme_signature_t sig = verifyResult->signatures; bool verified = (sig->status == GPG_ERR_NO_ERROR); if (verified) { LOG_INFO << "Signature verification successful for challenge"; } else { LOG_WARN << "Signature verification failed: " << gpgme_strerror(sig->status); } // Cleanup temporary directory std::filesystem::remove_all(tmpDir); return verified; } catch (const std::exception& e) { LOG_ERROR << "Exception during signature verification: " << e.what(); return false; } } void AuthService::generateUniqueColor(std::function callback) { try { auto dbClient = app().getDbClient(); // Create a structure to hold the state for recursive attempts struct ColorGenerator : public std::enable_shared_from_this { std::mt19937 gen; std::uniform_int_distribution<> dis; std::function callback; DbClientPtr dbClient; int attempts; ColorGenerator(std::function cb, DbClientPtr db) : gen(std::random_device{}()), dis(0, 0xFFFFFF), callback(cb), dbClient(db), attempts(0) {} void tryGenerate() { auto self = shared_from_this(); // Limit attempts to prevent infinite recursion if (++attempts > 100) { LOG_ERROR << "Failed to generate unique color after 100 attempts"; callback("#561D5E"); // Fallback to default return; } // Generate random color int colorValue = dis(gen); char colorHex[8]; snprintf(colorHex, sizeof(colorHex), "#%06X", colorValue); std::string color(colorHex); // Check if color exists *dbClient << "SELECT id FROM users WHERE user_color = $1 LIMIT 1" << color >> [self, color](const Result& r) { if (r.empty()) { // Color is unique, use it self->callback(color); } else { // Color exists, try again self->tryGenerate(); } } >> [self](const DrogonDbException& e) { LOG_ERROR << "Database error checking color: " << e.base().what(); // Fallback to a default color self->callback("#561D5E"); }; } }; auto generator = std::make_shared(callback, dbClient); generator->tryGenerate(); } catch (const std::exception& e) { LOG_ERROR << "Exception in generateUniqueColor: " << e.what(); callback("#561D5E"); // Fallback to default } } void AuthService::updateUserColor(int64_t userId, const std::string& newColor, std::function callback) { try { // Validate color format if (newColor.length() != 7 || newColor[0] != '#') { callback(false, "Invalid color format. Use #RRGGBB", ""); return; } // Check if color is valid hex for (size_t i = 1; i < 7; i++) { if (!std::isxdigit(newColor[i])) { callback(false, "Invalid color format. Use #RRGGBB", ""); return; } } auto dbClient = app().getDbClient(); // Check if color is already taken *dbClient << "SELECT id FROM users WHERE user_color = $1 AND id != $2 LIMIT 1" << newColor << userId >> [dbClient, userId, newColor, callback](const Result& r) { if (!r.empty()) { callback(false, "This color is already taken", ""); return; } // Update the color *dbClient << "UPDATE users SET user_color = $1 WHERE id = $2 RETURNING user_color" << newColor << userId >> [callback, newColor](const Result& r2) { if (!r2.empty()) { callback(true, "", newColor); } else { callback(false, "Failed to update color", ""); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update color: " << e.base().what(); callback(false, "Database error", ""); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error", ""); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in updateUserColor: " << e.what(); callback(false, "Internal server error", ""); } } void AuthService::registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, std::function callback) { try { LOG_DEBUG << "Starting user registration for: " << username; // Validate username if (username.length() < 3 || username.length() > 30) { callback(false, "Username must be between 3 and 30 characters", 0); return; } if (!std::regex_match(username, std::regex("^[a-zA-Z][a-zA-Z0-9_]*$"))) { callback(false, "Username must start with a letter and contain only letters, numbers, and underscores", 0); return; } // Validate password std::string error; if (!validatePassword(password, error)) { callback(false, error, 0); return; } LOG_DEBUG << "Validation passed, generating unique color"; // Generate unique color first generateUniqueColor([this, username, password, publicKey, fingerprint, callback](const std::string& color) { try { LOG_DEBUG << "Got unique color: " << color << ", checking username availability"; auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "Database connection error", 0); return; } // Check if username exists *dbClient << "SELECT id FROM users WHERE username = $1 LIMIT 1" << username >> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r) { try { if (!r.empty()) { LOG_WARN << "Username already exists: " << username; callback(false, "Username already exists", 0); return; } LOG_DEBUG << "Username available, checking fingerprint"; // Check if fingerprint exists *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1 LIMIT 1" << fingerprint >> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r2) { try { if (!r2.empty()) { LOG_WARN << "Fingerprint already exists"; callback(false, "This PGP key is already registered", 0); return; } LOG_DEBUG << "Fingerprint available, hashing password"; // Hash password std::string hash; try { hash = BCrypt::generateHash(password); } catch (const std::exception& e) { LOG_ERROR << "Failed to hash password: " << e.what(); callback(false, "Failed to process password", 0); return; } LOG_DEBUG << "Password hashed, inserting user"; // Insert user first (without transaction for simplicity) *dbClient << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only, user_color) " "VALUES ($1, $2, false, false, false, $3) RETURNING id" << username << hash << color >> [dbClient, publicKey, fingerprint, callback, username](const Result& r3) { try { if (r3.empty()) { LOG_ERROR << "Failed to insert user"; callback(false, "Failed to create user", 0); return; } int64_t userId = r3[0]["id"].as(); LOG_INFO << "User created with ID: " << userId; // Insert PGP key *dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" << userId << publicKey << fingerprint >> [callback, userId, username](const Result&) { LOG_INFO << "User registration complete for: " << username << " (ID: " << userId << ")"; callback(true, "", userId); } >> [dbClient, userId, callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert PGP key: " << e.base().what(); // Try to clean up the user *dbClient << "DELETE FROM users WHERE id = $1" << userId >> [](const Result&) {} >> [](const DrogonDbException&) {}; callback(false, "Failed to save PGP key", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception processing user insert result: " << e.what(); callback(false, "Registration failed", 0); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert user: " << e.base().what(); callback(false, "Registration failed", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in fingerprint check callback: " << e.what(); callback(false, "Registration failed", 0); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error checking fingerprint: " << e.base().what(); callback(false, "Database error", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in username check callback: " << e.what(); callback(false, "Registration failed", 0); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error checking username: " << e.base().what(); callback(false, "Database error", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in color generation callback: " << e.what(); callback(false, "Registration failed", 0); } }); } catch (const std::exception& e) { LOG_ERROR << "Exception in registerUser: " << e.what(); callback(false, "Registration failed", 0); } } void AuthService::loginUser(const std::string& username, const std::string& password, std::function callback) { try { auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "", UserInfo{}); return; } *dbClient << "SELECT id, username, password_hash, is_admin, is_moderator, is_streamer, is_restreamer, is_bot, is_texter, is_pgp_only, is_disabled, bio, avatar_url, banner_url, banner_position, banner_zoom, banner_position_x, graffiti_url, pgp_only_enabled_at, user_color " "FROM users WHERE username = $1 LIMIT 1" << username >> [password, callback, this](const Result& r) { try { if (r.empty()) { callback(false, "", UserInfo{}); return; } // Check if account is disabled bool isDisabled = r[0]["is_disabled"].isNull() ? false : r[0]["is_disabled"].as(); if (isDisabled) { callback(false, "Account disabled", UserInfo{}); return; } // Check if PGP-only is enabled BEFORE password validation bool isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); if (isPgpOnly) { // Return a specific error for PGP-only accounts callback(false, "PGP-only login enabled for this account", UserInfo{}); return; } std::string hash = r[0]["password_hash"].as(); bool valid = false; try { valid = BCrypt::validatePassword(password, hash); } catch (const std::exception& e) { LOG_ERROR << "Failed to validate password: " << e.what(); callback(false, "", UserInfo{}); return; } if (!valid) { callback(false, "", UserInfo{}); return; } UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isModerator = r[0]["is_moderator"].isNull() ? false : r[0]["is_moderator"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isRestreamer = r[0]["is_restreamer"].isNull() ? false : r[0]["is_restreamer"].as(); user.isBot = r[0]["is_bot"].isNull() ? false : r[0]["is_bot"].as(); user.isTexter = r[0]["is_texter"].isNull() ? false : r[0]["is_texter"].as(); user.isPgpOnly = isPgpOnly; user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.bannerUrl = r[0]["banner_url"].isNull() ? "" : r[0]["banner_url"].as(); user.bannerPosition = r[0]["banner_position"].isNull() ? 50 : r[0]["banner_position"].as(); user.bannerZoom = r[0]["banner_zoom"].isNull() ? 100 : r[0]["banner_zoom"].as(); user.bannerPositionX = r[0]["banner_position_x"].isNull() ? 50 : r[0]["banner_position_x"].as(); user.graffitiUrl = r[0]["graffiti_url"].isNull() ? "" : r[0]["graffiti_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); std::string token = generateToken(user); callback(true, token, user); } catch (const std::exception& e) { LOG_ERROR << "Exception in login callback: " << e.what(); callback(false, "", UserInfo{}); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in loginUser: " << e.what(); callback(false, "", UserInfo{}); } } void AuthService::initiatePgpLogin(const std::string& username, std::function callback) { try { auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "", ""); return; } // Generate random challenge auto bytes = drogon::utils::genRandomString(32); std::string challenge = drogon::utils::base64Encode( reinterpret_cast(bytes.data()), bytes.length() ); // Store challenge in Redis with 5 minute TTL RedisHelper::storeKeyAsync("pgp_challenge:" + username, challenge, 300, [dbClient, username, challenge, callback](bool stored) { if (!stored) { callback(false, "", ""); return; } // Get user's latest public key *dbClient << "SELECT pk.public_key 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, challenge](const Result& r) { if (r.empty()) { callback(false, "", ""); return; } std::string publicKey = r[0]["public_key"].as(); callback(true, challenge, publicKey); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", ""); }; } ); } catch (const std::exception& e) { LOG_ERROR << "Exception in initiatePgpLogin: " << e.what(); callback(false, "", ""); } } void AuthService::verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge, std::function callback) { try { // Get stored challenge from Redis RedisHelper::getKeyAsync("pgp_challenge:" + username, [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; } // Delete challenge after use RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {}); // Get user's public key and verify signature auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "", UserInfo{}); return; } *dbClient << "SELECT pk.public_key, u.id, u.username, u.is_admin, u.is_moderator, u.is_streamer, u.is_restreamer, u.is_bot, u.is_texter, " "u.is_pgp_only, u.is_disabled, u.bio, u.avatar_url, u.banner_url, u.banner_position, u.banner_zoom, u.banner_position_x, u.graffiti_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, signature, challenge, this](const Result& r) { try { if (r.empty()) { LOG_WARN << "No PGP key found for user"; callback(false, "", UserInfo{}); return; } // Check if account is disabled bool isDisabled = r[0]["is_disabled"].isNull() ? false : r[0]["is_disabled"].as(); if (isDisabled) { callback(false, "Account disabled", UserInfo{}); return; } std::string publicKey = r[0]["public_key"].as(); // 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(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isModerator = r[0]["is_moderator"].isNull() ? false : r[0]["is_moderator"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isRestreamer = r[0]["is_restreamer"].isNull() ? false : r[0]["is_restreamer"].as(); user.isBot = r[0]["is_bot"].isNull() ? false : r[0]["is_bot"].as(); user.isTexter = r[0]["is_texter"].isNull() ? false : r[0]["is_texter"].as(); user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.bannerUrl = r[0]["banner_url"].isNull() ? "" : r[0]["banner_url"].as(); user.bannerPosition = r[0]["banner_position"].isNull() ? 50 : r[0]["banner_position"].as(); user.bannerZoom = r[0]["banner_zoom"].isNull() ? 100 : r[0]["banner_zoom"].as(); user.bannerPositionX = r[0]["banner_position_x"].isNull() ? 50 : r[0]["banner_position_x"].as(); user.graffitiUrl = r[0]["graffiti_url"].isNull() ? "" : r[0]["graffiti_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); std::string token = generateToken(user); callback(true, token, user); } catch (const std::exception& e) { LOG_ERROR << "Exception processing user data: " << e.what(); callback(false, "", UserInfo{}); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in Redis callback: " << e.what(); callback(false, "", UserInfo{}); } } ); } catch (const std::exception& e) { LOG_ERROR << "Exception in verifyPgpLogin: " << e.what(); callback(false, "", UserInfo{}); } } // SECURITY FIX #5: Validate JWT secret has minimum length and entropy void AuthService::validateAndLoadJwtSecret() { if (!jwtSecret_.empty()) { return; // Already loaded and validated } const char* envSecret = std::getenv("JWT_SECRET"); if (!envSecret || strlen(envSecret) == 0) { throw std::runtime_error("JWT_SECRET environment variable is not set"); } size_t secretLen = strlen(envSecret); // Require at least 32 characters (256 bits) for HS256 if (secretLen < 32) { throw std::runtime_error("JWT_SECRET must be at least 32 characters (256 bits) for security"); } // Basic entropy check - ensure not all same character bool hasVariety = false; for (size_t i = 1; i < secretLen && !hasVariety; ++i) { if (envSecret[i] != envSecret[0]) { hasVariety = true; } } if (!hasVariety) { throw std::runtime_error("JWT_SECRET has insufficient entropy - all characters are the same"); } // Check for common weak secrets std::string secretLower = envSecret; std::transform(secretLower.begin(), secretLower.end(), secretLower.begin(), ::tolower); if (secretLower.find("secret") != std::string::npos || secretLower.find("password") != std::string::npos || secretLower.find("123456") != std::string::npos) { LOG_WARN << "JWT_SECRET appears to contain common weak patterns - consider using a stronger secret"; } jwtSecret_ = std::string(envSecret); LOG_INFO << "JWT secret loaded and validated (" << secretLen << " characters)"; } std::string AuthService::generateToken(const UserInfo& user) { try { validateAndLoadJwtSecret(); // SECURITY FIX: Reduced JWT expiry from 24h to 1h to limit token exposure window auto token = jwt::create() .set_issuer("streaming-app") .set_type("JWT") .set_issued_at(std::chrono::system_clock::now()) .set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(1)) .set_payload_claim("user_id", jwt::claim(std::to_string(user.id))) .set_payload_claim("username", jwt::claim(user.username)) .set_payload_claim("is_admin", jwt::claim(std::to_string(user.isAdmin))) .set_payload_claim("is_moderator", jwt::claim(std::to_string(user.isModerator))) .set_payload_claim("is_streamer", jwt::claim(std::to_string(user.isStreamer))) .set_payload_claim("is_restreamer", jwt::claim(std::to_string(user.isRestreamer))) .set_payload_claim("is_disabled", jwt::claim(std::to_string(user.isDisabled))) // SECURITY FIX #26 .set_payload_claim("token_version", jwt::claim(std::to_string(user.tokenVersion))) // SECURITY FIX #10 .set_payload_claim("color_code", jwt::claim( user.colorCode.empty() ? "#561D5E" : user.colorCode )) // Ensure color is never empty .set_payload_claim("avatar_url", jwt::claim(user.avatarUrl)) .sign(jwt::algorithm::hs256{jwtSecret_}); return token; } catch (const std::exception& e) { LOG_ERROR << "Failed to generate token: " << e.what(); return ""; } } bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) { try { validateAndLoadJwtSecret(); auto decoded = jwt::decode(token); auto verifier = jwt::verify() .allow_algorithm(jwt::algorithm::hs256{jwtSecret_}) .with_issuer("streaming-app"); verifier.verify(decoded); userInfo.id = std::stoll(decoded.get_payload_claim("user_id").as_string()); userInfo.username = decoded.get_payload_claim("username").as_string(); userInfo.isAdmin = decoded.get_payload_claim("is_admin").as_string() == "1"; userInfo.isModerator = decoded.has_payload_claim("is_moderator") ? decoded.get_payload_claim("is_moderator").as_string() == "1" : false; userInfo.isStreamer = decoded.has_payload_claim("is_streamer") ? decoded.get_payload_claim("is_streamer").as_string() == "1" : false; userInfo.isRestreamer = decoded.has_payload_claim("is_restreamer") ? decoded.get_payload_claim("is_restreamer").as_string() == "1" : false; // SECURITY FIX #26: Extract disabled status userInfo.isDisabled = decoded.has_payload_claim("is_disabled") ? decoded.get_payload_claim("is_disabled").as_string() == "1" : false; // SECURITY FIX #10: Extract token version for revocation check userInfo.tokenVersion = decoded.has_payload_claim("token_version") ? std::stoi(decoded.get_payload_claim("token_version").as_string()) : 1; // Get color from token if available, otherwise will need to fetch from DB if (decoded.has_payload_claim("color_code")) { userInfo.colorCode = decoded.get_payload_claim("color_code").as_string(); } else { // For older tokens without color, default value userInfo.colorCode = "#561D5E"; } // SECURITY FIX #26: Reject tokens from disabled accounts if (userInfo.isDisabled) { LOG_DEBUG << "Token rejected - user account is disabled: " << userInfo.username; return false; } return true; } catch (const std::exception& e) { LOG_DEBUG << "Token validation failed: " << e.what(); return false; } } // Chat service compatibility method std::optional AuthService::verifyToken(const std::string& token) { UserInfo userInfo; if (validateToken(token, userInfo)) { UserClaims claims; claims.userId = std::to_string(userInfo.id); claims.username = userInfo.username; claims.userColor = userInfo.colorCode; claims.isAdmin = userInfo.isAdmin; claims.isModerator = userInfo.isModerator; claims.isStreamer = userInfo.isStreamer; claims.isRestreamer = userInfo.isRestreamer; return claims; } return std::nullopt; } void AuthService::updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword, std::function callback) { try { // Validate new password std::string error; if (!validatePassword(newPassword, error)) { callback(false, error); return; } auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "Database connection error"); return; } // Verify old password *dbClient << "SELECT password_hash FROM users WHERE id = $1 LIMIT 1" << userId >> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) { try { if (r.empty()) { callback(false, "User not found"); return; } std::string hash = r[0]["password_hash"].as(); bool valid = false; try { valid = BCrypt::validatePassword(oldPassword, hash); } catch (const std::exception& e) { LOG_ERROR << "Failed to validate password: " << e.what(); callback(false, "Password validation error"); return; } if (!valid) { callback(false, "Incorrect password"); return; } // Update password std::string newHash; try { newHash = BCrypt::generateHash(newPassword); } catch (const std::exception& e) { LOG_ERROR << "Failed to hash new password: " << e.what(); callback(false, "Failed to process new password"); return; } // SECURITY FIX #10: Increment token_version to invalidate all existing tokens *dbClient << "UPDATE users SET password_hash = $1, token_version = COALESCE(token_version, 0) + 1 WHERE id = $2" << newHash << userId >> [callback, userId](const Result&) { LOG_INFO << "Password updated and token_version incremented for user " << userId; callback(true, ""); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update password: " << e.base().what(); callback(false, "Failed to update password"); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in password update callback: " << e.what(); callback(false, "Failed to update password"); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error"); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in updatePassword: " << e.what(); callback(false, "Failed to update password"); } } void AuthService::fetchUserInfo(int64_t userId, std::function callback) { try { auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, UserInfo{}); return; } *dbClient << "SELECT id, username, is_admin, is_moderator, is_streamer, is_restreamer, is_bot, is_texter, is_pgp_only, bio, avatar_url, banner_url, banner_position, banner_zoom, banner_position_x, graffiti_url, pgp_only_enabled_at, user_color " "FROM users WHERE id = $1 LIMIT 1" << userId >> [callback](const Result& r) { try { if (r.empty()) { callback(false, UserInfo{}); return; } UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isModerator = r[0]["is_moderator"].isNull() ? false : r[0]["is_moderator"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isRestreamer = r[0]["is_restreamer"].isNull() ? false : r[0]["is_restreamer"].as(); user.isBot = r[0]["is_bot"].isNull() ? false : r[0]["is_bot"].as(); user.isTexter = r[0]["is_texter"].isNull() ? false : r[0]["is_texter"].as(); user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.bannerUrl = r[0]["banner_url"].isNull() ? "" : r[0]["banner_url"].as(); user.bannerPosition = r[0]["banner_position"].isNull() ? 50 : r[0]["banner_position"].as(); user.bannerZoom = r[0]["banner_zoom"].isNull() ? 100 : r[0]["banner_zoom"].as(); user.bannerPositionX = r[0]["banner_position_x"].isNull() ? 50 : r[0]["banner_position_x"].as(); user.graffitiUrl = r[0]["graffiti_url"].isNull() ? "" : r[0]["graffiti_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); callback(true, user); } catch (const std::exception& e) { LOG_ERROR << "Exception processing user data: " << e.what(); callback(false, UserInfo{}); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, UserInfo{}); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in fetchUserInfo: " << e.what(); callback(false, UserInfo{}); } }