1 #pragma once 2 3 #include <nlohmann/json.hpp> 4 #include <pam_authenticate.hpp> 5 #include <webassets.hpp> 6 #include <random> 7 #include <crow/app.h> 8 #include <crow/http_request.h> 9 #include <crow/http_response.h> 10 #include <boost/container/flat_map.hpp> 11 #include <boost/uuid/uuid.hpp> 12 #include <boost/uuid/uuid_generators.hpp> 13 #include <boost/uuid/uuid_io.hpp> 14 15 namespace crow { 16 17 namespace persistent_data { 18 19 enum class PersistenceType { 20 TIMEOUT, // User session times out after a predetermined amount of time 21 SINGLE_REQUEST // User times out once this request is completed. 22 }; 23 24 struct UserSession { 25 std::string uniqueId; 26 std::string sessionToken; 27 std::string username; 28 std::string csrfToken; 29 std::chrono::time_point<std::chrono::steady_clock> lastUpdated; 30 PersistenceType persistence; 31 32 /** 33 * @brief Fills object with data from UserSession's JSON representation 34 * 35 * This replaces nlohmann's from_json to ensure no-throw approach 36 * 37 * @param[in] j JSON object from which data should be loaded 38 * 39 * @return a shared pointer if data has been loaded properly, nullptr 40 * otherwise 41 */ 42 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j) { 43 std::shared_ptr<UserSession> userSession = std::make_shared<UserSession>(); 44 for (const auto& element : j.items()) { 45 const std::string* thisValue = 46 element.value().get_ptr<const std::string*>(); 47 if (thisValue == nullptr) { 48 BMCWEB_LOG_ERROR << "Error reading persistent store. Property " 49 << element.key() << " was not of type string"; 50 return nullptr; 51 } 52 if (element.key() == "unique_id") { 53 userSession->uniqueId = *thisValue; 54 } else if (element.key() == "session_token") { 55 userSession->sessionToken = *thisValue; 56 } else if (element.key() == "csrf_token") { 57 userSession->csrfToken = *thisValue; 58 } else if (element.key() == "username") { 59 userSession->username = *thisValue; 60 } else { 61 BMCWEB_LOG_ERROR << "Got unexpected property reading persistent file: " 62 << element.key(); 63 return nullptr; 64 } 65 } 66 67 // For now, sessions that were persisted through a reboot get their idle 68 // timer reset. This could probably be overcome with a better understanding 69 // of wall clock time and steady timer time, possibly persisting values with 70 // wall clock time instead of steady timer, but the tradeoffs of all the 71 // corner cases involved are non-trivial, so this is done temporarily 72 userSession->lastUpdated = std::chrono::steady_clock::now(); 73 userSession->persistence = PersistenceType::TIMEOUT; 74 75 return userSession; 76 } 77 }; 78 79 class Middleware; 80 81 class SessionStore { 82 public: 83 std::shared_ptr<UserSession> generateUserSession( 84 const boost::string_view username, 85 PersistenceType persistence = PersistenceType::TIMEOUT) { 86 // TODO(ed) find a secure way to not generate session identifiers if 87 // persistence is set to SINGLE_REQUEST 88 static constexpr std::array<char, 62> alphanum = { 89 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'b', 'C', 90 'D', 'E', 'F', 'g', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 91 'Q', 'r', 'S', 'T', 'U', 'v', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 92 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 93 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; 94 95 // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of 96 // entropy. OWASP recommends at least 60 97 // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy 98 std::string sessionToken; 99 sessionToken.resize(20, '0'); 100 std::uniform_int_distribution<int> dist(0, alphanum.size() - 1); 101 for (int i = 0; i < sessionToken.size(); ++i) { 102 sessionToken[i] = alphanum[dist(rd)]; 103 } 104 // Only need csrf tokens for cookie based auth, token doesn't matter 105 std::string csrfToken; 106 csrfToken.resize(20, '0'); 107 for (int i = 0; i < csrfToken.size(); ++i) { 108 csrfToken[i] = alphanum[dist(rd)]; 109 } 110 111 std::string uniqueId; 112 uniqueId.resize(10, '0'); 113 for (int i = 0; i < uniqueId.size(); ++i) { 114 uniqueId[i] = alphanum[dist(rd)]; 115 } 116 auto session = std::make_shared<UserSession>( 117 UserSession{uniqueId, sessionToken, std::string(username), csrfToken, 118 std::chrono::steady_clock::now(), persistence}); 119 auto it = authTokens.emplace(std::make_pair(sessionToken, session)); 120 // Only need to write to disk if session isn't about to be destroyed. 121 needWrite = persistence == PersistenceType::TIMEOUT; 122 return it.first->second; 123 } 124 125 std::shared_ptr<UserSession> loginSessionByToken( 126 const boost::string_view token) { 127 applySessionTimeouts(); 128 auto sessionIt = authTokens.find(std::string(token)); 129 if (sessionIt == authTokens.end()) { 130 return nullptr; 131 } 132 std::shared_ptr<UserSession> userSession = sessionIt->second; 133 userSession->lastUpdated = std::chrono::steady_clock::now(); 134 return userSession; 135 } 136 137 std::shared_ptr<UserSession> getSessionByUid(const boost::string_view uid) { 138 applySessionTimeouts(); 139 // TODO(Ed) this is inefficient 140 auto sessionIt = authTokens.begin(); 141 while (sessionIt != authTokens.end()) { 142 if (sessionIt->second->uniqueId == uid) { 143 return sessionIt->second; 144 } 145 sessionIt++; 146 } 147 return nullptr; 148 } 149 150 void removeSession(std::shared_ptr<UserSession> session) { 151 authTokens.erase(session->sessionToken); 152 needWrite = true; 153 } 154 155 std::vector<const std::string*> getUniqueIds( 156 bool getAll = true, 157 const PersistenceType& type = PersistenceType::SINGLE_REQUEST) { 158 applySessionTimeouts(); 159 160 std::vector<const std::string*> ret; 161 ret.reserve(authTokens.size()); 162 for (auto& session : authTokens) { 163 if (getAll || type == session.second->persistence) { 164 ret.push_back(&session.second->uniqueId); 165 } 166 } 167 return ret; 168 } 169 170 bool needsWrite() { return needWrite; } 171 int getTimeoutInSeconds() const { 172 return std::chrono::seconds(timeoutInMinutes).count(); 173 }; 174 175 // Persistent data middleware needs to be able to serialize our authTokens 176 // structure, which is private 177 friend Middleware; 178 179 static SessionStore& getInstance() { 180 static SessionStore sessionStore; 181 return sessionStore; 182 } 183 184 SessionStore(const SessionStore&) = delete; 185 SessionStore& operator=(const SessionStore&) = delete; 186 187 private: 188 SessionStore() : timeoutInMinutes(60) {} 189 190 void applySessionTimeouts() { 191 auto timeNow = std::chrono::steady_clock::now(); 192 if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1)) { 193 lastTimeoutUpdate = timeNow; 194 auto authTokensIt = authTokens.begin(); 195 while (authTokensIt != authTokens.end()) { 196 if (timeNow - authTokensIt->second->lastUpdated >= timeoutInMinutes) { 197 authTokensIt = authTokens.erase(authTokensIt); 198 needWrite = true; 199 } else { 200 authTokensIt++; 201 } 202 } 203 } 204 } 205 std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate; 206 boost::container::flat_map<std::string, std::shared_ptr<UserSession>> 207 authTokens; 208 std::random_device rd; 209 bool needWrite{false}; 210 std::chrono::minutes timeoutInMinutes; 211 }; 212 213 } // namespace persistent_data 214 } // namespace crow 215 216 // to_json(...) definition for objects of UserSession type 217 namespace nlohmann { 218 template <> 219 struct adl_serializer<std::shared_ptr<crow::persistent_data::UserSession>> { 220 static void to_json( 221 nlohmann::json& j, 222 const std::shared_ptr<crow::persistent_data::UserSession>& p) { 223 if (p->persistence != 224 crow::persistent_data::PersistenceType::SINGLE_REQUEST) { 225 j = nlohmann::json{{"unique_id", p->uniqueId}, 226 {"session_token", p->sessionToken}, 227 {"username", p->username}, 228 {"csrf_token", p->csrfToken}}; 229 } 230 } 231 }; 232 } // namespace nlohmann 233