xref: /openbmc/bmcweb/include/login_routes.hpp (revision 994fd86a)
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                     asyncResp->res.addHeader(
200                         boost::beast::http::field::set_cookie,
201                         "XSRF-TOKEN=" + session->csrfToken +
202                             "; SameSite=Strict; Secure");
203                     asyncResp->res.addHeader(
204                         boost::beast::http::field::set_cookie,
205                         "SESSION=" + session->sessionToken +
206                             "; SameSite=Strict; Secure; HttpOnly");
207                 }
208                 else
209                 {
210                     // if content type is json, assume json token
211                     asyncResp->res.jsonValue["token"] = session->sessionToken;
212                 }
213             }
214         }
215         else
216         {
217             BMCWEB_LOG_DEBUG << "Couldn't interpret password";
218             asyncResp->res.result(boost::beast::http::status::bad_request);
219         }
220         });
221 
222     BMCWEB_ROUTE(app, "/logout")
223         .methods(boost::beast::http::verb::post)(
224             [](const crow::Request& req,
225                const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) {
226         const auto& session = req.session;
227         if (session != nullptr)
228         {
229             asyncResp->res.jsonValue["data"] = "User '" + session->username +
230                                                "' logged out";
231             asyncResp->res.jsonValue["message"] = "200 OK";
232             asyncResp->res.jsonValue["status"] = "ok";
233 
234             asyncResp->res.addHeader("Set-Cookie",
235                                      "SESSION="
236                                      "; SameSite=Strict; Secure; HttpOnly; "
237                                      "expires=Thu, 01 Jan 1970 00:00:00 GMT");
238 
239             persistent_data::SessionStore::getInstance().removeSession(session);
240         }
241         });
242 }
243 } // namespace login_routes
244 } // namespace crow
245