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( 25 boost::beast::http::verb:: 26 post)([](const crow::Request& req, 27 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 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 (boost::starts_with(contentType, "application/json")) 39 { 40 loginCredentials = 41 nlohmann::json::parse(req.body, nullptr, false); 42 if (loginCredentials.is_discarded()) 43 { 44 BMCWEB_LOG_DEBUG << "Bad json in request"; 45 asyncResp->res.result( 46 boost::beast::http::status::bad_request); 47 return; 48 } 49 50 // check for username/password in the root object 51 // THis method is how intel APIs authenticate 52 nlohmann::json::iterator userIt = 53 loginCredentials.find("username"); 54 nlohmann::json::iterator passIt = 55 loginCredentials.find("password"); 56 if (userIt != loginCredentials.end() && 57 passIt != loginCredentials.end()) 58 { 59 const std::string* userStr = 60 userIt->get_ptr<const std::string*>(); 61 const std::string* passStr = 62 passIt->get_ptr<const std::string*>(); 63 if (userStr != nullptr && passStr != nullptr) 64 { 65 username = *userStr; 66 password = *passStr; 67 } 68 } 69 else 70 { 71 // Openbmc appears to push a data object that contains the 72 // same keys (username and password), attempt to use that 73 auto dataIt = loginCredentials.find("data"); 74 if (dataIt != loginCredentials.end()) 75 { 76 // Some apis produce an array of value ["username", 77 // "password"] 78 if (dataIt->is_array()) 79 { 80 if (dataIt->size() == 2) 81 { 82 nlohmann::json::iterator userIt2 = 83 dataIt->begin(); 84 nlohmann::json::iterator passIt2 = 85 dataIt->begin() + 1; 86 looksLikePhosphorRest = true; 87 if (userIt2 != dataIt->end() && 88 passIt2 != dataIt->end()) 89 { 90 const std::string* userStr = 91 userIt2->get_ptr<const std::string*>(); 92 const std::string* passStr = 93 passIt2->get_ptr<const std::string*>(); 94 if (userStr != nullptr && 95 passStr != nullptr) 96 { 97 username = *userStr; 98 password = *passStr; 99 } 100 } 101 } 102 } 103 else if (dataIt->is_object()) 104 { 105 nlohmann::json::iterator userIt2 = 106 dataIt->find("username"); 107 nlohmann::json::iterator passIt2 = 108 dataIt->find("password"); 109 if (userIt2 != dataIt->end() && 110 passIt2 != dataIt->end()) 111 { 112 const std::string* userStr = 113 userIt2->get_ptr<const std::string*>(); 114 const std::string* passStr = 115 passIt2->get_ptr<const std::string*>(); 116 if (userStr != nullptr && passStr != nullptr) 117 { 118 username = *userStr; 119 password = *passStr; 120 } 121 } 122 } 123 } 124 } 125 } 126 else if (boost::starts_with(contentType, "multipart/form-data")) 127 { 128 looksLikePhosphorRest = true; 129 MultipartParser parser; 130 ParserError ec = parser.parse(req); 131 if (ec != ParserError::PARSER_SUCCESS) 132 { 133 // handle error 134 BMCWEB_LOG_ERROR << "MIME parse failed, ec : " 135 << static_cast<int>(ec); 136 asyncResp->res.result( 137 boost::beast::http::status::bad_request); 138 return; 139 } 140 141 for (const FormPart& formpart : parser.mime_fields) 142 { 143 boost::beast::http::fields::const_iterator it = 144 formpart.fields.find("Content-Disposition"); 145 if (it == formpart.fields.end()) 146 { 147 BMCWEB_LOG_ERROR << "Couldn't find Content-Disposition"; 148 asyncResp->res.result( 149 boost::beast::http::status::bad_request); 150 continue; 151 } 152 153 BMCWEB_LOG_INFO << "Parsing value " << it->value(); 154 155 if (it->value() == "form-data; name=\"username\"") 156 { 157 username = formpart.content; 158 } 159 else if (it->value() == "form-data; name=\"password\"") 160 { 161 password = formpart.content; 162 } 163 else 164 { 165 BMCWEB_LOG_INFO << "Extra format, ignore it." 166 << it->value(); 167 } 168 } 169 } 170 else 171 { 172 // check if auth was provided as a headers 173 username = req.getHeaderValue("username"); 174 password = req.getHeaderValue("password"); 175 } 176 177 if (!username.empty() && !password.empty()) 178 { 179 int pamrc = pamAuthenticateUser(username, password); 180 bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD; 181 if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly) 182 { 183 asyncResp->res.result( 184 boost::beast::http::status::unauthorized); 185 } 186 else 187 { 188 std::string unsupportedClientId; 189 auto session = 190 persistent_data::SessionStore::getInstance() 191 .generateUserSession( 192 username, req.ipAddress, unsupportedClientId, 193 persistent_data::PersistenceType::TIMEOUT, 194 isConfigureSelfOnly); 195 196 if (looksLikePhosphorRest) 197 { 198 // Phosphor-Rest requires a very specific login 199 // structure, and doesn't actually look at the status 200 // code. 201 // TODO(ed).... Fix that upstream 202 203 asyncResp->res.jsonValue["data"] = 204 "User '" + std::string(username) + "' logged in"; 205 asyncResp->res.jsonValue["message"] = "200 OK"; 206 asyncResp->res.jsonValue["status"] = "ok"; 207 208 // Hack alert. Boost beast by default doesn't let you 209 // declare multiple headers of the same name, and in 210 // most cases this is fine. Unfortunately here we need 211 // to set the Session cookie, which requires the 212 // httpOnly attribute, as well as the XSRF cookie, which 213 // requires it to not have an httpOnly attribute. To get 214 // the behavior we want, we simply inject the second 215 // "set-cookie" string into the value header, and get 216 // the result we want, even though we are technicaly 217 // declaring two headers here. 218 asyncResp->res.addHeader( 219 "Set-Cookie", 220 "XSRF-TOKEN=" + session->csrfToken + 221 "; SameSite=Strict; Secure\r\nSet-Cookie: " 222 "SESSION=" + 223 session->sessionToken + 224 "; SameSite=Strict; Secure; HttpOnly"); 225 } 226 else 227 { 228 // if content type is json, assume json token 229 asyncResp->res.jsonValue["token"] = 230 session->sessionToken; 231 } 232 } 233 } 234 else 235 { 236 BMCWEB_LOG_DEBUG << "Couldn't interpret password"; 237 asyncResp->res.result(boost::beast::http::status::bad_request); 238 } 239 }); 240 241 BMCWEB_ROUTE(app, "/logout") 242 .methods(boost::beast::http::verb::post)( 243 [](const crow::Request& req, 244 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) { 245 const auto& session = req.session; 246 if (session != nullptr) 247 { 248 asyncResp->res.jsonValue["data"] = 249 "User '" + session->username + "' logged out"; 250 asyncResp->res.jsonValue["message"] = "200 OK"; 251 asyncResp->res.jsonValue["status"] = "ok"; 252 253 persistent_data::SessionStore::getInstance().removeSession( 254 session); 255 } 256 }); 257 } 258 } // namespace login_routes 259 } // namespace crow 260