xref: /openbmc/bmcweb/include/login_routes.hpp (revision 7b7f0410)
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         std::string_view contentType = req.getHeaderValue("content-type");
28         std::string_view username;
29         std::string_view password;
30 
31         bool looksLikePhosphorRest = false;
32 
33         // This object needs to be declared at this scope so the strings
34         // within it are not destroyed before we can use them
35         nlohmann::json loginCredentials;
36         // Check if auth was provided by a payload
37         if (boost::starts_with(contentType, "application/json"))
38         {
39             loginCredentials = nlohmann::json::parse(req.body, nullptr, false);
40             if (loginCredentials.is_discarded())
41             {
42                 BMCWEB_LOG_DEBUG << "Bad json in request";
43                 asyncResp->res.result(boost::beast::http::status::bad_request);
44                 return;
45             }
46 
47             // check for username/password in the root object
48             // THis method is how intel APIs authenticate
49             nlohmann::json::iterator userIt = loginCredentials.find("username");
50             nlohmann::json::iterator passIt = loginCredentials.find("password");
51             if (userIt != loginCredentials.end() &&
52                 passIt != loginCredentials.end())
53             {
54                 const std::string* userStr =
55                     userIt->get_ptr<const std::string*>();
56                 const std::string* passStr =
57                     passIt->get_ptr<const std::string*>();
58                 if (userStr != nullptr && passStr != nullptr)
59                 {
60                     username = *userStr;
61                     password = *passStr;
62                 }
63             }
64             else
65             {
66                 // Openbmc appears to push a data object that contains the
67                 // same keys (username and password), attempt to use that
68                 auto dataIt = loginCredentials.find("data");
69                 if (dataIt != loginCredentials.end())
70                 {
71                     // Some apis produce an array of value ["username",
72                     // "password"]
73                     if (dataIt->is_array())
74                     {
75                         if (dataIt->size() == 2)
76                         {
77                             nlohmann::json::iterator userIt2 = dataIt->begin();
78                             nlohmann::json::iterator passIt2 =
79                                 dataIt->begin() + 1;
80                             looksLikePhosphorRest = true;
81                             if (userIt2 != dataIt->end() &&
82                                 passIt2 != dataIt->end())
83                             {
84                                 const std::string* userStr =
85                                     userIt2->get_ptr<const std::string*>();
86                                 const std::string* passStr =
87                                     passIt2->get_ptr<const std::string*>();
88                                 if (userStr != nullptr && passStr != nullptr)
89                                 {
90                                     username = *userStr;
91                                     password = *passStr;
92                                 }
93                             }
94                         }
95                     }
96                     else if (dataIt->is_object())
97                     {
98                         nlohmann::json::iterator userIt2 =
99                             dataIt->find("username");
100                         nlohmann::json::iterator passIt2 =
101                             dataIt->find("password");
102                         if (userIt2 != dataIt->end() &&
103                             passIt2 != dataIt->end())
104                         {
105                             const std::string* userStr =
106                                 userIt2->get_ptr<const std::string*>();
107                             const std::string* passStr =
108                                 passIt2->get_ptr<const std::string*>();
109                             if (userStr != nullptr && passStr != nullptr)
110                             {
111                                 username = *userStr;
112                                 password = *passStr;
113                             }
114                         }
115                     }
116                 }
117             }
118         }
119         else if (boost::starts_with(contentType, "multipart/form-data"))
120         {
121             looksLikePhosphorRest = true;
122             MultipartParser parser;
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