xref: /openbmc/bmcweb/include/sessions.hpp (revision a3b9eb98)
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 
15 namespace persistent_data
16 {
17 
18 // entropy: 20 characters, 62 possibilities.  log2(62^20) = 119 bits of
19 // entropy.  OWASP recommends at least 64
20 // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-entropy
21 constexpr std::size_t sessionTokenSize = 20;
22 
23 enum class PersistenceType
24 {
25     TIMEOUT, // User session times out after a predetermined amount of time
26     SINGLE_REQUEST // User times out once this request is completed.
27 };
28 
29 struct UserSession
30 {
31     std::string uniqueId;
32     std::string sessionToken;
33     std::string username;
34     std::string csrfToken;
35     std::optional<std::string> clientId;
36     std::string clientIp;
37     std::chrono::time_point<std::chrono::steady_clock> lastUpdated;
38     PersistenceType persistence{PersistenceType::TIMEOUT};
39     bool cookieAuth = false;
40     bool isConfigureSelfOnly = false;
41     std::string userRole;
42     std::vector<std::string> userGroups;
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(
73                     "Error reading persistent store.  Property {} was not of type string",
74                     element.key());
75                 continue;
76             }
77             if (element.key() == "unique_id")
78             {
79                 userSession->uniqueId = *thisValue;
80             }
81             else if (element.key() == "session_token")
82             {
83                 userSession->sessionToken = *thisValue;
84             }
85             else if (element.key() == "csrf_token")
86             {
87                 userSession->csrfToken = *thisValue;
88             }
89             else if (element.key() == "username")
90             {
91                 userSession->username = *thisValue;
92             }
93             else if (element.key() == "client_id")
94             {
95                 userSession->clientId = *thisValue;
96             }
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     bool basic = BMCWEB_BASIC_AUTH;
138     bool sessionToken = BMCWEB_SESSION_AUTH;
139     bool xtoken = BMCWEB_XTOKEN_AUTH;
140     bool cookie = BMCWEB_COOKIE_AUTH;
141     bool tls = BMCWEB_MUTUAL_TLS_AUTH;
142 
143     void fromJson(const nlohmann::json& j)
144     {
145         for (const auto& element : j.items())
146         {
147             const bool* value = element.value().get_ptr<const bool*>();
148             if (value == nullptr)
149             {
150                 continue;
151             }
152 
153             if (element.key() == "XToken")
154             {
155                 xtoken = *value;
156             }
157             else if (element.key() == "Cookie")
158             {
159                 cookie = *value;
160             }
161             else if (element.key() == "SessionToken")
162             {
163                 sessionToken = *value;
164             }
165             else if (element.key() == "BasicAuth")
166             {
167                 basic = *value;
168             }
169             else if (element.key() == "TLS")
170             {
171                 tls = *value;
172             }
173         }
174     }
175 };
176 
177 class SessionStore
178 {
179   public:
180     std::shared_ptr<UserSession> generateUserSession(
181         std::string_view username, const boost::asio::ip::address& clientIp,
182         const std::optional<std::string>& clientId,
183         PersistenceType persistence = PersistenceType::TIMEOUT,
184         bool isConfigureSelfOnly = false)
185     {
186         // TODO(ed) find a secure way to not generate session identifiers if
187         // persistence is set to SINGLE_REQUEST
188         static constexpr std::array<char, 62> alphanum = {
189             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
190             'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
191             'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
192             'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
193             'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
194 
195         std::string sessionToken;
196         sessionToken.resize(sessionTokenSize, '0');
197         std::uniform_int_distribution<size_t> dist(0, alphanum.size() - 1);
198 
199         bmcweb::OpenSSLGenerator gen;
200 
201         for (char& sessionChar : sessionToken)
202         {
203             sessionChar = alphanum[dist(gen)];
204             if (gen.error())
205             {
206                 return nullptr;
207             }
208         }
209         // Only need csrf tokens for cookie based auth, token doesn't matter
210         std::string csrfToken;
211         csrfToken.resize(sessionTokenSize, '0');
212         for (char& csrfChar : csrfToken)
213         {
214             csrfChar = alphanum[dist(gen)];
215             if (gen.error())
216             {
217                 return nullptr;
218             }
219         }
220 
221         std::string uniqueId;
222         uniqueId.resize(10, '0');
223         for (char& uidChar : uniqueId)
224         {
225             uidChar = alphanum[dist(gen)];
226             if (gen.error())
227             {
228                 return nullptr;
229             }
230         }
231 
232         auto session = std::make_shared<UserSession>(
233             UserSession{uniqueId,
234                         sessionToken,
235                         std::string(username),
236                         csrfToken,
237                         clientId,
238                         redfish::ip_util::toString(clientIp),
239                         std::chrono::steady_clock::now(),
240                         persistence,
241                         false,
242                         isConfigureSelfOnly,
243                         "",
244                         {}});
245         auto it = authTokens.emplace(sessionToken, session);
246         // Only need to write to disk if session isn't about to be destroyed.
247         needWrite = persistence == PersistenceType::TIMEOUT;
248         return it.first->second;
249     }
250 
251     std::shared_ptr<UserSession> loginSessionByToken(std::string_view token)
252     {
253         applySessionTimeouts();
254         if (token.size() != sessionTokenSize)
255         {
256             return nullptr;
257         }
258         auto sessionIt = authTokens.find(std::string(token));
259         if (sessionIt == authTokens.end())
260         {
261             return nullptr;
262         }
263         std::shared_ptr<UserSession> userSession = sessionIt->second;
264         userSession->lastUpdated = std::chrono::steady_clock::now();
265         return userSession;
266     }
267 
268     std::shared_ptr<UserSession> getSessionByUid(std::string_view uid)
269     {
270         applySessionTimeouts();
271         // TODO(Ed) this is inefficient
272         auto sessionIt = authTokens.begin();
273         while (sessionIt != authTokens.end())
274         {
275             if (sessionIt->second->uniqueId == uid)
276             {
277                 return sessionIt->second;
278             }
279             sessionIt++;
280         }
281         return nullptr;
282     }
283 
284     void removeSession(const std::shared_ptr<UserSession>& session)
285     {
286         authTokens.erase(session->sessionToken);
287         needWrite = true;
288     }
289 
290     std::vector<const std::string*> getUniqueIds(
291         bool getAll = true,
292         const PersistenceType& type = PersistenceType::SINGLE_REQUEST)
293     {
294         applySessionTimeouts();
295 
296         std::vector<const std::string*> ret;
297         ret.reserve(authTokens.size());
298         for (auto& session : authTokens)
299         {
300             if (getAll || type == session.second->persistence)
301             {
302                 ret.push_back(&session.second->uniqueId);
303             }
304         }
305         return ret;
306     }
307 
308     void removeSessionsByUsername(std::string_view username)
309     {
310         std::erase_if(authTokens, [username](const auto& value) {
311             if (value.second == nullptr)
312             {
313                 return false;
314             }
315             return value.second->username == username;
316         });
317     }
318 
319     void removeSessionsByUsernameExceptSession(
320         std::string_view username, const std::shared_ptr<UserSession>& session)
321     {
322         std::erase_if(authTokens, [username, session](const auto& value) {
323             if (value.second == nullptr)
324             {
325                 return false;
326             }
327 
328             return value.second->username == username &&
329                    value.second->uniqueId != session->uniqueId;
330         });
331     }
332 
333     void updateAuthMethodsConfig(const AuthConfigMethods& config)
334     {
335         bool isTLSchanged = (authMethodsConfig.tls != config.tls);
336         authMethodsConfig = config;
337         needWrite = true;
338         if (isTLSchanged)
339         {
340             // recreate socket connections with new settings
341             std::raise(SIGHUP);
342         }
343     }
344 
345     AuthConfigMethods& getAuthMethodsConfig()
346     {
347         return authMethodsConfig;
348     }
349 
350     bool needsWrite() const
351     {
352         return needWrite;
353     }
354     int64_t getTimeoutInSeconds() const
355     {
356         return std::chrono::seconds(timeoutInSeconds).count();
357     }
358 
359     void updateSessionTimeout(std::chrono::seconds newTimeoutInSeconds)
360     {
361         timeoutInSeconds = newTimeoutInSeconds;
362         needWrite = true;
363     }
364 
365     static SessionStore& getInstance()
366     {
367         static SessionStore sessionStore;
368         return sessionStore;
369     }
370 
371     void applySessionTimeouts()
372     {
373         auto timeNow = std::chrono::steady_clock::now();
374         if (timeNow - lastTimeoutUpdate > std::chrono::seconds(1))
375         {
376             lastTimeoutUpdate = timeNow;
377             auto authTokensIt = authTokens.begin();
378             while (authTokensIt != authTokens.end())
379             {
380                 if (timeNow - authTokensIt->second->lastUpdated >=
381                     timeoutInSeconds)
382                 {
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 } // namespace persistent_data
416