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