xref: /openbmc/bmcweb/include/login_routes.hpp (revision 46228e0e80488ecbe1058f01cdaa018eaa6bd654)
1 #pragma once
2 
3 #include "app.hpp"
4 #include "common.hpp"
5 #include "http_request.hpp"
6 #include "http_response.hpp"
7 #include "multipart_parser.hpp"
8 #include "pam_authenticate.hpp"
9 #include "webassets.hpp"
10 
11 #include <boost/container/flat_set.hpp>
12 
13 #include <random>
14 
15 namespace crow
16 {
17 
18 namespace login_routes
19 {
20 
21 inline void handleLogin(const crow::Request& req,
22                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
23 {
24     MultipartParser parser;
25     std::string_view contentType = req.getHeaderValue("content-type");
26     std::string_view username;
27     std::string_view password;
28 
29     bool looksLikePhosphorRest = false;
30 
31     // This object needs to be declared at this scope so the strings
32     // within it are not destroyed before we can use them
33     nlohmann::json loginCredentials;
34     // Check if auth was provided by a payload
35     if (contentType.starts_with("application/json"))
36     {
37         loginCredentials = nlohmann::json::parse(req.body(), nullptr, false);
38         if (loginCredentials.is_discarded())
39         {
40             BMCWEB_LOG_DEBUG("Bad json in request");
41             asyncResp->res.result(boost::beast::http::status::bad_request);
42             return;
43         }
44 
45         // check for username/password in the root object
46         // THis method is how intel APIs authenticate
47         nlohmann::json::iterator userIt = loginCredentials.find("username");
48         nlohmann::json::iterator passIt = loginCredentials.find("password");
49         if (userIt != loginCredentials.end() &&
50             passIt != loginCredentials.end())
51         {
52             const std::string* userStr = userIt->get_ptr<const std::string*>();
53             const std::string* passStr = passIt->get_ptr<const std::string*>();
54             if (userStr != nullptr && passStr != nullptr)
55             {
56                 username = *userStr;
57                 password = *passStr;
58             }
59         }
60         else
61         {
62             // Openbmc appears to push a data object that contains the
63             // same keys (username and password), attempt to use that
64             auto dataIt = loginCredentials.find("data");
65             if (dataIt != loginCredentials.end())
66             {
67                 // Some apis produce an array of value ["username",
68                 // "password"]
69                 if (dataIt->is_array())
70                 {
71                     if (dataIt->size() == 2)
72                     {
73                         nlohmann::json::iterator userIt2 = dataIt->begin();
74                         nlohmann::json::iterator passIt2 = dataIt->begin() + 1;
75                         looksLikePhosphorRest = true;
76                         if (userIt2 != dataIt->end() &&
77                             passIt2 != dataIt->end())
78                         {
79                             const std::string* userStr =
80                                 userIt2->get_ptr<const std::string*>();
81                             const std::string* passStr =
82                                 passIt2->get_ptr<const std::string*>();
83                             if (userStr != nullptr && passStr != nullptr)
84                             {
85                                 username = *userStr;
86                                 password = *passStr;
87                             }
88                         }
89                     }
90                 }
91                 else if (dataIt->is_object())
92                 {
93                     nlohmann::json::iterator userIt2 = dataIt->find("username");
94                     nlohmann::json::iterator passIt2 = dataIt->find("password");
95                     if (userIt2 != dataIt->end() && passIt2 != dataIt->end())
96                     {
97                         const std::string* userStr =
98                             userIt2->get_ptr<const std::string*>();
99                         const std::string* passStr =
100                             passIt2->get_ptr<const std::string*>();
101                         if (userStr != nullptr && passStr != nullptr)
102                         {
103                             username = *userStr;
104                             password = *passStr;
105                         }
106                     }
107                 }
108             }
109         }
110     }
111     else if (contentType.starts_with("multipart/form-data"))
112     {
113         looksLikePhosphorRest = true;
114         ParserError ec = parser.parse(req);
115         if (ec != ParserError::PARSER_SUCCESS)
116         {
117             // handle error
118             BMCWEB_LOG_ERROR("MIME parse failed, ec : {}",
119                              static_cast<int>(ec));
120             asyncResp->res.result(boost::beast::http::status::bad_request);
121             return;
122         }
123 
124         for (const FormPart& formpart : parser.mime_fields)
125         {
126             boost::beast::http::fields::const_iterator it =
127                 formpart.fields.find("Content-Disposition");
128             if (it == formpart.fields.end())
129             {
130                 BMCWEB_LOG_ERROR("Couldn't find Content-Disposition");
131                 asyncResp->res.result(boost::beast::http::status::bad_request);
132                 continue;
133             }
134 
135             BMCWEB_LOG_INFO("Parsing value {}", it->value());
136 
137             if (it->value() == "form-data; name=\"username\"")
138             {
139                 username = formpart.content;
140             }
141             else if (it->value() == "form-data; name=\"password\"")
142             {
143                 password = formpart.content;
144             }
145             else
146             {
147                 BMCWEB_LOG_INFO("Extra format, ignore it.{}", it->value());
148             }
149         }
150     }
151     else
152     {
153         // check if auth was provided as a headers
154         username = req.getHeaderValue("username");
155         password = req.getHeaderValue("password");
156     }
157 
158     if (!username.empty() && !password.empty())
159     {
160         int pamrc = pamAuthenticateUser(username, password);
161         bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
162         if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
163         {
164             asyncResp->res.result(boost::beast::http::status::unauthorized);
165         }
166         else
167         {
168             auto session = persistent_data::SessionStore::getInstance()
169                                .generateUserSession(
170                                    username, req.ipAddress, std::nullopt,
171                                    persistent_data::PersistenceType::TIMEOUT,
172                                    isConfigureSelfOnly);
173 
174             if (looksLikePhosphorRest)
175             {
176                 // Phosphor-Rest requires a very specific login
177                 // structure, and doesn't actually look at the status
178                 // code.
179                 // TODO(ed).... Fix that upstream
180 
181                 asyncResp->res.jsonValue["data"] =
182                     "User '" + std::string(username) + "' logged in";
183                 asyncResp->res.jsonValue["message"] = "200 OK";
184                 asyncResp->res.jsonValue["status"] = "ok";
185 
186                 asyncResp->res.addHeader(boost::beast::http::field::set_cookie,
187                                          "XSRF-TOKEN=" + session->csrfToken +
188                                              "; SameSite=Strict; Secure");
189                 asyncResp->res.addHeader(
190                     boost::beast::http::field::set_cookie,
191                     "SESSION=" + session->sessionToken +
192                         "; SameSite=Strict; Secure; HttpOnly");
193             }
194             else
195             {
196                 // if content type is json, assume json token
197                 asyncResp->res.jsonValue["token"] = session->sessionToken;
198             }
199         }
200     }
201     else
202     {
203         BMCWEB_LOG_DEBUG("Couldn't interpret password");
204         asyncResp->res.result(boost::beast::http::status::bad_request);
205     }
206 }
207 
208 inline void handleLogout(const crow::Request& req,
209                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
210 {
211     const auto& session = req.session;
212     if (session != nullptr)
213     {
214         asyncResp->res.jsonValue["data"] = "User '" + session->username +
215                                            "' logged out";
216         asyncResp->res.jsonValue["message"] = "200 OK";
217         asyncResp->res.jsonValue["status"] = "ok";
218 
219         asyncResp->res.addHeader("Set-Cookie",
220                                  "SESSION="
221                                  "; SameSite=Strict; Secure; HttpOnly; "
222                                  "expires=Thu, 01 Jan 1970 00:00:00 GMT");
223         asyncResp->res.addHeader("Clear-Site-Data",
224                                  R"("cache","cookies","storage")");
225         persistent_data::SessionStore::getInstance().removeSession(session);
226     }
227 }
228 
229 inline void requestRoutes(App& app)
230 {
231     BMCWEB_ROUTE(app, "/login")
232         .methods(boost::beast::http::verb::post)(handleLogin);
233 
234     BMCWEB_ROUTE(app, "/logout")
235         .methods(boost::beast::http::verb::post)(handleLogout);
236 }
237 } // namespace login_routes
238 } // namespace crow
239