R-Type
Distributed multiplayer game engine in C++
Loading...
Searching...
No Matches
AuthService.cpp
Go to the documentation of this file.
1/*
2** EPITECH PROJECT, 2025
3** Created by hugo on 06/12/2025
4** File description:
5** AuthService.cpp
6*/
7
9#include <algorithm>
10#include <chrono>
11#include <cstring>
12#include <fstream>
13#include <nlohmann/json.hpp>
14#include <random>
15#include <sstream>
18
19using json = nlohmann::json;
20
21namespace server {
22
23 AuthService::AuthService() : _passwordHasher(std::make_unique<Argon2PasswordHasher>()) {
25 }
26
27 AuthService::AuthService(const std::string &accountsFile)
28 : _accountsFile(accountsFile), _passwordHasher(std::make_unique<Argon2PasswordHasher>()) {
30 }
31
37
38 bool AuthService::authenticate(const std::string &username, const std::string &password) {
39 // Special case: guest login doesn't require account registration
40 if (username == "guest" && password == "guest") {
41 _authenticatedUsers.insert(username);
42 return true;
43 }
44
45 if (username.empty() || password.empty()) {
46 LOG_WARNING("Authentication failed: empty credentials");
47 return false;
48 }
49
50 // Minimum length requirements
51 if (username.length() < 3 || password.length() < 4) {
52 LOG_WARNING("Authentication failed: credentials too short (username: ", username.length(),
53 ", password: ", password.length(), " chars)");
54 return false;
55 }
56
57 // Check against stored accounts
58 auto it = _accounts.find(username);
59 if (it == _accounts.end()) {
60 LOG_WARNING("Authentication failed: account '", username, "' doesn't exist");
61 return false; // Account doesn't exist
62 }
63
64 // Verify password using password hasher
65 if (!_passwordHasher->verify(password, it->second.passwordHash)) {
66 LOG_WARNING("Authentication failed: incorrect password for '", username, "'");
67 return false; // Wrong password
68 }
69
70 // Update last login timestamp
71 auto now = std::chrono::system_clock::now();
72 uint64_t nowSeconds =
73 std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
74 it->second.lastLogin = nowSeconds;
75 _accountsDirty = true;
76
77 // Deferred save: only save if enough time has passed
78 if (nowSeconds - _lastSaveTime >= SAVE_INTERVAL_SECONDS) {
80 _lastSaveTime = nowSeconds;
81 _accountsDirty = false;
82 }
83
84 // Store authenticated session
85 _authenticatedUsers.insert(username);
86
87 LOG_INFO("✓ Authentication successful for user: ", username);
88 return true;
89 }
90
91 std::string AuthService::generateToken(const std::string &username) {
92 // Generate a simple session token (in production, use JWT or secure tokens)
93 std::random_device rd;
94 std::mt19937 gen(rd());
95 std::uniform_int_distribution<> dis(0, 15);
96
97 const char *hex = "0123456789abcdef";
98 std::ostringstream token;
99 token << username << "_";
100 for (int i = 0; i < 32; ++i) {
101 token << hex[dis(gen)];
102 }
103
104 _activeTokens[token.str()] = username;
105 return token.str();
106 }
107
108 bool AuthService::validateToken(const std::string &token) {
109 return _activeTokens.contains(token);
110 }
111
112 void AuthService::revokeToken(const std::string &token) {
113 auto it = _activeTokens.find(token);
114 if (it != _activeTokens.end()) {
115 _activeTokens.erase(it);
116 }
117 }
118
119 bool AuthService::isUserAuthenticated(const std::string &username) const {
120 return _authenticatedUsers.contains(username);
121 }
122
123 bool AuthService::registerUser(const std::string &username, const std::string &password) {
124 // Prevent registering "guest" as a regular account
125 if (username == "guest" || username.starts_with("Guest_")) {
126 LOG_WARNING("Registration failed: username '", username, "' is reserved for guest access");
127 return false; // Guest is reserved for anonymous access
128 }
129
130 // Validation
131 if (username.empty() || password.empty()) {
132 LOG_WARNING("Registration failed: empty username or password");
133 return false;
134 }
135
136 if (username.length() < 3) {
137 LOG_WARNING("Registration failed: username '", username, "' too short (", username.length(),
138 " chars, minimum 3)");
139 return false;
140 }
141
142 if (password.length() < 4) {
143 LOG_WARNING("Registration failed: password too short (", password.length(), " chars, minimum 4)");
144 return false;
145 }
146
147 // Check if username already exists
148 if (_accounts.find(username) != _accounts.end()) {
149 LOG_WARNING("Registration failed: username '", username, "' already exists");
150 return false;
151 }
152
153 // Hash the password
154 std::string passwordHash;
155 try {
156 passwordHash = _passwordHasher->hash(password);
157 } catch (const std::exception &e) {
158 LOG_ERROR("Registration failed: password hashing failed for '", username, "': ", e.what());
159 return false;
160 }
161
162 // Create new account
163 auto now = std::chrono::system_clock::now();
164 uint64_t timestamp = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
165
166 AccountData account;
167 account.username = username;
168 account.passwordHash = passwordHash;
169 account.createdAt = timestamp;
170 account.lastLogin = 0; // Never logged in yet
171
172 _accounts[username] = account;
173 saveAccounts();
174 _accountsDirty = false;
175
176 LOG_INFO("✓ Registration successful for user: ", username);
177 return true;
178 }
179
181 std::ifstream file(_accountsFile);
182 if (!file.is_open()) {
183 LOG_INFO("No accounts file found at '", _accountsFile, "', starting with empty database");
184 // File doesn't exist, start with empty accounts
185 // Guest login works without registration
186 return;
187 }
188
189 // Check if file is empty
190 file.seekg(0, std::ios::end);
191 if (file.tellg() == 0) {
192 LOG_INFO("Accounts file is empty, starting with empty database");
193 file.close();
194 return;
195 }
196 file.seekg(0, std::ios::beg);
197
198 try {
199 json j;
200 file >> j;
201 file.close();
202
203 if (!j.contains("accounts") || !j["accounts"].is_array()) {
204 LOG_WARNING("Invalid accounts file format, starting with empty database");
205 return;
206 }
207
208 for (const auto &accountJson : j["accounts"]) {
209 if (!accountJson.contains("username") || !accountJson.contains("passwordHash")) {
210 LOG_WARNING("Skipping invalid account entry (missing username or passwordHash)");
211 continue;
212 }
213
214 AccountData account;
215 account.username = accountJson["username"].get<std::string>();
216 account.passwordHash = accountJson["passwordHash"].get<std::string>();
217 account.createdAt = accountJson.value("createdAt", 0ULL);
218 account.lastLogin = accountJson.value("lastLogin", 0ULL);
219 account.autoMatchmaking = accountJson.value("autoMatchmaking", false);
220
221 _accounts[account.username] = account;
222 }
223
224 LOG_INFO("✓ Loaded ", _accounts.size(), " accounts from '", _accountsFile, "'");
225
226 } catch (const json::exception &e) {
227 LOG_ERROR("Failed to parse accounts file: ", e.what());
228 LOG_WARNING("Starting with empty accounts database");
229 }
230 }
231
233 try {
234 json j;
235 j["version"] = "1.0";
236 j["accounts"] = json::array();
237
238 for (const auto &[username, account] : _accounts) {
239 json accountJson;
240 accountJson["username"] = account.username;
241 accountJson["passwordHash"] = account.passwordHash;
242 accountJson["createdAt"] = account.createdAt;
243 accountJson["lastLogin"] = account.lastLogin;
244 accountJson["autoMatchmaking"] = account.autoMatchmaking;
245
246 j["accounts"].push_back(accountJson);
247 }
248
249 std::ofstream file(_accountsFile);
250 if (!file.is_open()) {
251 LOG_ERROR("Failed to open accounts file '", _accountsFile, "' for writing");
252 return;
253 }
254
255 file << j.dump(2); // Pretty print with 2 spaces indentation
256 file.close();
257
258 LOG_INFO("✓ Saved ", _accounts.size(), " accounts to '", _accountsFile, "'");
259
260 } catch (const json::exception &e) {
261 LOG_ERROR("Failed to save accounts: ", e.what());
262 }
263 }
264
265 bool AuthService::updateAutoMatchmaking(const std::string &username, bool enabled) {
266 auto it = _accounts.find(username);
267 if (it == _accounts.end()) {
268 LOG_WARNING("Cannot update auto-matchmaking: user '", username, "' not found");
269 return false;
270 }
271
272 it->second.autoMatchmaking = enabled;
273
274 // Force immediate save for user preferences (important UX)
275 saveAccounts();
276 auto now = std::chrono::system_clock::now();
277 _lastSaveTime = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
278 _accountsDirty = false;
279
280 LOG_INFO("✓ Auto-matchmaking ", enabled ? "enabled" : "disabled", " for user '", username,
281 "' (saved)");
282 return true;
283 }
284
285 bool AuthService::getAutoMatchmaking(const std::string &username) const {
286 auto it = _accounts.find(username);
287 if (it == _accounts.end()) {
288 return false; // Default: auto-matchmaking disabled
289 }
290 return it->second.autoMatchmaking;
291 }
292
293} // namespace server
nlohmann::json json
#define LOG_INFO(...)
Definition Logger.hpp:181
#define LOG_ERROR(...)
Definition Logger.hpp:183
#define LOG_WARNING(...)
Definition Logger.hpp:182
Argon2id password hashing wrapper.
std::unordered_map< std::string, AccountData > _accounts
Map of username to account data.
std::unique_ptr< IPasswordHasher > _passwordHasher
Password hashing implementation.
bool validateToken(const std::string &token)
Validate a token.
bool getAutoMatchmaking(const std::string &username) const
Get auto-matchmaking preference for a user.
std::unordered_map< std::string, std::string > _activeTokens
Map of tokens to usernames.
void loadAccounts()
Load user accounts from JSON file.
std::unordered_set< std::string > _authenticatedUsers
Set of authenticated usernames.
static constexpr uint64_t SAVE_INTERVAL_SECONDS
Save every 60 seconds.
bool isUserAuthenticated(const std::string &username) const
Check if a user is authenticated.
std::string generateToken(const std::string &username)
Generate an authentication token for a user.
bool registerUser(const std::string &username, const std::string &password)
Register a new user account.
void saveAccounts()
Save user accounts to JSON file.
uint64_t _lastSaveTime
Timestamp of last save.
~AuthService() override
std::string _accountsFile
JSON file to store accounts.
bool _accountsDirty
Flag indicating unsaved changes.
bool updateAutoMatchmaking(const std::string &username, bool enabled)
Update auto-matchmaking preference for a user.
bool authenticate(const std::string &username, const std::string &password) override
Authenticate a user with username and password.
void revokeToken(const std::string &token)
Revoke a token.
std::string passwordHash