#include "smartbotic/webserver/auth_service.hpp" #include #include #include #include #include namespace smartbotic::webserver { namespace { auto GenerateUuid() -> std::string { static std::random_device rd; static std::mt19937 gen(rd()); static std::uniform_int_distribution<> dis(0, 15); static const char* hex_chars = "0123456789abcdef"; std::string uuid; uuid.reserve(36); for (int i = 0; i < 36; ++i) { if (i == 8 || i == 13 || i == 18 || i == 23) { uuid += '-'; } else { uuid += hex_chars[dis(gen)]; } } return uuid; } } // namespace AuthService::AuthService(const JwtConfig& config) : config_(config) { // Initialize libsodium if not already done if (sodium_init() < 0) { spdlog::warn("libsodium initialization failed or already initialized"); } } AuthService::~AuthService() = default; AuthService::AuthService(AuthService&&) noexcept = default; auto AuthService::operator=(AuthService&&) noexcept -> AuthService& = default; auto AuthService::GenerateTokens(const std::string& user_id, const std::string& email, const std::vector& workspace_ids, const std::vector& groups) -> TokenPair { using traits = jwt::traits::nlohmann_json; auto now = std::chrono::system_clock::now(); auto access_exp = now + std::chrono::seconds(config_.access_token_expiry); auto refresh_exp = now + std::chrono::seconds(config_.refresh_token_expiry); std::string access_jti = GenerateJti(); std::string refresh_jti = GenerateJti(); // Build workspace_ids and groups arrays for JWT nlohmann::json ws_json = workspace_ids; nlohmann::json groups_json = groups; // Create access token auto access_token = jwt::create() .set_issuer(config_.issuer) .set_subject(user_id) .set_type("JWT") .set_id(access_jti) .set_issued_at(now) .set_expires_at(access_exp) .set_payload_claim("email", jwt::basic_claim(email)) .set_payload_claim("workspace_ids", jwt::basic_claim(ws_json)) .set_payload_claim("groups", jwt::basic_claim(groups_json)) .set_payload_claim("token_type", jwt::basic_claim(std::string("access"))) .sign(jwt::algorithm::hs256{config_.secret}); // Create refresh token (contains user info for renewal) auto refresh_token = jwt::create() .set_issuer(config_.issuer) .set_subject(user_id) .set_type("JWT") .set_id(refresh_jti) .set_issued_at(now) .set_expires_at(refresh_exp) .set_payload_claim("email", jwt::basic_claim(email)) .set_payload_claim("workspace_ids", jwt::basic_claim(ws_json)) .set_payload_claim("groups", jwt::basic_claim(groups_json)) .set_payload_claim("token_type", jwt::basic_claim(std::string("refresh"))) .sign(jwt::algorithm::hs256{config_.secret}); spdlog::debug("Generated token pair for user {} (access_jti: {}, refresh_jti: {})", user_id, access_jti, refresh_jti); return TokenPair{ .access_token = access_token, .refresh_token = refresh_token, .access_expires_in = config_.access_token_expiry, .refresh_expires_in = config_.refresh_token_expiry}; } auto AuthService::ValidateAccessToken(const std::string& token) -> TokenValidation { using traits = jwt::traits::nlohmann_json; try { auto decoded = jwt::decode(token); // Create verifier auto verifier = jwt::verify() .allow_algorithm(jwt::algorithm::hs256{config_.secret}) .with_issuer(config_.issuer); // Verify token verifier.verify(decoded); // Check token type auto token_type = decoded.get_payload_claim("token_type").as_string(); if (token_type != "access") { return TokenValidation{.valid = false, .error = "Not an access token", .user = std::nullopt}; } // Check if token is invalidated auto jti = decoded.get_id(); if (IsTokenInvalidated(jti)) { return TokenValidation{.valid = false, .error = "Token has been invalidated", .user = std::nullopt}; } // Extract user info AuthUser user; user.user_id = decoded.get_subject(); user.email = decoded.get_payload_claim("email").as_string(); auto ws_claim = decoded.get_payload_claim("workspace_ids").to_json(); if (ws_claim.is_array()) { for (const auto& ws : ws_claim) { user.workspace_ids.push_back(ws.get()); } } auto groups_claim = decoded.get_payload_claim("groups").to_json(); if (groups_claim.is_array()) { for (const auto& g : groups_claim) { user.groups.push_back(g.get()); } } return TokenValidation{.valid = true, .error = "", .user = user}; } catch (const jwt::error::token_verification_exception& e) { spdlog::debug("Token verification failed: {}", e.what()); return TokenValidation{.valid = false, .error = e.what(), .user = std::nullopt}; } catch (const std::exception& e) { spdlog::debug("Token parsing failed: {}", e.what()); return TokenValidation{.valid = false, .error = "Invalid token format", .user = std::nullopt}; } } auto AuthService::ValidateRefreshToken(const std::string& token) -> TokenValidation { using traits = jwt::traits::nlohmann_json; try { auto decoded = jwt::decode(token); // Create verifier auto verifier = jwt::verify() .allow_algorithm(jwt::algorithm::hs256{config_.secret}) .with_issuer(config_.issuer); // Verify token verifier.verify(decoded); // Check token type auto token_type = decoded.get_payload_claim("token_type").as_string(); if (token_type != "refresh") { return TokenValidation{.valid = false, .error = "Not a refresh token", .user = std::nullopt}; } // Check if token is invalidated auto jti = decoded.get_id(); if (IsTokenInvalidated(jti)) { return TokenValidation{.valid = false, .error = "Token has been invalidated", .user = std::nullopt}; } // Extract user info AuthUser user; user.user_id = decoded.get_subject(); user.email = decoded.get_payload_claim("email").as_string(); auto ws_claim = decoded.get_payload_claim("workspace_ids").to_json(); if (ws_claim.is_array()) { for (const auto& ws : ws_claim) { user.workspace_ids.push_back(ws.get()); } } auto groups_claim = decoded.get_payload_claim("groups").to_json(); if (groups_claim.is_array()) { for (const auto& g : groups_claim) { user.groups.push_back(g.get()); } } return TokenValidation{.valid = true, .error = "", .user = user}; } catch (const jwt::error::token_verification_exception& e) { spdlog::debug("Refresh token verification failed: {}", e.what()); return TokenValidation{.valid = false, .error = e.what(), .user = std::nullopt}; } catch (const std::exception& e) { spdlog::debug("Refresh token parsing failed: {}", e.what()); return TokenValidation{.valid = false, .error = "Invalid token format", .user = std::nullopt}; } } auto AuthService::RefreshAccessToken(const std::string& refresh_token) -> std::optional { using traits = jwt::traits::nlohmann_json; auto validation = ValidateRefreshToken(refresh_token); if (!validation.valid || !validation.user) { spdlog::debug("Refresh token validation failed: {}", validation.error); return std::nullopt; } const auto& user = *validation.user; // Invalidate the old refresh token (token rotation) try { auto decoded = jwt::decode(refresh_token); invalidated_tokens_.insert(decoded.get_id()); } catch (...) { // Ignore decoding errors here since we already validated } // Generate new token pair return GenerateTokens(user.user_id, user.email, user.workspace_ids, user.groups); } auto AuthService::InvalidateRefreshToken(const std::string& refresh_token) -> bool { using traits = jwt::traits::nlohmann_json; try { auto decoded = jwt::decode(refresh_token); auto jti = decoded.get_id(); invalidated_tokens_.insert(jti); spdlog::debug("Invalidated refresh token: {}", jti); return true; } catch (const std::exception& e) { spdlog::debug("Failed to invalidate token: {}", e.what()); return false; } } auto AuthService::IsTokenInvalidated(const std::string& jti) const -> bool { return invalidated_tokens_.contains(jti); } auto AuthService::ExtractBearerToken(const std::string& auth_header) -> std::optional { const std::string prefix = "Bearer "; if (auth_header.size() <= prefix.size()) { return std::nullopt; } if (auth_header.substr(0, prefix.size()) != prefix) { return std::nullopt; } return auth_header.substr(prefix.size()); } auto AuthService::HashPassword(const std::string& password) -> std::string { // Ensure libsodium is initialized if (sodium_init() < 0) { throw std::runtime_error("Failed to initialize libsodium"); } char hash[crypto_pwhash_STRBYTES]; if (crypto_pwhash_str(hash, password.c_str(), password.length(), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) { throw std::runtime_error("Password hashing failed (out of memory)"); } return std::string(hash); } auto AuthService::VerifyPassword(const std::string& password, const std::string& hash) -> bool { // Ensure libsodium is initialized if (sodium_init() < 0) { return false; } return crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.length()) == 0; } auto AuthService::GenerateJti() -> std::string { return GenerateUuid(); } } // namespace smartbotic::webserver