1 #pragma once 2 3 #include "logging.hpp" 4 #include "ossl_random.hpp" 5 #include "utility.hpp" 6 #include "utils/ip_utils.hpp" 7 8 #include <nlohmann/json.hpp> 9 10 #include <algorithm> 11 #include <csignal> 12 #include <optional> 13 #include <random> 14 15 namespace persistent_data 16 { 17 18 // entropy: 20 characters, 62 possibilities. log2(62^20) = 119 bits of 19 // entropy. OWASP recommends at least 64 20 // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy 21 constexpr std::size_t sessionTokenSize = 20; 22 23 enum class PersistenceType 24 { 25 TIMEOUT, // User session times out after a predetermined amount of time 26 SINGLE_REQUEST // User times out once this request is completed. 27 }; 28 29 struct UserSession 30 { 31 std::string uniqueId; 32 std::string sessionToken; 33 std::string username; 34 std::string csrfToken; 35 std::optional<std::string> clientId; 36 std::string clientIp; 37 std::chrono::time_point<std::chrono::steady_clock> lastUpdated; 38 PersistenceType persistence{PersistenceType::TIMEOUT}; 39 bool cookieAuth = false; 40 bool isConfigureSelfOnly = false; 41 std::string userRole; 42 std::vector<std::string> userGroups; 43 44 // There are two sources of truth for isConfigureSelfOnly: 45 // 1. When pamAuthenticateUser() returns PAM_NEW_AUTHTOK_REQD. 46 // 2. D-Bus User.Manager.GetUserInfo property UserPasswordExpired. 47 // These should be in sync, but the underlying condition can change at any 48 // time. For example, a password can expire or be changed outside of 49 // bmcweb. The value stored here is updated at the start of each 50 // operation and used as the truth within bmcweb. 51 52 /** 53 * @brief Fills object with data from UserSession's JSON representation 54 * 55 * This replaces nlohmann's from_json to ensure no-throw approach 56 * 57 * @param[in] j JSON object from which data should be loaded 58 * 59 * @return a shared pointer if data has been loaded properly, nullptr 60 * otherwise 61 */ 62 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j) 63 { 64 std::shared_ptr<UserSession> userSession = 65 std::make_shared<UserSession>(); 66 for (const auto& element : j.items()) 67 { 68 const std::string* thisValue = 69 element.value().get_ptr<const std::string*>(); 70 if (thisValue == nullptr) 71 { 72 BMCWEB_LOG_ERROR( 73 "Error reading persistent store. Property {} was not of type string", 74 element.key()); 75 continue; 76 } 77 if (element.key() == "unique_id") 78 { 79 userSession->uniqueId = *thisValue; 80 } 81 else if (element.key() == "session_token") 82 { 83 userSession->sessionToken = *thisValue; 84 } 85 else if (element.key() == "csrf_token") 86 { 87 userSession->csrfToken = *thisValue; 88 } 89 else if (element.key() == "username") 90 { 91 userSession->username = *thisValue; 92 } 93 else if (element.key() == "client_id") 94 { 95 userSession->clientId = *thisValue; 96 } 97 else if (element.key() == "client_ip") 98 { 99 userSession->clientIp = *thisValue; 100 } 101 102 else 103 { 104 BMCWEB_LOG_ERROR( 105 "Got unexpected property reading persistent file: {}", 106 element.key()); 107 continue; 108 } 109 } 110 // If any of these fields are missing, we can't restore the session, as 111 // we don't have enough information. These 4 fields have been present 112 // in every version of this file in bmcwebs history, so any file, even 113 // on upgrade, should have these present 114 if (userSession->uniqueId.empty() || userSession->username.empty() || 115 userSession->sessionToken.empty() || userSession->csrfToken.empty()) 116 { 117 BMCWEB_LOG_DEBUG("Session missing required security " 118 "information, refusing to restore"); 119 return nullptr; 120 } 121 122 // For now, sessions that were persisted through a reboot get their idle 123 // timer reset. This could probably be overcome with a better 124 // understanding of wall clock time and steady timer time, possibly 125 // persisting values with wall clock time instead of steady timer, but 126 // the tradeoffs of all the corner cases involved are non-trivial, so 127 // this is done temporarily 128 userSession->lastUpdated = std::chrono::steady_clock::now(); 129 userSession->persistence = PersistenceType::TIMEOUT; 130 131 return userSession; 132 } 133 }; 134 135 struct AuthConfigMethods 136 { 137 bool basic = BMCWEB_BASIC_AUTH; 138 bool sessionToken = BMCWEB_SESSION_AUTH; 139 bool xtoken = BMCWEB_XTOKEN_AUTH; 140 bool cookie = BMCWEB_COOKIE_AUTH; 141 bool tls = BMCWEB_MUTUAL_TLS_AUTH; 142 143 void fromJson(const nlohmann::json& j) 144 { 145 for (const auto& element : j.items()) 146 { 147 const bool* value = element.value().get_ptr<const bool*>(); 148 if (value == nullptr) 149 { 150 continue; 151 } 152 153 if (element.key() == "XToken") 154 { 155 xtoken = *value; 156 } 157 else if (element.key() == "Cookie") 158 { 159 cookie = *value; 160 } 161 else if (element.key() == "SessionToken") 162 { 163 sessionToken = *value; 164 } 165 else if (element.key() == "BasicAuth") 166 { 167 basic = *value; 168 } 169 else if (element.key() == "TLS") 170 { 171 tls = *value; 172 } 173 } 174 } 175 }; 176 177 class SessionStore 178 { 179 public: 180 std::shared_ptr<UserSession> generateUserSession( 181 std::string_view username, const boost::asio::ip::address& clientIp, 182 const std::optional<std::string>& clientId, 183 PersistenceType persistence = PersistenceType::TIMEOUT, 184 bool isConfigureSelfOnly = false) 185 { 186 // TODO(ed) find a secure way to not generate session identifiers if 187 // persistence is set to SINGLE_REQUEST 188 static constexpr std::array<char, 62> alphanum = { 189 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 190 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 191 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 192 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 193 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; 194 195 std::string sessionToken; 196 sessionToken.resize(sessionTokenSize, '0'); 197 std::uniform_int_distribution<size_t> dist(0, alphanum.size() - 1); 198 199 bmcweb::OpenSSLGenerator gen; 200 201 for (char& sessionChar : sessionToken) 202 { 203 sessionChar = alphanum[dist(gen)]; 204 if (gen.error()) 205 { 206 return nullptr; 207 } 208 } 209 // Only need csrf tokens for cookie based auth, token doesn't matter 210 std::string csrfToken; 211 csrfToken.resize(sessionTokenSize, '0'); 212 for (char& csrfChar : csrfToken) 213 { 214 csrfChar = alphanum[dist(gen)]; 215 if (gen.error()) 216 { 217 return nullptr; 218 } 219 } 220 221 std::string uniqueId; 222 uniqueId.resize(10, '0'); 223 for (char& uidChar : uniqueId) 224 { 225 uidChar = alphanum[dist(gen)]; 226 if (gen.error()) 227 { 228 return nullptr; 229 } 230 } 231 232 auto session = std::make_shared<UserSession>( 233 UserSession{uniqueId, 234 sessionToken, 235 std::string(username), 236 csrfToken, 237 clientId, 238 redfish::ip_util::toString(clientIp), 239 std::chrono::steady_clock::now(), 240 persistence, 241 false, 242 isConfigureSelfOnly, 243 "", 244 {}}); 245 auto it = authTokens.emplace(sessionToken, session); 246 // Only need to write to disk if session isn't about to be destroyed. 247 needWrite = persistence == PersistenceType::TIMEOUT; 248 return it.first->second; 249 } 250 251 std::shared_ptr<UserSession> loginSessionByToken(std::string_view token) 252 { 253 applySessionTimeouts(); 254 if (token.size() != sessionTokenSize) 255 { 256 return nullptr; 257 } 258 auto sessionIt = authTokens.find(std::string(token)); 259 if (sessionIt == authTokens.end()) 260 { 261 return nullptr; 262 } 263 std::shared_ptr<UserSession> userSession = sessionIt->second; 264 userSession->lastUpdated = std::chrono::steady_clock::now(); 265 return userSession; 266 } 267 268 std::shared_ptr<UserSession> getSessionByUid(std::string_view uid) 269 { 270 applySessionTimeouts(); 271 // TODO(Ed) this is inefficient 272 auto sessionIt = authTokens.begin(); 273 while (sessionIt != authTokens.end()) 274 { 275 if (sessionIt->second->uniqueId == uid) 276 { 277 return sessionIt->second; 278 } 279 sessionIt++; 280 } 281 return nullptr; 282 } 283 284 void removeSession(const std::shared_ptr<UserSession>& session) 285 { 286 authTokens.erase(session->sessionToken); 287 needWrite = true; 288 } 289 290 std::vector<const std::string*> getUniqueIds( 291 bool getAll = true, 292 const PersistenceType& type = PersistenceType::SINGLE_REQUEST) 293 { 294 applySessionTimeouts(); 295 296 std::vector<const std::string*> ret; 297 ret.reserve(authTokens.size()); 298 for (auto& session : authTokens) 299 { 300 if (getAll || type == session.second->persistence) 301 { 302 ret.push_back(&session.second->uniqueId); 303 } 304 } 305 return ret; 306 } 307 308 void removeSessionsByUsername(std::string_view username) 309 { 310 std::erase_if(authTokens, [username](const auto& value) { 311 if (value.second == nullptr) 312 { 313 return false; 314 } 315 return value.second->username == username; 316 }); 317 } 318 319 void removeSessionsByUsernameExceptSession( 320 std::string_view username, const std::shared_ptr<UserSession>& session) 321 { 322 std::erase_if(authTokens, [username, session](const auto& value) { 323 if (value.second == nullptr) 324 { 325 return false; 326 } 327 328 return value.second->username == username && 329 value.second->uniqueId != session->uniqueId; 330 }); 331 } 332 333 void updateAuthMethodsConfig(const AuthConfigMethods& config) 334 { 335 bool isTLSchanged = (authMethodsConfig.tls != config.tls); 336 authMethodsConfig = config; 337 needWrite = true; 338 if (isTLSchanged) 339 { 340 // recreate socket connections with new settings 341 std::raise(SIGHUP); 342 } 343 } 344 345 AuthConfigMethods& getAuthMethodsConfig() 346 { 347 return authMethodsConfig; 348 } 349 350 bool needsWrite() const 351 { 352 return needWrite; 353 } 354 int64_t getTimeoutInSeconds() const 355 { 356 return std::chrono::seconds(timeoutInSeconds).count(); 357 } 358 359 void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds) 360 { 361 timeoutInSeconds = newTimeoutInSeconds; 362 needWrite = true; 363 } 364 365 static SessionStore& getInstance() 366 { 367 static SessionStore sessionStore; 368 return sessionStore; 369 } 370 371 void applySessionTimeouts() 372 { 373 auto timeNow = std::chrono::steady_clock::now(); 374 if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1)) 375 { 376 lastTimeoutUpdate = timeNow; 377 auto authTokensIt = authTokens.begin(); 378 while (authTokensIt != authTokens.end()) 379 { 380 if (timeNow - authTokensIt->second->lastUpdated >= 381 timeoutInSeconds) 382 { 383 authTokensIt = authTokens.erase(authTokensIt); 384 385 needWrite = true; 386 } 387 else 388 { 389 authTokensIt++; 390 } 391 } 392 } 393 } 394 395 SessionStore(const SessionStore&) = delete; 396 SessionStore& operator=(const SessionStore&) = delete; 397 SessionStore(SessionStore&&) = delete; 398 SessionStore& operator=(const SessionStore&&) = delete; 399 ~SessionStore() = default; 400 401 std::unordered_map<std::string, std::shared_ptr<UserSession>, 402 std::hash<std::string>, 403 crow::utility::ConstantTimeCompare> 404 authTokens; 405 406 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate; 407 bool needWrite{false}; 408 std::chrono::seconds timeoutInSeconds; 409 AuthConfigMethods authMethodsConfig; 410 411 private: 412 SessionStore() : timeoutInSeconds(1800) {} 413 }; 414 415 } // namespace persistent_data 416