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 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, false); 41 if (loginCredentials.is_discarded()) 42 { 43 BMCWEB_LOG_DEBUG << "Bad json in request"; 44 asyncResp->res.result(boost::beast::http::status::bad_request); 45 return; 46 } 47 48 // check for username/password in the root object 49 // THis method is how intel APIs authenticate 50 nlohmann::json::iterator userIt = loginCredentials.find("username"); 51 nlohmann::json::iterator passIt = loginCredentials.find("password"); 52 if (userIt != loginCredentials.end() && 53 passIt != loginCredentials.end()) 54 { 55 const std::string* userStr = 56 userIt->get_ptr<const std::string*>(); 57 const std::string* passStr = 58 passIt->get_ptr<const std::string*>(); 59 if (userStr != nullptr && passStr != nullptr) 60 { 61 username = *userStr; 62 password = *passStr; 63 } 64 } 65 else 66 { 67 // Openbmc appears to push a data object that contains the 68 // same keys (username and password), attempt to use that 69 auto dataIt = loginCredentials.find("data"); 70 if (dataIt != loginCredentials.end()) 71 { 72 // Some apis produce an array of value ["username", 73 // "password"] 74 if (dataIt->is_array()) 75 { 76 if (dataIt->size() == 2) 77 { 78 nlohmann::json::iterator userIt2 = dataIt->begin(); 79 nlohmann::json::iterator passIt2 = 80 dataIt->begin() + 1; 81 looksLikePhosphorRest = true; 82 if (userIt2 != dataIt->end() && 83 passIt2 != dataIt->end()) 84 { 85 const std::string* userStr = 86 userIt2->get_ptr<const std::string*>(); 87 const std::string* passStr = 88 passIt2->get_ptr<const std::string*>(); 89 if (userStr != nullptr && passStr != nullptr) 90 { 91 username = *userStr; 92 password = *passStr; 93 } 94 } 95 } 96 } 97 else if (dataIt->is_object()) 98 { 99 nlohmann::json::iterator userIt2 = 100 dataIt->find("username"); 101 nlohmann::json::iterator passIt2 = 102 dataIt->find("password"); 103 if (userIt2 != dataIt->end() && 104 passIt2 != dataIt->end()) 105 { 106 const std::string* userStr = 107 userIt2->get_ptr<const std::string*>(); 108 const std::string* passStr = 109 passIt2->get_ptr<const std::string*>(); 110 if (userStr != nullptr && passStr != nullptr) 111 { 112 username = *userStr; 113 password = *passStr; 114 } 115 } 116 } 117 } 118 } 119 } 120 else if (contentType.starts_with("multipart/form-data")) 121 { 122 looksLikePhosphorRest = true; 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