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