xref: /openbmc/bmcweb/include/login_routes.hpp (revision cd40b060ee2df5469077a70d15590f86158f2c60)
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 inline void
21     afterAuthenticateUser(const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
22                           std::string_view username,
23                           const boost::asio::ip::address& ipAddress,
24                           int32_t pamrc)
25 {
26     bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
27     if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
28     {
29         asyncResp->res.result(boost::beast::http::status::unauthorized);
30         return;
31     }
32     auto session =
33         persistent_data::SessionStore::getInstance().generateUserSession(
34             username, ipAddress, std::nullopt,
35             persistent_data::PersistenceType::TIMEOUT, isConfigureSelfOnly);
36     // if content type is json, assume json token
37     asyncResp->res.jsonValue["token"] = session->sessionToken;
38 }
39 
40 inline void handleLogin(const crow::Request& req,
41                         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
42 {
43     MultipartParser parser;
44     std::string_view contentType = req.getHeaderValue("content-type");
45     std::string_view username;
46     std::string_view password;
47 
48     // This object needs to be declared at this scope so the strings
49     // within it are not destroyed before we can use them
50     nlohmann::json loginCredentials;
51     // Check if auth was provided by a payload
52     if (contentType.starts_with("application/json"))
53     {
54         loginCredentials = nlohmann::json::parse(req.body(), nullptr, false);
55         if (loginCredentials.is_discarded())
56         {
57             BMCWEB_LOG_DEBUG("Bad json in request");
58             asyncResp->res.result(boost::beast::http::status::bad_request);
59             return;
60         }
61 
62         // check for username/password in the root object
63         // THis method is how intel APIs authenticate
64         nlohmann::json::iterator userIt = loginCredentials.find("username");
65         nlohmann::json::iterator passIt = loginCredentials.find("password");
66         if (userIt != loginCredentials.end() &&
67             passIt != loginCredentials.end())
68         {
69             const std::string* userStr = userIt->get_ptr<const std::string*>();
70             const std::string* passStr = passIt->get_ptr<const std::string*>();
71             if (userStr != nullptr && passStr != nullptr)
72             {
73                 username = *userStr;
74                 password = *passStr;
75             }
76         }
77         else
78         {
79             // Openbmc appears to push a data object that contains the
80             // same keys (username and password), attempt to use that
81             auto dataIt = loginCredentials.find("data");
82             if (dataIt != loginCredentials.end())
83             {
84                 // Some apis produce an array of value ["username",
85                 // "password"]
86                 if (dataIt->is_array())
87                 {
88                     if (dataIt->size() == 2)
89                     {
90                         nlohmann::json::iterator userIt2 = dataIt->begin();
91                         nlohmann::json::iterator passIt2 = dataIt->begin() + 1;
92                         if (userIt2 != dataIt->end() &&
93                             passIt2 != dataIt->end())
94                         {
95                             const std::string* userStr =
96                                 userIt2->get_ptr<const std::string*>();
97                             const std::string* passStr =
98                                 passIt2->get_ptr<const std::string*>();
99                             if (userStr != nullptr && passStr != nullptr)
100                             {
101                                 username = *userStr;
102                                 password = *passStr;
103                             }
104                         }
105                     }
106                 }
107                 else if (dataIt->is_object())
108                 {
109                     nlohmann::json::iterator userIt2 = dataIt->find("username");
110                     nlohmann::json::iterator passIt2 = dataIt->find("password");
111                     if (userIt2 != dataIt->end() && passIt2 != dataIt->end())
112                     {
113                         const std::string* userStr =
114                             userIt2->get_ptr<const std::string*>();
115                         const std::string* passStr =
116                             passIt2->get_ptr<const std::string*>();
117                         if (userStr != nullptr && passStr != nullptr)
118                         {
119                             username = *userStr;
120                             password = *passStr;
121                         }
122                     }
123                 }
124             }
125         }
126     }
127     else if (contentType.starts_with("multipart/form-data"))
128     {
129         ParserError ec = parser.parse(req);
130         if (ec != ParserError::PARSER_SUCCESS)
131         {
132             // handle error
133             BMCWEB_LOG_ERROR("MIME parse failed, ec : {}",
134                              static_cast<int>(ec));
135             asyncResp->res.result(boost::beast::http::status::bad_request);
136             return;
137         }
138 
139         for (const FormPart& formpart : parser.mime_fields)
140         {
141             boost::beast::http::fields::const_iterator it =
142                 formpart.fields.find("Content-Disposition");
143             if (it == formpart.fields.end())
144             {
145                 BMCWEB_LOG_ERROR("Couldn't find Content-Disposition");
146                 asyncResp->res.result(boost::beast::http::status::bad_request);
147                 continue;
148             }
149 
150             BMCWEB_LOG_INFO("Parsing value {}", it->value());
151 
152             if (it->value() == "form-data; name=\"username\"")
153             {
154                 username = formpart.content;
155             }
156             else if (it->value() == "form-data; name=\"password\"")
157             {
158                 password = formpart.content;
159             }
160             else
161             {
162                 BMCWEB_LOG_INFO("Extra format, ignore it.{}", it->value());
163             }
164         }
165     }
166     else
167     {
168         // check if auth was provided as a headers
169         username = req.getHeaderValue("username");
170         password = req.getHeaderValue("password");
171     }
172 
173     if (username.empty() || password.empty())
174     {
175         BMCWEB_LOG_DEBUG("Couldn't interpret password");
176         asyncResp->res.result(boost::beast::http::status::bad_request);
177         return;
178     }
179     int pamrc = pamAuthenticateUser(username, password);
180     afterAuthenticateUser(asyncResp, username, req.ipAddress, pamrc);
181 }
182 
183 inline void handleLogout(const crow::Request& req,
184                          const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
185 {
186     const auto& session = req.session;
187     if (session != nullptr)
188     {
189         asyncResp->res.jsonValue["data"] = "User '" + session->username +
190                                            "' logged out";
191         asyncResp->res.jsonValue["message"] = "200 OK";
192         asyncResp->res.jsonValue["status"] = "ok";
193 
194         asyncResp->res.addHeader("Set-Cookie",
195                                  "SESSION="
196                                  "; SameSite=Strict; Secure; HttpOnly; "
197                                  "expires=Thu, 01 Jan 1970 00:00:00 GMT");
198         asyncResp->res.addHeader("Clear-Site-Data",
199                                  R"("cache","cookies","storage")");
200         persistent_data::SessionStore::getInstance().removeSession(session);
201     }
202 }
203 
204 inline void requestRoutes(App& app)
205 {
206     BMCWEB_ROUTE(app, "/login")
207         .methods(boost::beast::http::verb::post)(handleLogin);
208 
209     BMCWEB_ROUTE(app, "/logout")
210         .methods(boost::beast::http::verb::post)(handleLogout);
211 }
212 } // namespace login_routes
213 } // namespace crow
214