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 = 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(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 = 81 dataIt->begin() + 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 // 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