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