xref: /openbmc/bmcweb/include/sessions.hpp (revision e9cc1bc9)
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>(UserSession{
260             uniqueId, sessionToken, std::string(username), csrfToken, clientId,
261             redfish::ip_util::toString(clientIp),
262             std::chrono::steady_clock::now(), persistence, false,
263             isConfigureSelfOnly});
264         auto it = authTokens.emplace(sessionToken, session);
265         // Only need to write to disk if session isn't about to be destroyed.
266         needWrite = persistence == PersistenceType::TIMEOUT;
267         return it.first->second;
268     }
269 
270     std::shared_ptr<UserSession> loginSessionByToken(std::string_view token)
271     {
272         applySessionTimeouts();
273         if (token.size() != sessionTokenSize)
274         {
275             return nullptr;
276         }
277         auto sessionIt = authTokens.find(std::string(token));
278         if (sessionIt == authTokens.end())
279         {
280             return nullptr;
281         }
282         std::shared_ptr<UserSession> userSession = sessionIt->second;
283         userSession->lastUpdated = std::chrono::steady_clock::now();
284         return userSession;
285     }
286 
287     std::shared_ptr<UserSession> getSessionByUid(std::string_view uid)
288     {
289         applySessionTimeouts();
290         // TODO(Ed) this is inefficient
291         auto sessionIt = authTokens.begin();
292         while (sessionIt != authTokens.end())
293         {
294             if (sessionIt->second->uniqueId == uid)
295             {
296                 return sessionIt->second;
297             }
298             sessionIt++;
299         }
300         return nullptr;
301     }
302 
303     void removeSession(const std::shared_ptr<UserSession>& session)
304     {
305 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
306         crow::ibm_mc_lock::Lock::getInstance().releaseLock(session->uniqueId);
307 #endif
308         authTokens.erase(session->sessionToken);
309         needWrite = true;
310     }
311 
312     std::vector<const std::string*> getUniqueIds(
313         bool getAll = true,
314         const PersistenceType& type = PersistenceType::SINGLE_REQUEST)
315     {
316         applySessionTimeouts();
317 
318         std::vector<const std::string*> ret;
319         ret.reserve(authTokens.size());
320         for (auto& session : authTokens)
321         {
322             if (getAll || type == session.second->persistence)
323             {
324                 ret.push_back(&session.second->uniqueId);
325             }
326         }
327         return ret;
328     }
329 
330     void removeSessionsByUsername(std::string_view username)
331     {
332         std::erase_if(authTokens, [username](const auto& value) {
333             if (value.second == nullptr)
334             {
335                 return false;
336             }
337             return value.second->username == username;
338         });
339     }
340 
341     void updateAuthMethodsConfig(const AuthConfigMethods& config)
342     {
343         bool isTLSchanged = (authMethodsConfig.tls != config.tls);
344         authMethodsConfig = config;
345         needWrite = true;
346         if (isTLSchanged)
347         {
348             // recreate socket connections with new settings
349             std::raise(SIGHUP);
350         }
351     }
352 
353     AuthConfigMethods& getAuthMethodsConfig()
354     {
355         return authMethodsConfig;
356     }
357 
358     bool needsWrite() const
359     {
360         return needWrite;
361     }
362     int64_t getTimeoutInSeconds() const
363     {
364         return std::chrono::seconds(timeoutInSeconds).count();
365     }
366 
367     void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds)
368     {
369         timeoutInSeconds = newTimeoutInSeconds;
370         needWrite = true;
371     }
372 
373     static SessionStore& getInstance()
374     {
375         static SessionStore sessionStore;
376         return sessionStore;
377     }
378 
379     void applySessionTimeouts()
380     {
381         auto timeNow = std::chrono::steady_clock::now();
382         if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1))
383         {
384             lastTimeoutUpdate = timeNow;
385             auto authTokensIt = authTokens.begin();
386             while (authTokensIt != authTokens.end())
387             {
388                 if (timeNow - authTokensIt->second->lastUpdated >=
389                     timeoutInSeconds)
390                 {
391 #ifdef BMCWEB_ENABLE_IBM_MANAGEMENT_CONSOLE
392                     crow::ibm_mc_lock::Lock::getInstance().releaseLock(
393                         authTokensIt->second->uniqueId);
394 #endif
395                     authTokensIt = authTokens.erase(authTokensIt);
396 
397                     needWrite = true;
398                 }
399                 else
400                 {
401                     authTokensIt++;
402                 }
403             }
404         }
405     }
406 
407     SessionStore(const SessionStore&) = delete;
408     SessionStore& operator=(const SessionStore&) = delete;
409     SessionStore(SessionStore&&) = delete;
410     SessionStore& operator=(const SessionStore&&) = delete;
411     ~SessionStore() = default;
412 
413     std::unordered_map<std::string, std::shared_ptr<UserSession>,
414                        std::hash<std::string>,
415                        crow::utility::ConstantTimeCompare>
416         authTokens;
417 
418     std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
419     bool needWrite{false};
420     std::chrono::seconds timeoutInSeconds;
421     AuthConfigMethods authMethodsConfig;
422 
423   private:
424     SessionStore() : timeoutInSeconds(1800) {}
425 };
426 
427 } // namespace persistent_data
428