This commit is contained in:
Aaron Kimbrell
2026-03-26 09:56:29 -05:00
parent 8372202d8f
commit f658da19a3
27 changed files with 1669 additions and 685 deletions

View File

@@ -1,132 +1,6 @@
#include "AuthMiddleware.h"
#include "DashboardAuthService.h"
#include "Game.h"
#include "Logger.h"
#include <string>
#include <cctype>
// Helper to extract cookie value from header
static std::string ExtractCookieValue(const std::string& cookieHeader, const std::string& cookieName) {
std::string searchStr = cookieName + "=";
size_t pos = cookieHeader.find(searchStr);
if (pos == std::string::npos) {
return "";
}
size_t valueStart = pos + searchStr.length();
size_t valueEnd = cookieHeader.find(";", valueStart);
if (valueEnd == std::string::npos) {
valueEnd = cookieHeader.length();
}
std::string value = cookieHeader.substr(valueStart, valueEnd - valueStart);
// URL decode the value
std::string decoded;
for (size_t i = 0; i < value.length(); ++i) {
if (value[i] == '%' && i + 2 < value.length()) {
std::string hex = value.substr(i + 1, 2);
char* endptr;
int charCode = static_cast<int>(std::strtol(hex.c_str(), &endptr, 16));
if (endptr - hex.c_str() == 2) {
decoded += static_cast<char>(charCode);
i += 2;
continue;
}
}
decoded += value[i];
}
return decoded;
}
std::string AuthMiddleware::ExtractTokenFromQueryString(const std::string& queryString) {
if (queryString.empty()) {
return "";
}
// Parse query string to find token parameter
// Expected format: "?token=eyJhbGc..."
std::string tokenPrefix = "token=";
size_t tokenPos = queryString.find(tokenPrefix);
if (tokenPos == std::string::npos) {
return "";
}
// Extract token value (from "token=" to next "&" or end of string)
size_t valueStart = tokenPos + tokenPrefix.length();
size_t valueEnd = queryString.find("&", valueStart);
if (valueEnd == std::string::npos) {
valueEnd = queryString.length();
}
return queryString.substr(valueStart, valueEnd - valueStart);
}
std::string AuthMiddleware::ExtractTokenFromCookies(const std::string& cookieHeader) {
if (cookieHeader.empty()) {
return "";
}
// Extract dashboardToken cookie value
return ExtractCookieValue(cookieHeader, "dashboardToken");
}
std::string AuthMiddleware::ExtractTokenFromAuthHeader(const std::string& authHeader) {
if (authHeader.empty()) {
return "";
}
// Check for "Bearer <token>" format
if (authHeader.substr(0, 7) == "Bearer ") {
return authHeader.substr(7);
}
// Check for "Token <token>" format
if (authHeader.substr(0, 6) == "Token ") {
return authHeader.substr(6);
}
// If no prefix, assume raw token
return authHeader;
}
#include "AuthTokenHandler.h"
bool AuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
// Try to extract token from various sources (in priority order)
std::string token = ExtractTokenFromQueryString(context.queryString);
if (token.empty()) {
const std::string& cookieHeader = context.GetHeader("Cookie");
token = ExtractTokenFromCookies(cookieHeader);
}
if (token.empty()) {
const std::string& authHeader = context.GetHeader("Authorization");
token = ExtractTokenFromAuthHeader(authHeader);
}
// If we found a token, try to verify it
if (!token.empty()) {
std::string username;
uint8_t gmLevel{};
if (DashboardAuthService::VerifyToken(token, username, gmLevel)) {
context.isAuthenticated = true;
context.authenticatedUser = username;
context.gmLevel = gmLevel;
LOG_DEBUG("User %s authenticated via API token (GM level %d)", username.c_str(), gmLevel);
return true;
} else {
LOG_DEBUG("Invalid authentication token provided");
return true; // Continue - let routes decide if auth is required
}
}
// No token found - continue without authentication
// Routes can use RequireAuthMiddleware to enforce authentication
return true;
return AuthTokenHandler::ProcessHTTPContext(context, reply);
}

View File

@@ -23,12 +23,6 @@ public:
bool Process(HTTPContext& context, HTTPReply& reply) override;
std::string GetName() const override { return "AuthMiddleware"; }
private:
// Extract token from various sources
static std::string ExtractTokenFromQueryString(const std::string& queryString);
static std::string ExtractTokenFromCookies(const std::string& cookieHeader);
static std::string ExtractTokenFromAuthHeader(const std::string& authHeader);
};
#endif // !__AUTHMIDDLEWARE_H__

View File

@@ -0,0 +1,186 @@
#include "AuthTokenHandler.h"
#include "DashboardAuthService.h"
#include "Game.h"
#include "Logger.h"
#include "HTTPContext.h"
#include "Web.h"
// Helper to extract cookie value from header
static std::string ExtractCookieValue(const std::string& cookieHeader, const std::string& cookieName) {
std::string searchStr = cookieName + "=";
size_t pos = cookieHeader.find(searchStr);
if (pos == std::string::npos) {
return "";
}
size_t valueStart = pos + searchStr.length();
size_t valueEnd = cookieHeader.find(";", valueStart);
if (valueEnd == std::string::npos) {
valueEnd = cookieHeader.length();
}
std::string value = cookieHeader.substr(valueStart, valueEnd - valueStart);
// URL decode the value
std::string decoded;
for (size_t i = 0; i < value.length(); ++i) {
if (value[i] == '%' && i + 2 < value.length()) {
std::string hex = value.substr(i + 1, 2);
char* endptr;
int charCode = static_cast<int>(std::strtol(hex.c_str(), &endptr, 16));
if (endptr - hex.c_str() == 2) {
decoded += static_cast<char>(charCode);
i += 2;
continue;
}
}
decoded += value[i];
}
return decoded;
}
std::string AuthTokenHandler::ExtractTokenFromQueryString(const std::string& queryString) {
if (queryString.empty()) {
return "";
}
// Parse query string to find token parameter
// Expected format: "?token=eyJhbGc..."
std::string tokenPrefix = "token=";
size_t tokenPos = queryString.find(tokenPrefix);
if (tokenPos == std::string::npos) {
return "";
}
// Extract token value (from "token=" to next "&" or end of string)
size_t valueStart = tokenPos + tokenPrefix.length();
size_t valueEnd = queryString.find("&", valueStart);
if (valueEnd == std::string::npos) {
valueEnd = queryString.length();
}
return queryString.substr(valueStart, valueEnd - valueStart);
}
std::string AuthTokenHandler::ExtractTokenFromCookieHeader(const std::string& cookieHeader) {
if (cookieHeader.empty()) {
return "";
}
// Extract dashboardToken cookie value
return ExtractCookieValue(cookieHeader, "dashboardToken");
}
std::string AuthTokenHandler::ExtractTokenFromAuthHeader(const std::string& authHeader) {
if (authHeader.empty()) {
return "";
}
// Check for "Bearer <token>" format
if (authHeader.length() >= 7 && authHeader.substr(0, 7) == "Bearer ") {
return authHeader.substr(7);
}
// Check for "Token <token>" format
if (authHeader.length() >= 6 && authHeader.substr(0, 6) == "Token ") {
return authHeader.substr(6);
}
// If no prefix, assume raw token
return authHeader;
}
std::string AuthTokenHandler::ExtractToken(
const std::string& queryString,
const std::string& cookieHeader,
const std::string& authHeader
) {
// Try in priority order: query string, cookie, auth header
std::string token = ExtractTokenFromQueryString(queryString);
if (!token.empty()) {
return token;
}
token = ExtractTokenFromCookieHeader(cookieHeader);
if (!token.empty()) {
return token;
}
token = ExtractTokenFromAuthHeader(authHeader);
return token;
}
AuthTokenHandler::TokenValidationResult AuthTokenHandler::ValidateToken(const std::string& token) {
TokenValidationResult result;
if (token.empty()) {
result.isValid = false;
result.errorMessage = "No token provided";
return result;
}
// Verify JWT token
std::string username;
uint8_t gmLevel = 0;
if (!DashboardAuthService::VerifyToken(token, username, gmLevel)) {
result.isValid = false;
result.errorMessage = "Invalid or expired token";
LOG_DEBUG("Token validation failed");
return result;
}
result.isValid = true;
result.username = username;
result.gmLevel = gmLevel;
LOG_DEBUG("Token validated successfully for user: %s (GM Level: %d)", username.c_str(), gmLevel);
return result;
}
AuthTokenHandler::TokenValidationResult AuthTokenHandler::ExtractAndValidateToken(
const std::string& queryString,
const std::string& cookieHeader,
const std::string& authHeader
) {
TokenValidationResult result;
// Extract token from any source
std::string token = ExtractToken(queryString, cookieHeader, authHeader);
if (token.empty()) {
result.isValid = false;
result.errorMessage = "No authentication token found";
return result;
}
// Validate the token
return ValidateToken(token);
}
bool AuthTokenHandler::ProcessHTTPContext(HTTPContext& context, HTTPReply& reply) {
// Extract and validate token from all available sources
const std::string& queryString = context.queryString;
const std::string& cookieHeader = context.GetHeader("Cookie");
const std::string& authHeader = context.GetHeader("Authorization");
auto result = ExtractAndValidateToken(queryString, cookieHeader, authHeader);
if (result.isValid) {
context.isAuthenticated = true;
context.authenticatedUser = result.username;
context.gmLevel = result.gmLevel;
LOG_DEBUG("User %s authenticated via API token (GM level %d)", result.username.c_str(), result.gmLevel);
return true;
} else {
LOG_DEBUG("Authentication token validation failed: %s", result.errorMessage.c_str());
return true; // Continue - let routes decide if auth is required
}
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include <string>
#include <cstdint>
/**
* Centralized authentication token handler
* Consolidates token extraction from multiple sources and validation
* Used by both HTTP API routes and WebSocket connections
*/
class AuthTokenHandler {
public:
/**
* Result of token extraction and validation
*/
struct TokenValidationResult {
bool isValid{false};
std::string username{};
uint8_t gmLevel{0};
std::string errorMessage{};
};
/**
* Extract token from query string
* Expected format: "?token=eyJhbGc..." or "?token=xyz&other=abc"
* @param queryString The query string from the request
* @return The token value, or empty string if not found
*/
static std::string ExtractTokenFromQueryString(const std::string& queryString);
/**
* Extract token from Cookie header
* Looks for "dashboardToken=<value>" in the cookie string
* @param cookieHeader The Cookie header value
* @return The token value, or empty string if not found
*/
static std::string ExtractTokenFromCookieHeader(const std::string& cookieHeader);
/**
* Extract token from Authorization header
* Supports "Bearer <token>", "Token <token>", or raw token
* @param authHeader The Authorization header value
* @return The token value, or empty string if not found
*/
static std::string ExtractTokenFromAuthHeader(const std::string& authHeader);
/**
* Extract token from any available source
* Tries in priority order: query string, cookie, auth header
* @param queryString The query string
* @param cookieHeader The Cookie header
* @param authHeader The Authorization header
* @return The first token found, or empty string
*/
static std::string ExtractToken(
const std::string& queryString,
const std::string& cookieHeader,
const std::string& authHeader
);
/**
* Validate a token and extract user information
* Checks JWT signature, expiration, and user permissions
* @param token The JWT token
* @return TokenValidationResult with validity status and user info
*/
static TokenValidationResult ValidateToken(const std::string& token);
/**
* Convenience method: Extract and validate token in one call
* @param queryString Query string from request
* @param cookieHeader Cookie header from request
* @param authHeader Authorization header from request
* @return TokenValidationResult with validity status and user info
*/
static TokenValidationResult ExtractAndValidateToken(
const std::string& queryString,
const std::string& cookieHeader,
const std::string& authHeader
);
/**
* Process authentication for HTTP middleware use
* Extracts and validates token from request, sets HTTPContext properties
* @param context HTTP request context (modified to include auth info)
* @param reply HTTP reply (not modified unless validation fails silently)
* @return true to continue middleware chain, false to stop
*/
static bool ProcessHTTPContext(class HTTPContext& context, class HTTPReply& reply);
};

View File

@@ -2,6 +2,7 @@ set(DASHBOARDAUTH_SOURCES
"JWTUtils.cpp"
"DashboardAuthService.cpp"
"AuthMiddleware.cpp"
"AuthTokenHandler.cpp"
"RequireAuthMiddleware.cpp"
)