xref: /openbmc/bmcweb/include/sessions.hpp (revision 4859bdbafbb2b78ae414fb050ad197db2f2cb28b)
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