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