Files
DarkflameServer/dDashboardServer/auth/AuthTokenHandler.cpp
Aaron Kimbrell 3f2a9cc5b0 feat(dashboard): scoped API keys with rate limits and quotas
Bearer keys (dlk_...) are checked per request against their owner's
current account (ban, lock, demotion, sign out everywhere stop or narrow
them at once) and their scope: permission routes need the permission in
scope, read-only keys only read, level-only routes need an all-permission
key, and session-only paths (sign-in, password, 2FA, key management)
are never reachable with a key. Per-key rate limit and daily quota with
429 and X-RateLimit/X-Quota/Retry-After headers; usage is written in
batches every minute. WebSocket subscriptions honour the scope too.

Routes to list, make, rotate and revoke keys; staff with
api_keys_manage can see and revoke others' keys under the rank rules.
POST /api/auth/token now makes an all-permission key. Audit entries for
create/rotate/revoke/denied, and actions done with a key are attributed
to "user (key name)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:03 -05:00

115 lines
4.2 KiB
C++

#include "AuthTokenHandler.h"
#include "ApiKeyService.h"
#include "DashboardAuthService.h"
#include "Game.h"
#include "Logger.h"
#include "HTTPContext.h"
#include "Web.h"
std::string AuthTokenHandler::ExtractCookie(const std::string& cookieHeader, const std::string& name) {
// Match whole cookie names only so "xdashboardToken" does not satisfy "dashboardToken"
size_t pos = 0;
while (pos < cookieHeader.size()) {
while (pos < cookieHeader.size() && (cookieHeader[pos] == ' ' || cookieHeader[pos] == ';')) pos++;
const size_t end = std::min(cookieHeader.find(';', pos), cookieHeader.size());
const std::string_view pair(cookieHeader.data() + pos, end - pos);
const size_t eq = pair.find('=');
if (eq != std::string_view::npos && pair.substr(0, eq) == name) {
const auto value = pair.substr(eq + 1);
std::string decoded;
decoded.reserve(value.size());
for (size_t i = 0; i < value.size(); ++i) {
if (value[i] == '%' && i + 2 < value.size()) {
char* endptr = nullptr;
const std::string hex(value.substr(i + 1, 2));
const auto code = std::strtol(hex.c_str(), &endptr, 16);
if (endptr == hex.c_str() + 2) {
decoded += static_cast<char>(code);
i += 2;
continue;
}
}
decoded += value[i];
}
return decoded;
}
pos = end + 1;
}
return "";
}
std::string AuthTokenHandler::ExtractTokenFromAuthHeader(const std::string& authHeader) {
if (authHeader.starts_with("Bearer ")) return authHeader.substr(7);
if (authHeader.starts_with("Token ")) return authHeader.substr(6);
return "";
}
std::string AuthTokenHandler::ExtractToken(const std::string& cookieHeader, const std::string& authHeader, eTokenSource& source) {
auto token = ExtractTokenFromAuthHeader(authHeader);
if (!token.empty()) {
source = eTokenSource::HEADER;
return token;
}
token = ExtractCookie(cookieHeader, COOKIE_NAME);
source = token.empty() ? eTokenSource::NONE : eTokenSource::COOKIE;
return token;
}
AuthTokenHandler::TokenValidationResult AuthTokenHandler::ValidateToken(const std::string& token) {
TokenValidationResult result;
if (token.empty()) {
result.errorMessage = "No token provided";
return result;
}
if (!DashboardAuthService::VerifyToken(token, result.username, result.gmLevel, result.accountId)) {
result.errorMessage = "Invalid or expired token";
return result;
}
result.isValid = true;
return result;
}
bool AuthTokenHandler::ProcessHTTPContext(HTTPContext& context, HTTPReply& reply) {
eTokenSource source = eTokenSource::NONE;
const auto token = ExtractToken(context.GetHeader("Cookie"), context.GetHeader("Authorization"), source);
if (token.empty()) return true;
// API keys: their scope and limits are checked here; a key over its limits is refused outright
if (source == eTokenSource::HEADER && ApiKeyService::LooksLikeKey(token)) {
const auto keyResult = ApiKeyService::Authenticate(token, context, reply);
if (keyResult == ApiKeyService::eResult::INVALID) LOG_DEBUG("API key validation failed from %s", context.clientIP.c_str());
return keyResult != ApiKeyService::eResult::REFUSED;
}
const auto result = ValidateToken(token);
if (!result.isValid) {
LOG_DEBUG("Authentication token validation failed: %s", result.errorMessage.c_str());
return true; // Let routes decide if auth is required
}
context.isAuthenticated = true;
context.authenticatedUser = result.username;
context.accountId = result.accountId;
context.gmLevel = result.gmLevel;
context.userData["auth_source"] = source == eTokenSource::COOKIE ? "cookie" : "header";
if (DashboardAuthService::NeedsTwoFactorSetup(result.accountId, result.gmLevel)) context.userData["needs_2fa"] = "1";
return true;
}
std::string AuthTokenHandler::BuildSessionCookie(const std::string& token, bool rememberMe, bool secure) {
std::string cookie = std::string("Set-Cookie: ") + COOKIE_NAME + "=" + token + "; Path=/; HttpOnly; SameSite=Strict";
if (rememberMe) cookie += "; Max-Age=" + std::to_string(30 * 24 * 60 * 60);
if (secure) cookie += "; Secure";
return cookie;
}
std::string AuthTokenHandler::BuildClearSessionCookie(bool secure) {
std::string cookie = std::string("Set-Cookie: ") + COOKIE_NAME + "=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0";
if (secure) cookie += "; Secure";
return cookie;
}