1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "bmcweb_config.h" 6 7 #include "logging.hpp" 8 #include "ossl_random.hpp" 9 #include "utils/ip_utils.hpp" 10 11 #include <boost/asio/ip/address.hpp> 12 #include <nlohmann/json.hpp> 13 14 #include <chrono> 15 #include <csignal> 16 #include <cstddef> 17 #include <cstdint> 18 #include <functional> 19 #include <memory> 20 #include <optional> 21 #include <string> 22 #include <string_view> 23 #include <unordered_map> 24 #include <vector> 25 26 namespace persistent_data 27 { 28 29 // entropy: 20 characters, 62 possibilities. log2(62^20) = 119 bits of 30 // entropy. OWASP recommends at least 64 31 // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy 32 constexpr std::size_t sessionTokenSize = 20; 33 34 enum class SessionType 35 { 36 None, 37 Basic, 38 Session, 39 Cookie, 40 MutualTLS 41 }; 42 43 struct UserSession 44 { 45 std::string uniqueId; 46 std::string sessionToken; 47 std::string username; 48 std::string csrfToken; 49 std::optional<std::string> clientId; 50 std::string clientIp; 51 std::chrono::time_point<std::chrono::steady_clock> lastUpdated; 52 SessionType sessionType{SessionType::None}; 53 bool cookieAuth = false; 54 bool isConfigureSelfOnly = false; 55 std::string userRole; 56 std::vector<std::string> userGroups; 57 58 // There are two sources of truth for isConfigureSelfOnly: 59 // 1. When pamAuthenticateUser() returns PAM_NEW_AUTHTOK_REQD. 60 // 2. D-Bus User.Manager.GetUserInfo property UserPasswordExpired. 61 // These should be in sync, but the underlying condition can change at any 62 // time. For example, a password can expire or be changed outside of 63 // bmcweb. The value stored here is updated at the start of each 64 // operation and used as the truth within bmcweb. 65 66 /** 67 * @brief Fills object with data from UserSession's JSON representation 68 * 69 * This replaces nlohmann's from_json to ensure no-throw approach 70 * 71 * @param[in] j JSON object from which data should be loaded 72 * 73 * @return a shared pointer if data has been loaded properly, nullptr 74 * otherwise 75 */ 76 static std::shared_ptr<UserSession> fromJson( 77 const nlohmann::json::object_t& j) 78 { 79 std::shared_ptr<UserSession> userSession = 80 std::make_shared<UserSession>(); 81 for (const auto& element : j) 82 { 83 const std::string* thisValue = 84 element.second.get_ptr<const std::string*>(); 85 if (thisValue == nullptr) 86 { 87 BMCWEB_LOG_ERROR( 88 "Error reading persistent store. Property {} was not of type string", 89 element.first); 90 continue; 91 } 92 if (element.first == "unique_id") 93 { 94 userSession->uniqueId = *thisValue; 95 } 96 else if (element.first == "session_token") 97 { 98 userSession->sessionToken = *thisValue; 99 } 100 else if (element.first == "csrf_token") 101 { 102 userSession->csrfToken = *thisValue; 103 } 104 else if (element.first == "username") 105 { 106 userSession->username = *thisValue; 107 } 108 else if (element.first == "client_id") 109 { 110 userSession->clientId = *thisValue; 111 } 112 else if (element.first == "client_ip") 113 { 114 userSession->clientIp = *thisValue; 115 } 116 117 else 118 { 119 BMCWEB_LOG_ERROR( 120 "Got unexpected property reading persistent file: {}", 121 element.first); 122 continue; 123 } 124 } 125 // If any of these fields are missing, we can't restore the session, as 126 // we don't have enough information. These 4 fields have been present 127 // in every version of this file in bmcwebs history, so any file, even 128 // on upgrade, should have these present 129 if (userSession->uniqueId.empty() || userSession->username.empty() || 130 userSession->sessionToken.empty() || userSession->csrfToken.empty()) 131 { 132 BMCWEB_LOG_DEBUG("Session missing required security " 133 "information, refusing to restore"); 134 return nullptr; 135 } 136 137 // For now, sessions that were persisted through a reboot get their idle 138 // timer reset. This could probably be overcome with a better 139 // understanding of wall clock time and steady timer time, possibly 140 // persisting values with wall clock time instead of steady timer, but 141 // the tradeoffs of all the corner cases involved are non-trivial, so 142 // this is done temporarily 143 userSession->lastUpdated = std::chrono::steady_clock::now(); 144 userSession->sessionType = SessionType::Session; 145 146 return userSession; 147 } 148 }; 149 150 enum class MTLSCommonNameParseMode 151 { 152 Invalid = 0, 153 // This section approximately matches Redfish AccountService 154 // CertificateMappingAttribute, plus bmcweb defined OEM ones. 155 // Note, IDs in this enum must be maintained between versions, as they are 156 // persisted to disk 157 Whole = 1, 158 CommonName = 2, 159 UserPrincipalName = 3, 160 161 // Intentional gap for future DMTF-defined enums 162 163 // OEM parsing modes for various OEMs 164 }; 165 166 inline MTLSCommonNameParseMode getMTLSCommonNameParseMode(std::string_view name) 167 { 168 if (name == "CommonName") 169 { 170 return MTLSCommonNameParseMode::CommonName; 171 } 172 if (name == "Whole") 173 { 174 // Not yet supported 175 // return MTLSCommonNameParseMode::Whole; 176 } 177 if (name == "UserPrincipalName") 178 { 179 return MTLSCommonNameParseMode::UserPrincipalName; 180 } 181 return MTLSCommonNameParseMode::Invalid; 182 } 183 184 struct AuthConfigMethods 185 { 186 // Authentication paths 187 bool basic = BMCWEB_BASIC_AUTH; 188 bool sessionToken = BMCWEB_SESSION_AUTH; 189 bool xtoken = BMCWEB_XTOKEN_AUTH; 190 bool cookie = BMCWEB_COOKIE_AUTH; 191 bool tls = BMCWEB_MUTUAL_TLS_AUTH; 192 193 // Whether or not unauthenticated TLS should be accepted 194 // true = reject connections if mutual tls is not provided 195 // false = allow connection, and allow user to use other auth method 196 // Always default to false, because root certificates will not 197 // be provisioned at startup 198 bool tlsStrict = false; 199 200 MTLSCommonNameParseMode mTLSCommonNameParsingMode = 201 getMTLSCommonNameParseMode( 202 BMCWEB_MUTUAL_TLS_COMMON_NAME_PARSING_DEFAULT); 203 204 void fromJson(const nlohmann::json::object_t& j) 205 { 206 for (const auto& element : j) 207 { 208 const bool* value = element.second.get_ptr<const bool*>(); 209 if (value != nullptr) 210 { 211 if (element.first == "XToken") 212 { 213 xtoken = *value; 214 } 215 else if (element.first == "Cookie") 216 { 217 cookie = *value; 218 } 219 else if (element.first == "SessionToken") 220 { 221 sessionToken = *value; 222 } 223 else if (element.first == "BasicAuth") 224 { 225 basic = *value; 226 } 227 else if (element.first == "TLS") 228 { 229 tls = *value; 230 } 231 else if (element.first == "TLSStrict") 232 { 233 tlsStrict = *value; 234 } 235 } 236 const uint64_t* intValue = 237 element.second.get_ptr<const uint64_t*>(); 238 if (intValue != nullptr) 239 { 240 if (element.first == "MTLSCommonNameParseMode") 241 { 242 MTLSCommonNameParseMode tmpMTLSCommonNameParseMode = 243 static_cast<MTLSCommonNameParseMode>(*intValue); 244 if (tmpMTLSCommonNameParseMode <= 245 MTLSCommonNameParseMode::UserPrincipalName) 246 { 247 mTLSCommonNameParsingMode = tmpMTLSCommonNameParseMode; 248 } 249 else 250 { 251 BMCWEB_LOG_WARNING( 252 "Json value of {} was out of range of the enum. Ignoring", 253 *intValue); 254 } 255 } 256 } 257 } 258 } 259 }; 260 261 class SessionStore 262 { 263 public: 264 std::shared_ptr<UserSession> generateUserSession( 265 std::string_view username, const boost::asio::ip::address& clientIp, 266 const std::optional<std::string>& clientId, SessionType sessionType, 267 bool isConfigureSelfOnly = false) 268 { 269 // Only need csrf tokens for cookie based auth, token doesn't matter 270 std::string sessionToken = 271 bmcweb::getRandomIdOfLength(sessionTokenSize); 272 std::string csrfToken = bmcweb::getRandomIdOfLength(sessionTokenSize); 273 std::string uniqueId = bmcweb::getRandomIdOfLength(10); 274 275 // 276 if (sessionToken.empty() || csrfToken.empty() || uniqueId.empty()) 277 { 278 BMCWEB_LOG_ERROR("Failed to generate session tokens"); 279 return nullptr; 280 } 281 282 auto session = std::make_shared<UserSession>(UserSession{ 283 uniqueId, 284 sessionToken, 285 std::string(username), 286 csrfToken, 287 clientId, 288 redfish::ip_util::toString(clientIp), 289 std::chrono::steady_clock::now(), 290 sessionType, 291 false, 292 isConfigureSelfOnly, 293 "", 294 {}}); 295 auto it = authTokens.emplace(sessionToken, session); 296 // Only need to write to disk if session isn't about to be destroyed. 297 needWrite = sessionType != SessionType::Basic && 298 sessionType != SessionType::MutualTLS; 299 return it.first->second; 300 } 301 302 std::shared_ptr<UserSession> loginSessionByToken(std::string_view token) 303 { 304 applySessionTimeouts(); 305 if (token.size() != sessionTokenSize) 306 { 307 return nullptr; 308 } 309 auto sessionIt = authTokens.find(std::string(token)); 310 if (sessionIt == authTokens.end()) 311 { 312 return nullptr; 313 } 314 std::shared_ptr<UserSession> userSession = sessionIt->second; 315 userSession->lastUpdated = std::chrono::steady_clock::now(); 316 return userSession; 317 } 318 319 std::shared_ptr<UserSession> getSessionByUid(std::string_view uid) 320 { 321 applySessionTimeouts(); 322 // TODO(Ed) this is inefficient 323 auto sessionIt = authTokens.begin(); 324 while (sessionIt != authTokens.end()) 325 { 326 if (sessionIt->second->uniqueId == uid) 327 { 328 return sessionIt->second; 329 } 330 sessionIt++; 331 } 332 return nullptr; 333 } 334 335 void removeSession(const std::shared_ptr<UserSession>& session) 336 { 337 authTokens.erase(session->sessionToken); 338 needWrite = true; 339 } 340 341 std::vector<std::string> getAllUniqueIds() 342 { 343 applySessionTimeouts(); 344 std::vector<std::string> ret; 345 ret.reserve(authTokens.size()); 346 for (auto& session : authTokens) 347 { 348 ret.push_back(session.second->uniqueId); 349 } 350 return ret; 351 } 352 353 std::vector<std::string> getUniqueIdsBySessionType(SessionType type) 354 { 355 applySessionTimeouts(); 356 357 std::vector<std::string> ret; 358 ret.reserve(authTokens.size()); 359 for (auto& session : authTokens) 360 { 361 if (type == session.second->sessionType) 362 { 363 ret.push_back(session.second->uniqueId); 364 } 365 } 366 return ret; 367 } 368 369 std::vector<std::shared_ptr<UserSession>> getSessions() 370 { 371 std::vector<std::shared_ptr<UserSession>> sessions; 372 sessions.reserve(authTokens.size()); 373 for (auto& session : authTokens) 374 { 375 sessions.push_back(session.second); 376 } 377 return sessions; 378 } 379 380 void removeSessionsByUsername(std::string_view username) 381 { 382 std::erase_if(authTokens, [username](const auto& value) { 383 if (value.second == nullptr) 384 { 385 return false; 386 } 387 return value.second->username == username; 388 }); 389 } 390 391 void removeSessionsByUsernameExceptSession( 392 std::string_view username, const std::shared_ptr<UserSession>& session) 393 { 394 std::erase_if(authTokens, [username, session](const auto& value) { 395 if (value.second == nullptr) 396 { 397 return false; 398 } 399 400 return value.second->username == username && 401 value.second->uniqueId != session->uniqueId; 402 }); 403 } 404 405 void updateAuthMethodsConfig(const AuthConfigMethods& config) 406 { 407 bool isTLSchanged = (authMethodsConfig.tls != config.tls); 408 authMethodsConfig = config; 409 needWrite = true; 410 if (isTLSchanged) 411 { 412 // recreate socket connections with new settings 413 // NOLINTNEXTLINE(misc-include-cleaner) 414 std::raise(SIGHUP); 415 } 416 } 417 418 AuthConfigMethods& getAuthMethodsConfig() 419 { 420 return authMethodsConfig; 421 } 422 423 bool needsWrite() const 424 { 425 return needWrite; 426 } 427 int64_t getTimeoutInSeconds() const 428 { 429 return std::chrono::seconds(timeoutInSeconds).count(); 430 } 431 432 void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds) 433 { 434 timeoutInSeconds = newTimeoutInSeconds; 435 needWrite = true; 436 } 437 438 static SessionStore& getInstance() 439 { 440 static SessionStore sessionStore; 441 return sessionStore; 442 } 443 444 void applySessionTimeouts() 445 { 446 auto timeNow = std::chrono::steady_clock::now(); 447 if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1)) 448 { 449 lastTimeoutUpdate = timeNow; 450 auto authTokensIt = authTokens.begin(); 451 while (authTokensIt != authTokens.end()) 452 { 453 if (timeNow - authTokensIt->second->lastUpdated >= 454 timeoutInSeconds) 455 { 456 authTokensIt = authTokens.erase(authTokensIt); 457 458 needWrite = true; 459 } 460 else 461 { 462 authTokensIt++; 463 } 464 } 465 } 466 } 467 468 SessionStore(const SessionStore&) = delete; 469 SessionStore& operator=(const SessionStore&) = delete; 470 SessionStore(SessionStore&&) = delete; 471 SessionStore& operator=(const SessionStore&&) = delete; 472 ~SessionStore() = default; 473 474 std::unordered_map<std::string, std::shared_ptr<UserSession>, 475 std::hash<std::string>, bmcweb::ConstantTimeCompare> 476 authTokens; 477 478 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate; 479 bool needWrite{false}; 480 std::chrono::seconds timeoutInSeconds; 481 AuthConfigMethods authMethodsConfig; 482 483 private: 484 SessionStore() : timeoutInSeconds(1800) {} 485 }; 486 487 } // namespace persistent_data 488