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