xref: /openbmc/bmcweb/include/sessions.hpp (revision ed76121b)
1 #pragma once
2 
3 #include "logging.hpp"
4 #include "random.hpp"
5 #include "utility.hpp"
6 
7 #include <nlohmann/json.hpp>
8 #include <utils/ip_utils.hpp>
9 
10 #include <csignal>
11 #include <random>
12 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
13 #include <ibm/locks.hpp>
14 #endif
15 
16 namespace persistent_data
17 {
18 
19 // entropy: 20 characters, 62 possibilities.  log2(62^20) = 119 bits of
20 // entropy.  OWASP recommends at least 64
21 // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
22 constexpr std::size_t sessionTokenSize = 20;
23 
24 enum class PersistenceType
25 {
26     TIMEOUT, // User session times out after a predetermined amount of time
27     SINGLE_REQUEST // User times out once this request is completed.
28 };
29 
30 struct UserSession
31 {
32     std::string uniqueId;
33     std::string sessionToken;
34     std::string username;
35     std::string csrfToken;
36     std::string clientId;
37     std::string clientIp;
38     std::chrono::time_point<std::chrono::steady_clock> lastUpdated;
39     PersistenceType persistence{PersistenceType::TIMEOUT};
40     bool cookieAuth = false;
41     bool isConfigureSelfOnly = false;
42 
43     // There are two sources of truth for isConfigureSelfOnly:
44     //  1. When pamAuthenticateUser() returns PAM_NEW_AUTHTOK_REQD.
45     //  2. D-Bus User.Manager.GetUserInfo property UserPasswordExpired.
46     // These should be in sync, but the underlying condition can change at any
47     // time.  For example, a password can expire or be changed outside of
48     // bmcweb.  The value stored here is updated at the start of each
49     // operation and used as the truth within bmcweb.
50 
51     /**
52      * @brief Fills object with data from UserSession's JSON representation
53      *
54      * This replaces nlohmann's from_json to ensure no-throw approach
55      *
56      * @param[in] j   JSON object from which data should be loaded
57      *
58      * @return a shared pointer if data has been loaded properly, nullptr
59      * otherwise
60      */
61     static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j)
62     {
63         std::shared_ptr<UserSession> userSession =
64             std::make_shared<UserSession>();
65         for (const auto& element : j.items())
66         {
67             const std::string* thisValue =
68                 element.value().get_ptr<const std::string*>();
69             if (thisValue == nullptr)
70             {
71                 BMCWEB_LOG_ERROR << "Error reading persistent store.  Property "
72                                  << element.key() << " was not of type string";
73                 continue;
74             }
75             if (element.key() == "unique_id")
76             {
77                 userSession->uniqueId = *thisValue;
78             }
79             else if (element.key() == "session_token")
80             {
81                 userSession->sessionToken = *thisValue;
82             }
83             else if (element.key() == "csrf_token")
84             {
85                 userSession->csrfToken = *thisValue;
86             }
87             else if (element.key() == "username")
88             {
89                 userSession->username = *thisValue;
90             }
91 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
92             else if (element.key() == "client_id")
93             {
94                 userSession->clientId = *thisValue;
95             }
96 #endif
97             else if (element.key() == "client_ip")
98             {
99                 userSession->clientIp = *thisValue;
100             }
101 
102             else
103             {
104                 BMCWEB_LOG_ERROR
105                     << "Got unexpected property reading persistent file: "
106                     << element.key();
107                 continue;
108             }
109         }
110         // If any of these fields are missing, we can't restore the session, as
111         // we don't have enough information.  These 4 fields have been present
112         // in every version of this file in bmcwebs history, so any file, even
113         // on upgrade, should have these present
114         if (userSession->uniqueId.empty() || userSession->username.empty() ||
115             userSession->sessionToken.empty() || userSession->csrfToken.empty())
116         {
117             BMCWEB_LOG_DEBUG << "Session missing required security "
118                                 "information, refusing to restore";
119             return nullptr;
120         }
121 
122         // For now, sessions that were persisted through a reboot get their idle
123         // timer reset.  This could probably be overcome with a better
124         // understanding of wall clock time and steady timer time, possibly
125         // persisting values with wall clock time instead of steady timer, but
126         // the tradeoffs of all the corner cases involved are non-trivial, so
127         // this is done temporarily
128         userSession->lastUpdated = std::chrono::steady_clock::now();
129         userSession->persistence = PersistenceType::TIMEOUT;
130 
131         return userSession;
132     }
133 };
134 
135 struct AuthConfigMethods
136 {
137 #ifdef BMCWEB_ENABLE_BASIC_AUTHENTICATION
138     bool basic = true;
139 #else
140     bool basic = false;
141 #endif
142 
143 #ifdef BMCWEB_ENABLE_SESSION_AUTHENTICATION
144     bool sessionToken = true;
145 #else
146     bool sessionToken = false;
147 #endif
148 
149 #ifdef BMCWEB_ENABLE_XTOKEN_AUTHENTICATION
150     bool xtoken = true;
151 #else
152     bool xtoken = false;
153 #endif
154 
155 #ifdef BMCWEB_ENABLE_COOKIE_AUTHENTICATION
156     bool cookie = true;
157 #else
158     bool cookie = false;
159 #endif
160 
161 #ifdef BMCWEB_ENABLE_MUTUAL_TLS_AUTHENTICATION
162     bool tls = true;
163 #else
164     bool tls = false;
165 #endif
166 
167     void fromJson(const nlohmann::json& j)
168     {
169         for (const auto& element : j.items())
170         {
171             const bool* value = element.value().get_ptr<const bool*>();
172             if (value == nullptr)
173             {
174                 continue;
175             }
176 
177             if (element.key() == "XToken")
178             {
179                 xtoken = *value;
180             }
181             else if (element.key() == "Cookie")
182             {
183                 cookie = *value;
184             }
185             else if (element.key() == "SessionToken")
186             {
187                 sessionToken = *value;
188             }
189             else if (element.key() == "BasicAuth")
190             {
191                 basic = *value;
192             }
193             else if (element.key() == "TLS")
194             {
195                 tls = *value;
196             }
197         }
198     }
199 };
200 
201 class SessionStore
202 {
203   public:
204     std::shared_ptr<UserSession> generateUserSession(
205         const std::string_view username,
206         const boost::asio::ip::address& clientIp,
207         const std::string_view 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,
259             std::string(clientId), redfish::ip_util::toString(clientIp),
260             std::chrono::steady_clock::now(), persistence, false,
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>
269         loginSessionByToken(const 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(const 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 updateAuthMethodsConfig(const AuthConfigMethods& config)
330     {
331         bool isTLSchanged = (authMethodsConfig.tls != config.tls);
332         authMethodsConfig = config;
333         needWrite = true;
334         if (isTLSchanged)
335         {
336             // recreate socket connections with new settings
337             std::raise(SIGHUP);
338         }
339     }
340 
341     AuthConfigMethods& getAuthMethodsConfig()
342     {
343         return authMethodsConfig;
344     }
345 
346     bool needsWrite() const
347     {
348         return needWrite;
349     }
350     int64_t getTimeoutInSeconds() const
351     {
352         return std::chrono::seconds(timeoutInSeconds).count();
353     }
354 
355     void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds)
356     {
357         timeoutInSeconds = newTimeoutInSeconds;
358         needWrite = true;
359     }
360 
361     static SessionStore& getInstance()
362     {
363         static SessionStore sessionStore;
364         return sessionStore;
365     }
366 
367     void applySessionTimeouts()
368     {
369         auto timeNow = std::chrono::steady_clock::now();
370         if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1))
371         {
372             lastTimeoutUpdate = timeNow;
373             auto authTokensIt = authTokens.begin();
374             while (authTokensIt != authTokens.end())
375             {
376                 if (timeNow - authTokensIt->second->lastUpdated >=
377                     timeoutInSeconds)
378                 {
379 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
380                     crow::ibm_mc_lock::Lock::getInstance().releaseLock(
381                         authTokensIt->second->uniqueId);
382 #endif
383                     authTokensIt = authTokens.erase(authTokensIt);
384 
385                     needWrite = true;
386                 }
387                 else
388                 {
389                     authTokensIt++;
390                 }
391             }
392         }
393     }
394 
395     SessionStore(const SessionStore&) = delete;
396     SessionStore& operator=(const SessionStore&) = delete;
397     SessionStore(SessionStore&&) = delete;
398     SessionStore& operator=(const SessionStore&&) = delete;
399     ~SessionStore() = default;
400 
401     std::unordered_map<std::string, std::shared_ptr<UserSession>,
402                        std::hash<std::string>,
403                        crow::utility::ConstantTimeCompare>
404         authTokens;
405 
406     std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
407     bool needWrite{false};
408     std::chrono::seconds timeoutInSeconds;
409     AuthConfigMethods authMethodsConfig;
410 
411   private:
412     SessionStore() : timeoutInSeconds(1800)
413     {}
414 };
415 
416 } // namespace persistent_data
417