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