xref: /openbmc/bmcweb/include/login_routes.hpp (revision 60719639)
1 #pragma once
2 
3 #include "multipart_parser.hpp"
4 
5 #include <app.hpp>
6 #include <boost/container/flat_set.hpp>
7 #include <common.hpp>
8 #include <http_request.hpp>
9 #include <http_response.hpp>
10 #include <pam_authenticate.hpp>
11 #include <webassets.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                 std::string unsupportedClientId;
180                 auto session =
181                     persistent_data::SessionStore::getInstance()
182                         .generateUserSession(
183                             username, req.ipAddress, unsupportedClientId,
184                             persistent_data::PersistenceType::TIMEOUT,
185                             isConfigureSelfOnly);
186 
187                 if (looksLikePhosphorRest)
188                 {
189                     // Phosphor-Rest requires a very specific login
190                     // structure, and doesn't actually look at the status
191                     // code.
192                     // TODO(ed).... Fix that upstream
193 
194                     asyncResp->res.jsonValue["data"] =
195                         "User '" + std::string(username) + "' logged in";
196                     asyncResp->res.jsonValue["message"] = "200 OK";
197                     asyncResp->res.jsonValue["status"] = "ok";
198 
199                     // Hack alert.  Boost beast by default doesn't let you
200                     // declare multiple headers of the same name, and in
201                     // most cases this is fine.  Unfortunately here we need
202                     // to set the Session cookie, which requires the
203                     // httpOnly attribute, as well as the XSRF cookie, which
204                     // requires it to not have an httpOnly attribute. To get
205                     // the behavior we want, we simply inject the second
206                     // "set-cookie" string into the value header, and get
207                     // the result we want, even though we are technicaly
208                     // declaring two headers here.
209                     asyncResp->res.addHeader(
210                         "Set-Cookie",
211                         "XSRF-TOKEN=" + session->csrfToken +
212                             "; SameSite=Strict; Secure\r\nSet-Cookie: "
213                             "SESSION=" +
214                             session->sessionToken +
215                             "; SameSite=Strict; Secure; HttpOnly");
216                 }
217                 else
218                 {
219                     // if content type is json, assume json token
220                     asyncResp->res.jsonValue["token"] = session->sessionToken;
221                 }
222             }
223         }
224         else
225         {
226             BMCWEB_LOG_DEBUG << "Couldn't interpret password";
227             asyncResp->res.result(boost::beast::http::status::bad_request);
228         }
229         });
230 
231     BMCWEB_ROUTE(app, "/logout")
232         .methods(boost::beast::http::verb::post)(
233             [](const crow::Request& req,
234                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
235         const auto& session = req.session;
236         if (session != nullptr)
237         {
238             asyncResp->res.jsonValue["data"] =
239                 "User '" + session->username + "' logged out";
240             asyncResp->res.jsonValue["message"] = "200 OK";
241             asyncResp->res.jsonValue["status"] = "ok";
242 
243             asyncResp->res.addHeader("Set-Cookie",
244                                      "SESSION="
245                                      "; SameSite=Strict; Secure; HttpOnly; "
246                                      "expires=Thu, 01 Jan 1970 00:00:00 GMT");
247 
248             persistent_data::SessionStore::getInstance().removeSession(session);
249         }
250         });
251 }
252 } // namespace login_routes
253 } // namespace crow
254