xref: /openbmc/bmcweb/include/sessions.hpp (revision 683f7276)
1 #pragma once
2 
3 #include <crow/app.h>
4 #include <crow/http_request.h>
5 #include <crow/http_response.h>
6 
7 #include <boost/container/flat_map.hpp>
8 #include <boost/uuid/uuid.hpp>
9 #include <boost/uuid/uuid_generators.hpp>
10 #include <boost/uuid/uuid_io.hpp>
11 #include <nlohmann/json.hpp>
12 #include <pam_authenticate.hpp>
13 #include <random>
14 #include <webassets.hpp>
15 
16 namespace crow
17 {
18 
19 namespace persistent_data
20 {
21 
22 enum class PersistenceType
23 {
24     TIMEOUT, // User session times out after a predetermined amount of time
25     SINGLE_REQUEST // User times out once this request is completed.
26 };
27 
28 struct UserSession
29 {
30     std::string uniqueId;
31     std::string sessionToken;
32     std::string username;
33     std::string csrfToken;
34     std::chrono::time_point<std::chrono::steady_clock> lastUpdated;
35     PersistenceType persistence;
36 
37     /**
38      * @brief Fills object with data from UserSession's JSON representation
39      *
40      * This replaces nlohmann's from_json to ensure no-throw approach
41      *
42      * @param[in] j   JSON object from which data should be loaded
43      *
44      * @return a shared pointer if data has been loaded properly, nullptr
45      * otherwise
46      */
47     static std::shared_ptr<UserSession> fromJson(const nlohmann::json& j)
48     {
49         std::shared_ptr<UserSession> userSession =
50             std::make_shared<UserSession>();
51         for (const auto& element : j.items())
52         {
53             const std::string* thisValue =
54                 element.value().get_ptr<const std::string*>();
55             if (thisValue == nullptr)
56             {
57                 BMCWEB_LOG_ERROR << "Error reading persistent store.  Property "
58                                  << element.key() << " was not of type string";
59                 return nullptr;
60             }
61             if (element.key() == "unique_id")
62             {
63                 userSession->uniqueId = *thisValue;
64             }
65             else if (element.key() == "session_token")
66             {
67                 userSession->sessionToken = *thisValue;
68             }
69             else if (element.key() == "csrf_token")
70             {
71                 userSession->csrfToken = *thisValue;
72             }
73             else if (element.key() == "username")
74             {
75                 userSession->username = *thisValue;
76             }
77             else
78             {
79                 BMCWEB_LOG_ERROR
80                     << "Got unexpected property reading persistent file: "
81                     << element.key();
82                 return nullptr;
83             }
84         }
85 
86         // For now, sessions that were persisted through a reboot get their idle
87         // timer reset.  This could probably be overcome with a better
88         // understanding of wall clock time and steady timer time, possibly
89         // persisting values with wall clock time instead of steady timer, but
90         // the tradeoffs of all the corner cases involved are non-trivial, so
91         // this is done temporarily
92         userSession->lastUpdated = std::chrono::steady_clock::now();
93         userSession->persistence = PersistenceType::TIMEOUT;
94 
95         return userSession;
96     }
97 };
98 
99 class Middleware;
100 
101 class SessionStore
102 {
103   public:
104     std::shared_ptr<UserSession> generateUserSession(
105         const boost::string_view username,
106         PersistenceType persistence = PersistenceType::TIMEOUT)
107     {
108         // TODO(ed) find a secure way to not generate session identifiers if
109         // persistence is set to SINGLE_REQUEST
110         static constexpr std::array<char, 62> alphanum = {
111             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'b', 'C',
112             'D', 'E', 'F', 'g', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
113             'Q', 'r', 'S', 'T', 'U', 'v', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c',
114             'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
115             'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
116 
117         // entropy: 30 characters, 62 possibilities.  log2(62^30) = 178 bits of
118         // entropy.  OWASP recommends at least 60
119         // https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy
120         std::string sessionToken;
121         sessionToken.resize(20, '0');
122         std::uniform_int_distribution<int> dist(0, alphanum.size() - 1);
123         for (int i = 0; i < sessionToken.size(); ++i)
124         {
125             sessionToken[i] = alphanum[dist(rd)];
126         }
127         // Only need csrf tokens for cookie based auth, token doesn't matter
128         std::string csrfToken;
129         csrfToken.resize(20, '0');
130         for (int i = 0; i < csrfToken.size(); ++i)
131         {
132             csrfToken[i] = alphanum[dist(rd)];
133         }
134 
135         std::string uniqueId;
136         uniqueId.resize(10, '0');
137         for (int i = 0; i < uniqueId.size(); ++i)
138         {
139             uniqueId[i] = alphanum[dist(rd)];
140         }
141         auto session = std::make_shared<UserSession>(UserSession{
142             uniqueId, sessionToken, std::string(username), csrfToken,
143             std::chrono::steady_clock::now(), persistence});
144         auto it = authTokens.emplace(std::make_pair(sessionToken, session));
145         // Only need to write to disk if session isn't about to be destroyed.
146         needWrite = persistence == PersistenceType::TIMEOUT;
147         return it.first->second;
148     }
149 
150     std::shared_ptr<UserSession>
151         loginSessionByToken(const boost::string_view token)
152     {
153         applySessionTimeouts();
154         auto sessionIt = authTokens.find(std::string(token));
155         if (sessionIt == authTokens.end())
156         {
157             return nullptr;
158         }
159         std::shared_ptr<UserSession> userSession = sessionIt->second;
160         userSession->lastUpdated = std::chrono::steady_clock::now();
161         return userSession;
162     }
163 
164     std::shared_ptr<UserSession> getSessionByUid(const boost::string_view uid)
165     {
166         applySessionTimeouts();
167         // TODO(Ed) this is inefficient
168         auto sessionIt = authTokens.begin();
169         while (sessionIt != authTokens.end())
170         {
171             if (sessionIt->second->uniqueId == uid)
172             {
173                 return sessionIt->second;
174             }
175             sessionIt++;
176         }
177         return nullptr;
178     }
179 
180     void removeSession(std::shared_ptr<UserSession> session)
181     {
182         authTokens.erase(session->sessionToken);
183         needWrite = true;
184     }
185 
186     std::vector<const std::string*> getUniqueIds(
187         bool getAll = true,
188         const PersistenceType& type = PersistenceType::SINGLE_REQUEST)
189     {
190         applySessionTimeouts();
191 
192         std::vector<const std::string*> ret;
193         ret.reserve(authTokens.size());
194         for (auto& session : authTokens)
195         {
196             if (getAll || type == session.second->persistence)
197             {
198                 ret.push_back(&session.second->uniqueId);
199             }
200         }
201         return ret;
202     }
203 
204     bool needsWrite()
205     {
206         return needWrite;
207     }
208     int getTimeoutInSeconds() const
209     {
210         return std::chrono::seconds(timeoutInMinutes).count();
211     };
212 
213     // Persistent data middleware needs to be able to serialize our authTokens
214     // structure, which is private
215     friend Middleware;
216 
217     static SessionStore& getInstance()
218     {
219         static SessionStore sessionStore;
220         return sessionStore;
221     }
222 
223     SessionStore(const SessionStore&) = delete;
224     SessionStore& operator=(const SessionStore&) = delete;
225 
226   private:
227     SessionStore() : timeoutInMinutes(60)
228     {
229     }
230 
231     void applySessionTimeouts()
232     {
233         auto timeNow = std::chrono::steady_clock::now();
234         if (timeNow - lastTimeoutUpdate > std::chrono::minutes(1))
235         {
236             lastTimeoutUpdate = timeNow;
237             auto authTokensIt = authTokens.begin();
238             while (authTokensIt != authTokens.end())
239             {
240                 if (timeNow - authTokensIt->second->lastUpdated >=
241                     timeoutInMinutes)
242                 {
243                     authTokensIt = authTokens.erase(authTokensIt);
244                     needWrite = true;
245                 }
246                 else
247                 {
248                     authTokensIt++;
249                 }
250             }
251         }
252     }
253     std::chrono::time_point<std::chrono::steady_clock> lastTimeoutUpdate;
254     boost::container::flat_map<std::string, std::shared_ptr<UserSession>>
255         authTokens;
256     std::random_device rd;
257     bool needWrite{false};
258     std::chrono::minutes timeoutInMinutes;
259 };
260 
261 } // namespace persistent_data
262 } // namespace crow
263 
264 // to_json(...) definition for objects of UserSession type
265 namespace nlohmann
266 {
267 template <>
268 struct adl_serializer<std::shared_ptr<crow::persistent_data::UserSession>>
269 {
270     static void
271         to_json(nlohmann::json& j,
272                 const std::shared_ptr<crow::persistent_data::UserSession>& p)
273     {
274         if (p->persistence !=
275             crow::persistent_data::PersistenceType::SINGLE_REQUEST)
276         {
277             j = nlohmann::json{{"unique_id", p->uniqueId},
278                                {"session_token", p->sessionToken},
279                                {"username", p->username},
280                                {"csrf_token", p->csrfToken}};
281         }
282     }
283 };
284 } // namespace nlohmann
285