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