This commit is contained in:
parent
99151c6692
commit
3676dc46ed
16 changed files with 894 additions and 89 deletions
|
|
@ -31,17 +31,28 @@ namespace {
|
|||
return ss.str();
|
||||
}
|
||||
|
||||
// Helper to set httpOnly auth cookie
|
||||
// Helper to set httpOnly access token cookie (2.5 hours to match JWT expiry)
|
||||
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.setMaxAge(9000); // 2.5 hours (150 minutes, matches JWT expiry)
|
||||
authCookie.setSameSite(Cookie::SameSite::kLax);
|
||||
resp->addCookie(authCookie);
|
||||
}
|
||||
|
||||
|
||||
// Helper to set httpOnly refresh token cookie (long-lived: 90 days)
|
||||
void setRefreshCookie(const HttpResponsePtr& resp, const std::string& token) {
|
||||
Cookie refreshCookie("refresh_token", token);
|
||||
refreshCookie.setPath("/");
|
||||
refreshCookie.setHttpOnly(true);
|
||||
refreshCookie.setSecure(false); // Set to true in production with HTTPS
|
||||
refreshCookie.setMaxAge(90 * 24 * 60 * 60); // 90 days
|
||||
refreshCookie.setSameSite(Cookie::SameSite::kLax);
|
||||
resp->addCookie(refreshCookie);
|
||||
}
|
||||
|
||||
// Helper to clear auth cookie
|
||||
void clearAuthCookie(const HttpResponsePtr& resp) {
|
||||
Cookie authCookie("auth_token", "");
|
||||
|
|
@ -50,6 +61,15 @@ namespace {
|
|||
authCookie.setMaxAge(0); // Expire immediately
|
||||
resp->addCookie(authCookie);
|
||||
}
|
||||
|
||||
// Helper to clear refresh cookie
|
||||
void clearRefreshCookie(const HttpResponsePtr& resp) {
|
||||
Cookie refreshCookie("refresh_token", "");
|
||||
refreshCookie.setPath("/");
|
||||
refreshCookie.setHttpOnly(true);
|
||||
refreshCookie.setMaxAge(0); // Expire immediately
|
||||
resp->addCookie(refreshCookie);
|
||||
}
|
||||
}
|
||||
|
||||
void UserController::register_(const HttpRequestPtr &req,
|
||||
|
|
@ -179,31 +199,44 @@ void UserController::login(const HttpRequestPtr &req,
|
|||
if (success) {
|
||||
LOG_INFO << "Login successful for user: " << username;
|
||||
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
// Send token in body for WebSocket auth (cookie still used for HTTP requests)
|
||||
resp["token"] = token;
|
||||
resp["user"]["id"] = static_cast<Json::Int64>(user.id);
|
||||
resp["user"]["username"] = user.username;
|
||||
resp["user"]["isAdmin"] = user.isAdmin;
|
||||
resp["user"]["isStreamer"] = user.isStreamer;
|
||||
resp["user"]["isRestreamer"] = user.isRestreamer;
|
||||
resp["user"]["isBot"] = user.isBot;
|
||||
resp["user"]["isTexter"] = user.isTexter;
|
||||
resp["user"]["isPgpOnly"] = user.isPgpOnly;
|
||||
resp["user"]["bio"] = user.bio;
|
||||
resp["user"]["avatarUrl"] = user.avatarUrl;
|
||||
resp["user"]["bannerUrl"] = user.bannerUrl;
|
||||
resp["user"]["bannerPosition"] = user.bannerPosition;
|
||||
resp["user"]["bannerZoom"] = user.bannerZoom;
|
||||
resp["user"]["bannerPositionX"] = user.bannerPositionX;
|
||||
resp["user"]["graffitiUrl"] = user.graffitiUrl;
|
||||
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
|
||||
resp["user"]["colorCode"] = user.colorCode;
|
||||
// Create refresh token family for long-lived session
|
||||
AuthService::getInstance().createRefreshTokenFamily(user.id,
|
||||
[callback, username, token, user](bool refreshSuccess, const std::string& refreshToken,
|
||||
const std::string& familyId) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
// Send token in body for WebSocket auth (cookie still used for HTTP requests)
|
||||
resp["token"] = token;
|
||||
resp["user"]["id"] = static_cast<Json::Int64>(user.id);
|
||||
resp["user"]["username"] = user.username;
|
||||
resp["user"]["isAdmin"] = user.isAdmin;
|
||||
resp["user"]["isStreamer"] = user.isStreamer;
|
||||
resp["user"]["isRestreamer"] = user.isRestreamer;
|
||||
resp["user"]["isBot"] = user.isBot;
|
||||
resp["user"]["isTexter"] = user.isTexter;
|
||||
resp["user"]["isPgpOnly"] = user.isPgpOnly;
|
||||
resp["user"]["bio"] = user.bio;
|
||||
resp["user"]["avatarUrl"] = user.avatarUrl;
|
||||
resp["user"]["bannerUrl"] = user.bannerUrl;
|
||||
resp["user"]["bannerPosition"] = user.bannerPosition;
|
||||
resp["user"]["bannerZoom"] = user.bannerZoom;
|
||||
resp["user"]["bannerPositionX"] = user.bannerPositionX;
|
||||
resp["user"]["graffitiUrl"] = user.graffitiUrl;
|
||||
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
|
||||
resp["user"]["colorCode"] = user.colorCode;
|
||||
|
||||
auto response = jsonResp(resp);
|
||||
setAuthCookie(response, token);
|
||||
callback(response);
|
||||
auto response = jsonResp(resp);
|
||||
setAuthCookie(response, token);
|
||||
|
||||
// Also set refresh token cookie if created successfully
|
||||
if (refreshSuccess) {
|
||||
setRefreshCookie(response, refreshToken);
|
||||
} else {
|
||||
LOG_WARN << "Failed to create refresh token for user: " << username;
|
||||
}
|
||||
|
||||
callback(response);
|
||||
});
|
||||
} else {
|
||||
LOG_WARN << "Login failed for user: " << username;
|
||||
callback(jsonError(token.empty() ? "Invalid credentials" : token, k401Unauthorized));
|
||||
|
|
@ -224,9 +257,10 @@ void UserController::logout(const HttpRequestPtr &req,
|
|||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["message"] = "Logged out successfully";
|
||||
|
||||
|
||||
auto response = jsonResp(resp);
|
||||
clearAuthCookie(response);
|
||||
clearRefreshCookie(response); // Also clear refresh token
|
||||
callback(response);
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in logout: " << e.what();
|
||||
|
|
@ -234,6 +268,58 @@ void UserController::logout(const HttpRequestPtr &req,
|
|||
}
|
||||
}
|
||||
|
||||
void UserController::refresh(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
try {
|
||||
// Get refresh token from cookie
|
||||
std::string refreshToken = req->getCookie("refresh_token");
|
||||
|
||||
if (refreshToken.empty()) {
|
||||
LOG_DEBUG << "No refresh token provided";
|
||||
callback(jsonError("No refresh token", k401Unauthorized));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and rotate the refresh token
|
||||
AuthService::getInstance().validateAndRotateRefreshToken(refreshToken,
|
||||
[callback](RefreshTokenResult result) {
|
||||
if (result.success) {
|
||||
LOG_DEBUG << "Token refresh successful for user: " << result.user.username;
|
||||
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["token"] = result.accessToken;
|
||||
resp["user"]["id"] = static_cast<Json::Int64>(result.user.id);
|
||||
resp["user"]["username"] = result.user.username;
|
||||
resp["user"]["isAdmin"] = result.user.isAdmin;
|
||||
resp["user"]["isStreamer"] = result.user.isStreamer;
|
||||
resp["user"]["isRestreamer"] = result.user.isRestreamer;
|
||||
resp["user"]["isBot"] = result.user.isBot;
|
||||
resp["user"]["isTexter"] = result.user.isTexter;
|
||||
resp["user"]["isPgpOnly"] = result.user.isPgpOnly;
|
||||
resp["user"]["colorCode"] = result.user.colorCode;
|
||||
resp["user"]["avatarUrl"] = result.user.avatarUrl;
|
||||
|
||||
auto response = jsonResp(resp);
|
||||
setAuthCookie(response, result.accessToken);
|
||||
setRefreshCookie(response, result.refreshToken);
|
||||
callback(response);
|
||||
} else {
|
||||
LOG_DEBUG << "Token refresh failed: " << result.error;
|
||||
|
||||
// Clear cookies on failure
|
||||
auto response = jsonError(result.error, k401Unauthorized);
|
||||
clearAuthCookie(response);
|
||||
clearRefreshCookie(response);
|
||||
callback(response);
|
||||
}
|
||||
});
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Exception in refresh: " << e.what();
|
||||
callback(jsonError("Internal server error"));
|
||||
}
|
||||
}
|
||||
|
||||
void UserController::pgpChallenge(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
try {
|
||||
|
|
@ -301,31 +387,44 @@ void UserController::pgpVerify(const HttpRequestPtr &req,
|
|||
AuthService::getInstance().verifyPgpLogin(username, signature, challenge,
|
||||
[callback](bool success, const std::string& token, const UserInfo& user) {
|
||||
if (success) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
// Send token in body for WebSocket auth (cookie still used for HTTP requests)
|
||||
resp["token"] = token;
|
||||
resp["user"]["id"] = static_cast<Json::Int64>(user.id);
|
||||
resp["user"]["username"] = user.username;
|
||||
resp["user"]["isAdmin"] = user.isAdmin;
|
||||
resp["user"]["isStreamer"] = user.isStreamer;
|
||||
resp["user"]["isRestreamer"] = user.isRestreamer;
|
||||
resp["user"]["isBot"] = user.isBot;
|
||||
resp["user"]["isTexter"] = user.isTexter;
|
||||
resp["user"]["isPgpOnly"] = user.isPgpOnly;
|
||||
resp["user"]["bio"] = user.bio;
|
||||
resp["user"]["avatarUrl"] = user.avatarUrl;
|
||||
resp["user"]["bannerUrl"] = user.bannerUrl;
|
||||
resp["user"]["bannerPosition"] = user.bannerPosition;
|
||||
resp["user"]["bannerZoom"] = user.bannerZoom;
|
||||
resp["user"]["bannerPositionX"] = user.bannerPositionX;
|
||||
resp["user"]["graffitiUrl"] = user.graffitiUrl;
|
||||
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
|
||||
resp["user"]["colorCode"] = user.colorCode;
|
||||
// Create refresh token family for long-lived session
|
||||
AuthService::getInstance().createRefreshTokenFamily(user.id,
|
||||
[callback, token, user](bool refreshSuccess, const std::string& refreshToken,
|
||||
const std::string& familyId) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
// Send token in body for WebSocket auth (cookie still used for HTTP requests)
|
||||
resp["token"] = token;
|
||||
resp["user"]["id"] = static_cast<Json::Int64>(user.id);
|
||||
resp["user"]["username"] = user.username;
|
||||
resp["user"]["isAdmin"] = user.isAdmin;
|
||||
resp["user"]["isStreamer"] = user.isStreamer;
|
||||
resp["user"]["isRestreamer"] = user.isRestreamer;
|
||||
resp["user"]["isBot"] = user.isBot;
|
||||
resp["user"]["isTexter"] = user.isTexter;
|
||||
resp["user"]["isPgpOnly"] = user.isPgpOnly;
|
||||
resp["user"]["bio"] = user.bio;
|
||||
resp["user"]["avatarUrl"] = user.avatarUrl;
|
||||
resp["user"]["bannerUrl"] = user.bannerUrl;
|
||||
resp["user"]["bannerPosition"] = user.bannerPosition;
|
||||
resp["user"]["bannerZoom"] = user.bannerZoom;
|
||||
resp["user"]["bannerPositionX"] = user.bannerPositionX;
|
||||
resp["user"]["graffitiUrl"] = user.graffitiUrl;
|
||||
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
|
||||
resp["user"]["colorCode"] = user.colorCode;
|
||||
|
||||
auto response = jsonResp(resp);
|
||||
setAuthCookie(response, token);
|
||||
callback(response);
|
||||
auto response = jsonResp(resp);
|
||||
setAuthCookie(response, token);
|
||||
|
||||
// Also set refresh token cookie if created successfully
|
||||
if (refreshSuccess) {
|
||||
setRefreshCookie(response, refreshToken);
|
||||
} else {
|
||||
LOG_WARN << "Failed to create refresh token for PGP login";
|
||||
}
|
||||
|
||||
callback(response);
|
||||
});
|
||||
} else {
|
||||
callback(jsonError("Invalid signature", k401Unauthorized));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public:
|
|||
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::refresh, "/api/auth/refresh", 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);
|
||||
|
|
@ -57,7 +58,10 @@ public:
|
|||
|
||||
void logout(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
|
||||
void refresh(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void pgpChallenge(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue