xref: /openbmc/bmcweb/include/sessions.hpp (revision 09b9d45e)
1 #pragma once
2 
3 #include <boost/container/flat_map.hpp>
4 #include <boost/uuid/uuid.hpp>
5 #include <boost/uuid/uuid_generators.hpp>
6 #include <boost/uuid/uuid_io.hpp>
7 #include <csignal>
8 #include <dbus_singleton.hpp>
9 #include <nlohmann/json.hpp>
10 #include <pam_authenticate.hpp>
11 #include <random>
12 #include <sdbusplus/bus/match.hpp>
13 
14 #include "logging.h"
15 #include "utility.h"
16 
17 namespace crow
18 {
19 
20 namespace persistent_data
21 {
22 
23 // entropy: 20 characters, 62 possibilities.  log2(62^20) = 119 bits of
24 // entropy.  OWASP recommends at least 64
25 // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
26 constexpr std::size_t sessionTokenSize = 20;
27 
28 enum class PersistenceType
29 {
30     TIMEOUT, // User session times out after a predetermined amount of time
31     SINGLE_REQUEST // User times out once this request is completed.
32 };
33 
34 struct UserSession
35 {
36     std::string uniqueId;
37     std::string sessionToken;
38     std::string username;
39     std::string csrfToken;
40     std::chrono::time_point<std::chrono::steady_clock> lastUpdated;
41     PersistenceType persistence;
42 
43     /**
44      * @brief Fills object with data from UserSession's JSON representation
45      *
46      * This replaces nlohmann's from_json to ensure no-throw approach
47      *
48      * @param[in] j   JSON object from which data should be loaded
49      *
50      * @return a shared pointer if data has been loaded properly, nullptr
51      * otherwise
52      */
53     static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j)
54     {
55         std::shared_ptr<UserSession> userSession =
56             std::make_shared<UserSession>();
57         for (const auto& element : j.items())
58         {
59             const std::string* thisValue =
60                 element.value().get_ptr<const std::string*>();
61             if (thisValue == nullptr)
62             {
63                 BMCWEB_LOG_ERROR << "Error reading persistent store.  Property "
64                                  << element.key() << " was not of type string";
65                 return nullptr;
66             }
67             if (element.key() == "unique_id")
68             {
69                 userSession->uniqueId = *thisValue;
70             }
71             else if (element.key() == "session_token")
72             {
73                 userSession->sessionToken = *thisValue;
74             }
75             else if (element.key() == "csrf_token")
76             {
77                 userSession->csrfToken = *thisValue;
78             }
79             else if (element.key() == "username")
80             {
81                 userSession->username = *thisValue;
82             }
83             else
84             {
85                 BMCWEB_LOG_ERROR
86                     << "Got unexpected property reading persistent file: "
87                     << element.key();
88                 return nullptr;
89             }
90         }
91 
92         // For now, sessions that were persisted through a reboot get their idle
93         // timer reset.  This could probably be overcome with a better
94         // understanding of wall clock time and steady timer time, possibly
95         // persisting values with wall clock time instead of steady timer, but
96         // the tradeoffs of all the corner cases involved are non-trivial, so
97         // this is done temporarily
98         userSession->lastUpdated = std::chrono::steady_clock::now();
99         userSession->persistence = PersistenceType::TIMEOUT;
100 
101         return userSession;
102     }
103 };
104 
105 struct AuthConfigMethods
106 {
107     bool xtoken = true;
108     bool cookie = true;
109     bool sessionToken = true;
110     bool basic = true;
111     bool tls = false;
112 
113     void fromJson(const nlohmann::json& j)
114     {
115         for (const auto& element : j.items())
116         {
117             const bool* value = element.value().get_ptr<const bool*>();
118             if (value == nullptr)
119             {
120                 continue;
121             }
122 
123             if (element.key() == "XToken")
124             {
125                 xtoken = *value;
126             }
127             else if (element.key() == "Cookie")
128             {
129                 cookie = *value;
130             }
131             else if (element.key() == "SessionToken")
132             {
133                 sessionToken = *value;
134             }
135             else if (element.key() == "BasicAuth")
136             {
137                 basic = *value;
138             }
139             else if (element.key() == "TLS")
140             {
141                 tls = *value;
142             }
143         }
144     }
145 };
146 
147 class Middleware;
148 
149 class SessionStore
150 {
151   public:
152     std::shared_ptr<UserSession> generateUserSession(
153         const std::string_view username,
154         PersistenceType persistence = PersistenceType::TIMEOUT)
155     {
156         // TODO(ed) find a secure way to not generate session identifiers if
157         // persistence is set to SINGLE_REQUEST
158         static constexpr std::array<char, 62> alphanum = {
159             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
160             'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
161             'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
162             'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
163             'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
164 
165         std::string sessionToken;
166         sessionToken.resize(sessionTokenSize, '0');
167         std::uniform_int_distribution<size_t> dist(0, alphanum.size() - 1);
168         for (size_t i = 0; i < sessionToken.size(); ++i)
169         {
170             sessionToken[i] = alphanum[dist(rd)];
171         }
172         // Only need csrf tokens for cookie based auth, token doesn't matter
173         std::string csrfToken;
174         csrfToken.resize(sessionTokenSize, '0');
175         for (size_t i = 0; i < csrfToken.size(); ++i)
176         {
177             csrfToken[i] = alphanum[dist(rd)];
178         }
179 
180         std::string uniqueId;
181         uniqueId.resize(10, '0');
182         for (size_t i = 0; i < uniqueId.size(); ++i)
183         {
184             uniqueId[i] = alphanum[dist(rd)];
185         }
186 
187         auto session = std::make_shared<UserSession>(UserSession{
188             uniqueId, sessionToken, std::string(username), csrfToken,
189             std::chrono::steady_clock::now(), persistence});
190         auto it = authTokens.emplace(std::make_pair(sessionToken, session));
191         // Only need to write to disk if session isn't about to be destroyed.
192         needWrite = persistence == PersistenceType::TIMEOUT;
193         return it.first->second;
194     }
195 
196     std::shared_ptr<UserSession>
197         loginSessionByToken(const std::string_view token)
198     {
199         applySessionTimeouts();
200         if (token.size() != sessionTokenSize)
201         {
202             return nullptr;
203         }
204         auto sessionIt = authTokens.find(std::string(token));
205         if (sessionIt == authTokens.end())
206         {
207             return nullptr;
208         }
209         std::shared_ptr<UserSession> userSession = sessionIt->second;
210         userSession->lastUpdated = std::chrono::steady_clock::now();
211         return userSession;
212     }
213 
214     std::shared_ptr<UserSession> getSessionByUid(const std::string_view uid)
215     {
216         applySessionTimeouts();
217         // TODO(Ed) this is inefficient
218         auto sessionIt = authTokens.begin();
219         while (sessionIt != authTokens.end())
220         {
221             if (sessionIt->second->uniqueId == uid)
222             {
223                 return sessionIt->second;
224             }
225             sessionIt++;
226         }
227         return nullptr;
228     }
229 
230     void removeSession(std::shared_ptr<UserSession> session)
231     {
232         authTokens.erase(session->sessionToken);
233         needWrite = true;
234     }
235 
236     std::vector<const std::string*> getUniqueIds(
237         bool getAll = true,
238         const PersistenceType& type = PersistenceType::SINGLE_REQUEST)
239     {
240         applySessionTimeouts();
241 
242         std::vector<const std::string*> ret;
243         ret.reserve(authTokens.size());
244         for (auto& session : authTokens)
245         {
246             if (getAll || type == session.second->persistence)
247             {
248                 ret.push_back(&session.second->uniqueId);
249             }
250         }
251         return ret;
252     }
253 
254     void updateAuthMethodsConfig(const AuthConfigMethods& config)
255     {
256         bool isTLSchanged = (authMethodsConfig.tls != config.tls);
257         authMethodsConfig = config;
258         needWrite = true;
259         if (isTLSchanged)
260         {
261             // recreate socket connections with new settings
262             std::raise(SIGHUP);
263         }
264     }
265 
266     AuthConfigMethods& getAuthMethodsConfig()
267     {
268         return authMethodsConfig;
269     }
270 
271     bool needsWrite()
272     {
273         return needWrite;
274     }
275     int64_t getTimeoutInSeconds() const
276     {
277         return std::chrono::seconds(timeoutInMinutes).count();
278     };
279 
280     // Persistent data middleware needs to be able to serialize our authTokens
281     // structure, which is private
282     friend Middleware;
283 
284     static SessionStore& getInstance()
285     {
286         static SessionStore sessionStore;
287         return sessionStore;
288     }
289 
290     SessionStore(const SessionStore&) = delete;
291     SessionStore& operator=(const SessionStore&) = delete;
292 
293   private:
294     SessionStore() : timeoutInMinutes(60)
295     {
296     }
297 
298     void applySessionTimeouts()
299     {
300         auto timeNow = std::chrono::steady_clock::now();
301         if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1))
302         {
303             lastTimeoutUpdate = timeNow;
304             auto authTokensIt = authTokens.begin();
305             while (authTokensIt != authTokens.end())
306             {
307                 if (timeNow - authTokensIt->second->lastUpdated >=
308                     timeoutInMinutes)
309                 {
310                     authTokensIt = authTokens.erase(authTokensIt);
311                     needWrite = true;
312                 }
313                 else
314                 {
315                     authTokensIt++;
316                 }
317             }
318         }
319     }
320 
321     std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
322     std::unordered_map<std::string, std::shared_ptr<UserSession>,
323                        std::hash<std::string>,
324                        crow::utility::ConstantTimeCompare>
325         authTokens;
326     std::random_device rd;
327     bool needWrite{false};
328     std::chrono::minutes timeoutInMinutes;
329     AuthConfigMethods authMethodsConfig;
330 };
331 
332 } // namespace persistent_data
333 } // namespace crow
334 
335 // to_json(...) definition for objects of UserSession type
336 namespace nlohmann
337 {
338 template <>
339 struct adl_serializer<std::shared_ptr<crow::persistent_data::UserSession>>
340 {
341     static void
342         to_json(nlohmann::json& j,
343                 const std::shared_ptr<crow::persistent_data::UserSession>& p)
344     {
345         if (p->persistence !=
346             crow::persistent_data::PersistenceType::SINGLE_REQUEST)
347         {
348             j = nlohmann::json{{"unique_id", p->uniqueId},
349                                {"session_token", p->sessionToken},
350                                {"username", p->username},
351                                {"csrf_token", p->csrfToken}};
352         }
353     }
354 };
355 
356 template <> struct adl_serializer<crow::persistent_data::AuthConfigMethods>
357 {
358     static void to_json(nlohmann::json& j,
359                         const crow::persistent_data::AuthConfigMethods& c)
360     {
361         j = nlohmann::json{{"XToken", c.xtoken},
362                            {"Cookie", c.cookie},
363                            {"SessionToken", c.sessionToken},
364                            {"BasicAuth", c.basic},
365                            {"TLS", c.tls}};
366     }
367 };
368 } // namespace nlohmann
369