xref: /openbmc/bmcweb/include/sessions.hpp (revision a8d8f9d8)
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         const std::string_view username,
205         const boost::asio::ip::address& clientIp,
206         const std::optional<std::string>& clientId,
207         PersistenceType persistence = PersistenceType::TIMEOUT,
208         bool isConfigureSelfOnly = false)
209     {
210         // TODO(ed) find a secure way to not generate session identifiers if
211         // persistence is set to SINGLE_REQUEST
212         static constexpr std::array<char, 62> alphanum = {
213             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
214             'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
215             'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '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'};
218 
219         std::string sessionToken;
220         sessionToken.resize(sessionTokenSize, '0');
221         std::uniform_int_distribution<size_t> dist(0, alphanum.size() - 1);
222 
223         bmcweb::OpenSSLGenerator gen;
224 
225         for (char& sessionChar : sessionToken)
226         {
227             sessionChar = alphanum[dist(gen)];
228             if (gen.error())
229             {
230                 return nullptr;
231             }
232         }
233         // Only need csrf tokens for cookie based auth, token doesn't matter
234         std::string csrfToken;
235         csrfToken.resize(sessionTokenSize, '0');
236         for (char& csrfChar : csrfToken)
237         {
238             csrfChar = alphanum[dist(gen)];
239             if (gen.error())
240             {
241                 return nullptr;
242             }
243         }
244 
245         std::string uniqueId;
246         uniqueId.resize(10, '0');
247         for (char& uidChar : uniqueId)
248         {
249             uidChar = alphanum[dist(gen)];
250             if (gen.error())
251             {
252                 return nullptr;
253             }
254         }
255 
256         auto session = std::make_shared<UserSession>(UserSession{
257             uniqueId, sessionToken, std::string(username), csrfToken, clientId,
258             redfish::ip_util::toString(clientIp),
259             std::chrono::steady_clock::now(), persistence, false,
260             isConfigureSelfOnly});
261         auto it = authTokens.emplace(sessionToken, session);
262         // Only need to write to disk if session isn't about to be destroyed.
263         needWrite = persistence == PersistenceType::TIMEOUT;
264         return it.first->second;
265     }
266 
267     std::shared_ptr<UserSession>
268         loginSessionByToken(const 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(const 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 updateAuthMethodsConfig(const AuthConfigMethods& config)
329     {
330         bool isTLSchanged = (authMethodsConfig.tls != config.tls);
331         authMethodsConfig = config;
332         needWrite = true;
333         if (isTLSchanged)
334         {
335             // recreate socket connections with new settings
336             std::raise(SIGHUP);
337         }
338     }
339 
340     AuthConfigMethods& getAuthMethodsConfig()
341     {
342         return authMethodsConfig;
343     }
344 
345     bool needsWrite() const
346     {
347         return needWrite;
348     }
349     int64_t getTimeoutInSeconds() const
350     {
351         return std::chrono::seconds(timeoutInSeconds).count();
352     }
353 
354     void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds)
355     {
356         timeoutInSeconds = newTimeoutInSeconds;
357         needWrite = true;
358     }
359 
360     static SessionStore& getInstance()
361     {
362         static SessionStore sessionStore;
363         return sessionStore;
364     }
365 
366     void applySessionTimeouts()
367     {
368         auto timeNow = std::chrono::steady_clock::now();
369         if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1))
370         {
371             lastTimeoutUpdate = timeNow;
372             auto authTokensIt = authTokens.begin();
373             while (authTokensIt != authTokens.end())
374             {
375                 if (timeNow - authTokensIt->second->lastUpdated >=
376                     timeoutInSeconds)
377                 {
378 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
379                     crow::ibm_mc_lock::Lock::getInstance().releaseLock(
380                         authTokensIt->second->uniqueId);
381 #endif
382                     authTokensIt = authTokens.erase(authTokensIt);
383 
384                     needWrite = true;
385                 }
386                 else
387                 {
388                     authTokensIt++;
389                 }
390             }
391         }
392     }
393 
394     SessionStore(const SessionStore&) = delete;
395     SessionStore& operator=(const SessionStore&) = delete;
396     SessionStore(SessionStore&&) = delete;
397     SessionStore& operator=(const SessionStore&&) = delete;
398     ~SessionStore() = default;
399 
400     std::unordered_map<std::string, std::shared_ptr<UserSession>,
401                        std::hash<std::string>,
402                        crow::utility::ConstantTimeCompare>
403         authTokens;
404 
405     std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
406     bool needWrite{false};
407     std::chrono::seconds timeoutInSeconds;
408     AuthConfigMethods authMethodsConfig;
409 
410   private:
411     SessionStore() : timeoutInSeconds(1800)
412     {}
413 };
414 
415 } // namespace persistent_data
416