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>(UserSession{ 260 uniqueId, sessionToken, std::string(username), csrfToken, clientId, 261 redfish::ip_util::toString(clientIp), 262 std::chrono::steady_clock::now(), persistence, false, 263 isConfigureSelfOnly}); 264 auto it = authTokens.emplace(sessionToken, session); 265 // Only need to write to disk if session isn't about to be destroyed. 266 needWrite = persistence == PersistenceType::TIMEOUT; 267 return it.first->second; 268 } 269 270 std::shared_ptr<UserSession> loginSessionByToken(std::string_view token) 271 { 272 applySessionTimeouts(); 273 if (token.size() != sessionTokenSize) 274 { 275 return nullptr; 276 } 277 auto sessionIt = authTokens.find(std::string(token)); 278 if (sessionIt == authTokens.end()) 279 { 280 return nullptr; 281 } 282 std::shared_ptr<UserSession> userSession = sessionIt->second; 283 userSession->lastUpdated = std::chrono::steady_clock::now(); 284 return userSession; 285 } 286 287 std::shared_ptr<UserSession> getSessionByUid(std::string_view uid) 288 { 289 applySessionTimeouts(); 290 // TODO(Ed) this is inefficient 291 auto sessionIt = authTokens.begin(); 292 while (sessionIt != authTokens.end()) 293 { 294 if (sessionIt->second->uniqueId == uid) 295 { 296 return sessionIt->second; 297 } 298 sessionIt++; 299 } 300 return nullptr; 301 } 302 303 void removeSession(const std::shared_ptr<UserSession>& session) 304 { 305 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 306 crow::ibm_mc_lock::Lock::getInstance().releaseLock(session->uniqueId); 307 #endif 308 authTokens.erase(session->sessionToken); 309 needWrite = true; 310 } 311 312 std::vector<const std::string*> getUniqueIds( 313 bool getAll = true, 314 const PersistenceType& type = PersistenceType::SINGLE_REQUEST) 315 { 316 applySessionTimeouts(); 317 318 std::vector<const std::string*> ret; 319 ret.reserve(authTokens.size()); 320 for (auto& session : authTokens) 321 { 322 if (getAll || type == session.second->persistence) 323 { 324 ret.push_back(&session.second->uniqueId); 325 } 326 } 327 return ret; 328 } 329 330 void removeSessionsByUsername(std::string_view username) 331 { 332 std::erase_if(authTokens, [username](const auto& value) { 333 if (value.second == nullptr) 334 { 335 return false; 336 } 337 return value.second->username == username; 338 }); 339 } 340 341 void updateAuthMethodsConfig(const AuthConfigMethods& config) 342 { 343 bool isTLSchanged = (authMethodsConfig.tls != config.tls); 344 authMethodsConfig = config; 345 needWrite = true; 346 if (isTLSchanged) 347 { 348 // recreate socket connections with new settings 349 std::raise(SIGHUP); 350 } 351 } 352 353 AuthConfigMethods& getAuthMethodsConfig() 354 { 355 return authMethodsConfig; 356 } 357 358 bool needsWrite() const 359 { 360 return needWrite; 361 } 362 int64_t getTimeoutInSeconds() const 363 { 364 return std::chrono::seconds(timeoutInSeconds).count(); 365 } 366 367 void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds) 368 { 369 timeoutInSeconds = newTimeoutInSeconds; 370 needWrite = true; 371 } 372 373 static SessionStore& getInstance() 374 { 375 static SessionStore sessionStore; 376 return sessionStore; 377 } 378 379 void applySessionTimeouts() 380 { 381 auto timeNow = std::chrono::steady_clock::now(); 382 if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1)) 383 { 384 lastTimeoutUpdate = timeNow; 385 auto authTokensIt = authTokens.begin(); 386 while (authTokensIt != authTokens.end()) 387 { 388 if (timeNow - authTokensIt->second->lastUpdated >= 389 timeoutInSeconds) 390 { 391 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE 392 crow::ibm_mc_lock::Lock::getInstance().releaseLock( 393 authTokensIt->second->uniqueId); 394 #endif 395 authTokensIt = authTokens.erase(authTokensIt); 396 397 needWrite = true; 398 } 399 else 400 { 401 authTokensIt++; 402 } 403 } 404 } 405 } 406 407 SessionStore(const SessionStore&) = delete; 408 SessionStore& operator=(const SessionStore&) = delete; 409 SessionStore(SessionStore&&) = delete; 410 SessionStore& operator=(const SessionStore&&) = delete; 411 ~SessionStore() = default; 412 413 std::unordered_map<std::string, std::shared_ptr<UserSession>, 414 std::hash<std::string>, 415 crow::utility::ConstantTimeCompare> 416 authTokens; 417 418 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate; 419 bool needWrite{false}; 420 std::chrono::seconds timeoutInSeconds; 421 AuthConfigMethods authMethodsConfig; 422 423 private: 424 SessionStore() : timeoutInSeconds(1800) {} 425 }; 426 427 } // namespace persistent_data 428