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 PersistentData { 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 unique_id; 26 std::string session_token; 27 std::string username; 28 std::string csrf_token; 29 std::chrono::time_point<std::chrono::steady_clock> last_updated; 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 true if data has been loaded properly, false otherwise 40 */ 41 bool fromJson(const nlohmann::json& j) { 42 auto jUid = j.find("unique_id"); 43 auto jToken = j.find("session_token"); 44 auto jUsername = j.find("username"); 45 auto jCsrf = j.find("csrf_token"); 46 47 // Verify existence 48 if (jUid == j.end() || jToken == j.end() || jUsername == j.end() || 49 jCsrf == j.end()) { 50 return false; 51 } 52 53 // Verify types 54 if (!jUid->is_string() || !jToken->is_string() || !jUsername->is_string() || 55 !jCsrf->is_string()) { 56 return false; 57 } 58 59 unique_id = jUid->get<std::string>(); 60 session_token = jToken->get<std::string>(); 61 username = jUsername->get<std::string>(); 62 csrf_token = jCsrf->get<std::string>(); 63 64 // For now, sessions that were persisted through a reboot get their timer 65 // reset. This could probably be overcome with a better understanding of 66 // wall clock time and steady timer time, possibly persisting values with 67 // wall clock time instead of steady timer, but the tradeoffs of all the 68 // corner cases involved are non-trivial, so this is done temporarily 69 last_updated = std::chrono::steady_clock::now(); 70 persistence = PersistenceType::TIMEOUT; 71 72 return true; 73 } 74 }; 75 76 void to_json(nlohmann::json& j, const UserSession& p) { 77 if (p.persistence != PersistenceType::SINGLE_REQUEST) { 78 j = nlohmann::json{{"unique_id", p.unique_id}, 79 {"session_token", p.session_token}, 80 {"username", p.username}, 81 {"csrf_token", p.csrf_token}}; 82 } 83 } 84 85 class Middleware; 86 87 class SessionStore { 88 public: 89 SessionStore() : timeout_in_minutes(60) {} 90 const UserSession& generate_user_session( 91 const std::string& username, 92 PersistenceType persistence = PersistenceType::TIMEOUT) { 93 // TODO(ed) find a secure way to not generate session identifiers if 94 // persistence is set to SINGLE_REQUEST 95 static constexpr std::array<char, 62> alphanum = { 96 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 97 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 98 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 99 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 100 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; 101 102 // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of 103 // entropy. OWASP recommends at least 60 104 // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy 105 std::string session_token; 106 session_token.resize(20, '0'); 107 std::uniform_int_distribution<int> dist(0, alphanum.size() - 1); 108 for (int i = 0; i < session_token.size(); ++i) { 109 session_token[i] = alphanum[dist(rd)]; 110 } 111 // Only need csrf tokens for cookie based auth, token doesn't matter 112 std::string csrf_token; 113 csrf_token.resize(20, '0'); 114 for (int i = 0; i < csrf_token.size(); ++i) { 115 csrf_token[i] = alphanum[dist(rd)]; 116 } 117 118 std::string unique_id; 119 unique_id.resize(10, '0'); 120 for (int i = 0; i < unique_id.size(); ++i) { 121 unique_id[i] = alphanum[dist(rd)]; 122 } 123 124 const auto session_it = auth_tokens.emplace( 125 session_token, 126 std::move(UserSession{unique_id, session_token, username, csrf_token, 127 std::chrono::steady_clock::now(), persistence})); 128 const UserSession& user = (session_it).first->second; 129 // Only need to write to disk if session isn't about to be destroyed. 130 need_write_ = persistence == PersistenceType::TIMEOUT; 131 return user; 132 } 133 134 const UserSession* login_session_by_token(const std::string& token) { 135 apply_session_timeouts(); 136 auto session_it = auth_tokens.find(token); 137 if (session_it == auth_tokens.end()) { 138 return nullptr; 139 } 140 UserSession& foo = session_it->second; 141 foo.last_updated = std::chrono::steady_clock::now(); 142 return &foo; 143 } 144 145 const UserSession* get_session_by_uid(const std::string& uid) { 146 apply_session_timeouts(); 147 // TODO(Ed) this is inefficient 148 auto session_it = auth_tokens.begin(); 149 while (session_it != auth_tokens.end()) { 150 if (session_it->second.unique_id == uid) { 151 return &session_it->second; 152 } 153 session_it++; 154 } 155 return nullptr; 156 } 157 158 void remove_session(const UserSession* session) { 159 auth_tokens.erase(session->session_token); 160 need_write_ = true; 161 } 162 163 std::vector<const std::string*> get_unique_ids( 164 bool getAll = true, 165 const PersistenceType& type = PersistenceType::SINGLE_REQUEST) { 166 apply_session_timeouts(); 167 168 std::vector<const std::string*> ret; 169 ret.reserve(auth_tokens.size()); 170 for (auto& session : auth_tokens) { 171 if (getAll || type == session.second.persistence) { 172 ret.push_back(&session.second.unique_id); 173 } 174 } 175 return ret; 176 } 177 178 bool needs_write() { return need_write_; } 179 int get_timeout_in_seconds() const { 180 return std::chrono::seconds(timeout_in_minutes).count(); 181 }; 182 183 // Persistent data middleware needs to be able to serialize our auth_tokens 184 // structure, which is private 185 friend Middleware; 186 187 private: 188 void apply_session_timeouts() { 189 auto time_now = std::chrono::steady_clock::now(); 190 if (time_now - last_timeout_update > std::chrono::minutes(1)) { 191 last_timeout_update = time_now; 192 auto auth_tokens_it = auth_tokens.begin(); 193 while (auth_tokens_it != auth_tokens.end()) { 194 if (time_now - auth_tokens_it->second.last_updated >= 195 timeout_in_minutes) { 196 auth_tokens_it = auth_tokens.erase(auth_tokens_it); 197 need_write_ = true; 198 } else { 199 auth_tokens_it++; 200 } 201 } 202 } 203 } 204 std::chrono::time_point<std::chrono::steady_clock> last_timeout_update; 205 boost::container::flat_map<std::string, UserSession> auth_tokens; 206 std::random_device rd; 207 bool need_write_{false}; 208 std::chrono::minutes timeout_in_minutes; 209 }; 210 211 } // namespaec PersistentData 212 } // namespace crow 213