xref: /openbmc/bmcweb/include/login_routes.hpp (revision f263e09c)
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,
41                                                      false);
42             if (loginCredentials.is_discarded())
43             {
44                 BMCWEB_LOG_DEBUG << "Bad json in request";
45                 asyncResp->res.result(boost::beast::http::status::bad_request);
46                 return;
47             }
48 
49             // check for username/password in the root object
50             // THis method is how intel APIs authenticate
51             nlohmann::json::iterator userIt = loginCredentials.find("username");
52             nlohmann::json::iterator passIt = loginCredentials.find("password");
53             if (userIt != loginCredentials.end() &&
54                 passIt != loginCredentials.end())
55             {
56                 const std::string* userStr =
57                     userIt->get_ptr<const std::string*>();
58                 const std::string* passStr =
59                     passIt->get_ptr<const std::string*>();
60                 if (userStr != nullptr && passStr != nullptr)
61                 {
62                     username = *userStr;
63                     password = *passStr;
64                 }
65             }
66             else
67             {
68                 // Openbmc appears to push a data object that contains the
69                 // same keys (username and password), attempt to use that
70                 auto dataIt = loginCredentials.find("data");
71                 if (dataIt != loginCredentials.end())
72                 {
73                     // Some apis produce an array of value ["username",
74                     // "password"]
75                     if (dataIt->is_array())
76                     {
77                         if (dataIt->size() == 2)
78                         {
79                             nlohmann::json::iterator userIt2 = dataIt->begin();
80                             nlohmann::json::iterator passIt2 = dataIt->begin() +
81                                                                1;
82                             looksLikePhosphorRest = true;
83                             if (userIt2 != dataIt->end() &&
84                                 passIt2 != dataIt->end())
85                             {
86                                 const std::string* userStr =
87                                     userIt2->get_ptr<const std::string*>();
88                                 const std::string* passStr =
89                                     passIt2->get_ptr<const std::string*>();
90                                 if (userStr != nullptr && passStr != nullptr)
91                                 {
92                                     username = *userStr;
93                                     password = *passStr;
94                                 }
95                             }
96                         }
97                     }
98                     else if (dataIt->is_object())
99                     {
100                         nlohmann::json::iterator userIt2 =
101                             dataIt->find("username");
102                         nlohmann::json::iterator passIt2 =
103                             dataIt->find("password");
104                         if (userIt2 != dataIt->end() &&
105                             passIt2 != dataIt->end())
106                         {
107                             const std::string* userStr =
108                                 userIt2->get_ptr<const std::string*>();
109                             const std::string* passStr =
110                                 passIt2->get_ptr<const std::string*>();
111                             if (userStr != nullptr && passStr != nullptr)
112                             {
113                                 username = *userStr;
114                                 password = *passStr;
115                             }
116                         }
117                     }
118                 }
119             }
120         }
121         else if (contentType.starts_with("multipart/form-data"))
122         {
123             looksLikePhosphorRest = true;
124             ParserError ec = parser.parse(req);
125             if (ec != ParserError::PARSER_SUCCESS)
126             {
127                 // handle error
128                 BMCWEB_LOG_ERROR << "MIME parse failed, ec : "
129                                  << static_cast<int>(ec);
130                 asyncResp->res.result(boost::beast::http::status::bad_request);
131                 return;
132             }
133 
134             for (const FormPart& formpart : parser.mime_fields)
135             {
136                 boost::beast::http::fields::const_iterator it =
137                     formpart.fields.find("Content-Disposition");
138                 if (it == formpart.fields.end())
139                 {
140                     BMCWEB_LOG_ERROR << "Couldn't find Content-Disposition";
141                     asyncResp->res.result(
142                         boost::beast::http::status::bad_request);
143                     continue;
144                 }
145 
146                 BMCWEB_LOG_INFO << "Parsing value " << it->value();
147 
148                 if (it->value() == "form-data; name=\"username\"")
149                 {
150                     username = formpart.content;
151                 }
152                 else if (it->value() == "form-data; name=\"password\"")
153                 {
154                     password = formpart.content;
155                 }
156                 else
157                 {
158                     BMCWEB_LOG_INFO << "Extra format, ignore it."
159                                     << it->value();
160                 }
161             }
162         }
163         else
164         {
165             // check if auth was provided as a headers
166             username = req.getHeaderValue("username");
167             password = req.getHeaderValue("password");
168         }
169 
170         if (!username.empty() && !password.empty())
171         {
172             int pamrc = pamAuthenticateUser(username, password);
173             bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
174             if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
175             {
176                 asyncResp->res.result(boost::beast::http::status::unauthorized);
177             }
178             else
179             {
180                 auto session =
181                     persistent_data::SessionStore::getInstance()
182                         .generateUserSession(
183                             username, req.ipAddress, std::nullopt,
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"] = "User '" + session->username +
239                                                "' 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