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