blob: b4e86c721cb300c18cc469ccce60b2ef4460d93c [file] [log] [blame]
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +01001#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
15namespace crow {
16
17namespace PersistentData {
18
19enum 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
24struct 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;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +010031
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 *
Ed Tanouse1ae5332018-07-09 15:21:27 -070039 * @return a shared pointer if data has been loaded properly, nullptr otherwise
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +010040 */
Ed Tanouse1ae5332018-07-09 15:21:27 -070041 static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j) {
42 std::shared_ptr<UserSession> userSession = std::make_shared<UserSession>();
43 for (const auto& element : j.items()) {
44 const std::string* thisValue =
45 element.value().get_ptr<const std::string*>();
46 if (thisValue == nullptr) {
47 CROW_LOG_ERROR << "Error reading persistent store. Property "
48 << element.key() << " was not of type string";
49 return nullptr;
50 }
51 if (element.key() == "unique_id") {
52 userSession->unique_id = *thisValue;
53 } else if (element.key() == "session_token") {
54 userSession->session_token = *thisValue;
55 } else if (element.key() == "csrf_token") {
56 userSession->csrf_token = *thisValue;
57 } else if (element.key() == "username") {
58 userSession->username = *thisValue;
59 } else {
60 CROW_LOG_ERROR << "Got unexpected property reading persistent file: "
61 << element.key();
62 return nullptr;
63 }
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +010064 }
65
Ed Tanouse1ae5332018-07-09 15:21:27 -070066 // For now, sessions that were persisted through a reboot get their idle
67 // timer reset. This could probably be overcome with a better understanding
68 // of wall clock time and steady timer time, possibly persisting values with
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +010069 // wall clock time instead of steady timer, but the tradeoffs of all the
70 // corner cases involved are non-trivial, so this is done temporarily
Ed Tanouse1ae5332018-07-09 15:21:27 -070071 userSession->last_updated = std::chrono::steady_clock::now();
72 userSession->persistence = PersistenceType::TIMEOUT;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +010073
Ed Tanouse1ae5332018-07-09 15:21:27 -070074 return userSession;
Kowalski, Kamil5cef0f72018-02-15 15:26:51 +010075 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010076};
77
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010078class Middleware;
79
80class SessionStore {
81 public:
Ed Tanouse0d918b2018-03-27 17:41:04 -070082 std::shared_ptr<UserSession> generate_user_session(
83 const boost::string_view username,
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010084 PersistenceType persistence = PersistenceType::TIMEOUT) {
85 // TODO(ed) find a secure way to not generate session identifiers if
86 // persistence is set to SINGLE_REQUEST
87 static constexpr std::array<char, 62> alphanum = {
88 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
89 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
90 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
91 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
92 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
93
94 // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of
95 // entropy. OWASP recommends at least 60
96 // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy
97 std::string session_token;
98 session_token.resize(20, '0');
99 std::uniform_int_distribution<int> dist(0, alphanum.size() - 1);
100 for (int i = 0; i < session_token.size(); ++i) {
101 session_token[i] = alphanum[dist(rd)];
102 }
103 // Only need csrf tokens for cookie based auth, token doesn't matter
104 std::string csrf_token;
105 csrf_token.resize(20, '0');
106 for (int i = 0; i < csrf_token.size(); ++i) {
107 csrf_token[i] = alphanum[dist(rd)];
108 }
109
110 std::string unique_id;
111 unique_id.resize(10, '0');
112 for (int i = 0; i < unique_id.size(); ++i) {
113 unique_id[i] = alphanum[dist(rd)];
114 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700115 auto session = std::make_shared<UserSession>(
116 UserSession{unique_id, session_token, std::string(username), csrf_token,
117 std::chrono::steady_clock::now(), persistence});
118 auto it = auth_tokens.emplace(std::make_pair(session_token, session));
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100119 // Only need to write to disk if session isn't about to be destroyed.
120 need_write_ = persistence == PersistenceType::TIMEOUT;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700121 return it.first->second;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100122 }
123
Ed Tanouse0d918b2018-03-27 17:41:04 -0700124 std::shared_ptr<UserSession> login_session_by_token(
125 const boost::string_view token) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100126 apply_session_timeouts();
Ed Tanouse0d918b2018-03-27 17:41:04 -0700127 auto session_it = auth_tokens.find(std::string(token));
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100128 if (session_it == auth_tokens.end()) {
129 return nullptr;
130 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700131 std::shared_ptr<UserSession> user_session = session_it->second;
132 user_session->last_updated = std::chrono::steady_clock::now();
133 return user_session;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100134 }
135
Ed Tanouse0d918b2018-03-27 17:41:04 -0700136 std::shared_ptr<UserSession> get_session_by_uid(
137 const boost::string_view uid) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100138 apply_session_timeouts();
139 // TODO(Ed) this is inefficient
140 auto session_it = auth_tokens.begin();
141 while (session_it != auth_tokens.end()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700142 if (session_it->second->unique_id == uid) {
143 return session_it->second;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100144 }
145 session_it++;
146 }
147 return nullptr;
148 }
149
Ed Tanouse0d918b2018-03-27 17:41:04 -0700150 void remove_session(std::shared_ptr<UserSession> session) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100151 auth_tokens.erase(session->session_token);
152 need_write_ = true;
153 }
154
155 std::vector<const std::string*> get_unique_ids(
156 bool getAll = true,
157 const PersistenceType& type = PersistenceType::SINGLE_REQUEST) {
158 apply_session_timeouts();
159
160 std::vector<const std::string*> ret;
161 ret.reserve(auth_tokens.size());
162 for (auto& session : auth_tokens) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700163 if (getAll || type == session.second->persistence) {
164 ret.push_back(&session.second->unique_id);
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100165 }
166 }
167 return ret;
168 }
169
170 bool needs_write() { return need_write_; }
Borawski.Lukasz5d27b852018-02-08 13:24:24 +0100171 int get_timeout_in_seconds() const {
172 return std::chrono::seconds(timeout_in_minutes).count();
173 };
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100174
175 // Persistent data middleware needs to be able to serialize our auth_tokens
176 // structure, which is private
177 friend Middleware;
178
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200179 static SessionStore& getInstance() {
180 static SessionStore sessionStore;
181 return sessionStore;
182 }
183
184 SessionStore(const SessionStore&) = delete;
185 SessionStore& operator=(const SessionStore&) = delete;
186
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100187 private:
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200188 SessionStore() : timeout_in_minutes(60) {}
189
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100190 void apply_session_timeouts() {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100191 auto time_now = std::chrono::steady_clock::now();
192 if (time_now - last_timeout_update > std::chrono::minutes(1)) {
193 last_timeout_update = time_now;
194 auto auth_tokens_it = auth_tokens.begin();
195 while (auth_tokens_it != auth_tokens.end()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700196 if (time_now - auth_tokens_it->second->last_updated >=
Borawski.Lukasz5d27b852018-02-08 13:24:24 +0100197 timeout_in_minutes) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100198 auth_tokens_it = auth_tokens.erase(auth_tokens_it);
199 need_write_ = true;
200 } else {
201 auth_tokens_it++;
202 }
203 }
204 }
205 }
206 std::chrono::time_point<std::chrono::steady_clock> last_timeout_update;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700207 boost::container::flat_map<std::string, std::shared_ptr<UserSession>>
208 auth_tokens;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100209 std::random_device rd;
210 bool need_write_{false};
Borawski.Lukasz5d27b852018-02-08 13:24:24 +0100211 std::chrono::minutes timeout_in_minutes;
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100212};
213
Ed Tanouse0d918b2018-03-27 17:41:04 -0700214} // namespace PersistentData
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100215} // namespace crow
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200216
217// to_json(...) definition for objects of UserSession type
218namespace nlohmann {
219template <>
220struct adl_serializer<std::shared_ptr<crow::PersistentData::UserSession>> {
221 static void to_json(
222 nlohmann::json& j,
223 const std::shared_ptr<crow::PersistentData::UserSession>& p) {
224 if (p->persistence !=
225 crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
226 j = nlohmann::json{{"unique_id", p->unique_id},
227 {"session_token", p->session_token},
228 {"username", p->username},
229 {"csrf_token", p->csrf_token}};
230 }
231 }
232};
233} // namespace nlohmann