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