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