blob: a4559268518006aae2e4a2a37ef8ecb189d775ec [file] [log] [blame]
Ed Tanousf9273472017-02-28 16:05:13 -08001#pragma once
2
Ed Tanousc94ad492019-10-10 15:39:33 -07003#include <app.h>
4#include <common.h>
5#include <http_request.h>
6#include <http_response.h>
Ed Tanous1abe55e2018-09-05 08:30:59 -07007
Ed Tanous4758d5b2017-06-06 15:28:13 -07008#include <boost/container/flat_set.hpp>
Ed Tanous1abe55e2018-09-05 08:30:59 -07009#include <pam_authenticate.hpp>
10#include <persistent_data_middleware.hpp>
Ed Tanous1abe55e2018-09-05 08:30:59 -070011#include <webassets.hpp>
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010012
Gunnar Mills1214b7e2020-06-04 10:11:30 -050013#include <random>
14
Ed Tanous1abe55e2018-09-05 08:30:59 -070015namespace crow
16{
Ed Tanousb4d29f42017-03-24 16:39:25 -070017
Ed Tanous1abe55e2018-09-05 08:30:59 -070018namespace token_authorization
19{
Ed Tanousb4d29f42017-03-24 16:39:25 -070020
Ed Tanous1abe55e2018-09-05 08:30:59 -070021class Middleware
22{
23 public:
24 struct Context
Gunnar Mills1214b7e2020-06-04 10:11:30 -050025 {};
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010026
Ed Tanous1abe55e2018-09-05 08:30:59 -070027 void beforeHandle(crow::Request& req, Response& res, Context& ctx)
28 {
29 if (isOnWhitelist(req))
30 {
31 return;
Ed Tanouse0d918b2018-03-27 17:41:04 -070032 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010033
Zbigniew Kurzynski78158632019-11-05 12:57:37 +010034 const crow::persistent_data::AuthConfigMethods& authMethodsConfig =
35 crow::persistent_data::SessionStore::getInstance()
36 .getAuthMethodsConfig();
37
38 if (req.session == nullptr && authMethodsConfig.xtoken)
39 {
40 req.session = performXtokenAuth(req);
41 }
42 if (req.session == nullptr && authMethodsConfig.cookie)
Ed Tanous1abe55e2018-09-05 08:30:59 -070043 {
Ratan Gupta6f359562019-04-03 10:39:08 +053044 req.session = performCookieAuth(req);
Ed Tanous9bd21fc2018-04-26 16:08:56 -070045 }
Ratan Gupta6f359562019-04-03 10:39:08 +053046 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070047 {
Ed Tanous39e77502019-03-04 17:35:53 -080048 std::string_view authHeader = req.getHeaderValue("Authorization");
Ed Tanous1abe55e2018-09-05 08:30:59 -070049 if (!authHeader.empty())
50 {
51 // Reject any kind of auth other than basic or token
Zbigniew Kurzynski78158632019-11-05 12:57:37 +010052 if (boost::starts_with(authHeader, "Token ") &&
53 authMethodsConfig.sessionToken)
Ed Tanous1abe55e2018-09-05 08:30:59 -070054 {
Ratan Gupta6f359562019-04-03 10:39:08 +053055 req.session = performTokenAuth(authHeader);
Ed Tanous1abe55e2018-09-05 08:30:59 -070056 }
Zbigniew Kurzynski78158632019-11-05 12:57:37 +010057 else if (boost::starts_with(authHeader, "Basic ") &&
58 authMethodsConfig.basic)
Ed Tanous1abe55e2018-09-05 08:30:59 -070059 {
Ratan Gupta6f359562019-04-03 10:39:08 +053060 req.session = performBasicAuth(authHeader);
Ed Tanous1abe55e2018-09-05 08:30:59 -070061 }
62 }
63 }
Ed Tanouse0d918b2018-03-27 17:41:04 -070064
Ratan Gupta6f359562019-04-03 10:39:08 +053065 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070066 {
67 BMCWEB_LOG_WARNING << "[AuthMiddleware] authorization failed";
68
69 // If it's a browser connecting, don't send the HTTP authenticate
70 // header, to avoid possible CSRF attacks with basic auth
71 if (http_helpers::requestPrefersHtml(req))
72 {
73 res.result(boost::beast::http::status::temporary_redirect);
Ed Tanous6b5e77d2018-11-16 14:52:56 -080074 res.addHeader("Location", "/#/login?next=" +
75 http_helpers::urlEncode(req.url));
Ed Tanous1abe55e2018-09-05 08:30:59 -070076 }
77 else
78 {
79 res.result(boost::beast::http::status::unauthorized);
80 // only send the WWW-authenticate header if this isn't a xhr
81 // from the browser. most scripts,
82 if (req.getHeaderValue("User-Agent").empty())
83 {
84 res.addHeader("WWW-Authenticate", "Basic");
85 }
86 }
87
88 res.end();
89 return;
90 }
91
92 // TODO get user privileges here and propagate it via MW Context
93 // else let the request continue unharmed
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010094 }
Ed Tanousf9273472017-02-28 16:05:13 -080095
Ed Tanous1abe55e2018-09-05 08:30:59 -070096 template <typename AllContext>
97 void afterHandle(Request& req, Response& res, Context& ctx,
98 AllContext& allctx)
99 {
100 // TODO(ed) THis should really be handled by the persistent data
101 // middleware, but because it is upstream, it doesn't have access to the
102 // session information. Should the data middleware persist the current
103 // user session?
Ratan Gupta6f359562019-04-03 10:39:08 +0530104 if (req.session != nullptr &&
105 req.session->persistence ==
Ed Tanous1abe55e2018-09-05 08:30:59 -0700106 crow::persistent_data::PersistenceType::SINGLE_REQUEST)
107 {
108 persistent_data::SessionStore::getInstance().removeSession(
Ratan Gupta6f359562019-04-03 10:39:08 +0530109 req.session);
Ed Tanous1abe55e2018-09-05 08:30:59 -0700110 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100111 }
112
Ed Tanous1abe55e2018-09-05 08:30:59 -0700113 private:
114 const std::shared_ptr<crow::persistent_data::UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800115 performBasicAuth(std::string_view auth_header) const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700116 {
117 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100118
Ed Tanous1abe55e2018-09-05 08:30:59 -0700119 std::string authData;
Ed Tanous39e77502019-03-04 17:35:53 -0800120 std::string_view param = auth_header.substr(strlen("Basic "));
Ed Tanous1abe55e2018-09-05 08:30:59 -0700121 if (!crow::utility::base64Decode(param, authData))
122 {
123 return nullptr;
124 }
125 std::size_t separator = authData.find(':');
126 if (separator == std::string::npos)
127 {
128 return nullptr;
129 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100130
Ed Tanous1abe55e2018-09-05 08:30:59 -0700131 std::string user = authData.substr(0, separator);
132 separator += 1;
133 if (separator > authData.size())
134 {
135 return nullptr;
136 }
137 std::string pass = authData.substr(separator);
138
139 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
140
Joseph Reynolds3bf4e632020-02-06 14:44:32 -0600141 int pamrc = pamAuthenticateUser(user, pass);
142 bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
143 if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700144 {
145 return nullptr;
146 }
147
148 // TODO(ed) generateUserSession is a little expensive for basic
149 // auth, as it generates some random identifiers that will never be
150 // used. This should have a "fast" path for when user tokens aren't
151 // needed.
152 // This whole flow needs to be revisited anyway, as we can't be
153 // calling directly into pam for every request
154 return persistent_data::SessionStore::getInstance().generateUserSession(
Joseph Reynolds3bf4e632020-02-06 14:44:32 -0600155 user, crow::persistent_data::PersistenceType::SINGLE_REQUEST,
156 isConfigureSelfOnly);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100157 }
158
Ed Tanous1abe55e2018-09-05 08:30:59 -0700159 const std::shared_ptr<crow::persistent_data::UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800160 performTokenAuth(std::string_view auth_header) const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700161 {
162 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Token authentication";
Ed Tanous8041f312017-04-03 09:47:01 -0700163
Ed Tanous39e77502019-03-04 17:35:53 -0800164 std::string_view token = auth_header.substr(strlen("Token "));
Ed Tanous1abe55e2018-09-05 08:30:59 -0700165 auto session =
166 persistent_data::SessionStore::getInstance().loginSessionByToken(
167 token);
168 return session;
Ed Tanous1ea9f062018-03-27 17:45:20 -0700169 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100170
Ed Tanous1abe55e2018-09-05 08:30:59 -0700171 const std::shared_ptr<crow::persistent_data::UserSession>
172 performXtokenAuth(const crow::Request& req) const
173 {
174 BMCWEB_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100175
Ed Tanous39e77502019-03-04 17:35:53 -0800176 std::string_view token = req.getHeaderValue("X-Auth-Token");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700177 if (token.empty())
178 {
179 return nullptr;
180 }
181 auto session =
182 persistent_data::SessionStore::getInstance().loginSessionByToken(
183 token);
184 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100185 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700186
187 const std::shared_ptr<crow::persistent_data::UserSession>
188 performCookieAuth(const crow::Request& req) const
189 {
190 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
191
Ed Tanous39e77502019-03-04 17:35:53 -0800192 std::string_view cookieValue = req.getHeaderValue("Cookie");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700193 if (cookieValue.empty())
194 {
195 return nullptr;
196 }
197
198 auto startIndex = cookieValue.find("SESSION=");
199 if (startIndex == std::string::npos)
200 {
201 return nullptr;
202 }
203 startIndex += sizeof("SESSION=") - 1;
204 auto endIndex = cookieValue.find(";", startIndex);
205 if (endIndex == std::string::npos)
206 {
207 endIndex = cookieValue.size();
208 }
Ed Tanous39e77502019-03-04 17:35:53 -0800209 std::string_view authKey =
Ed Tanous1abe55e2018-09-05 08:30:59 -0700210 cookieValue.substr(startIndex, endIndex - startIndex);
211
212 const std::shared_ptr<crow::persistent_data::UserSession> session =
213 persistent_data::SessionStore::getInstance().loginSessionByToken(
214 authKey);
215 if (session == nullptr)
216 {
217 return nullptr;
218 }
Ed Tanous1e439872018-05-18 11:48:52 -0700219#ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
Ed Tanous1abe55e2018-09-05 08:30:59 -0700220 // RFC7231 defines methods that need csrf protection
221 if (req.method() != "GET"_method)
222 {
Ed Tanous39e77502019-03-04 17:35:53 -0800223 std::string_view csrf = req.getHeaderValue("X-XSRF-TOKEN");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700224 // Make sure both tokens are filled
225 if (csrf.empty() || session->csrfToken.empty())
226 {
227 return nullptr;
228 }
Ed Tanous51dae672018-09-05 16:07:32 -0700229
230 if (csrf.size() != crow::persistent_data::sessionTokenSize)
231 {
232 return nullptr;
233 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700234 // Reject if csrf token not available
Ed Tanous51dae672018-09-05 16:07:32 -0700235 if (!crow::utility::constantTimeStringCompare(csrf,
236 session->csrfToken))
Ed Tanous1abe55e2018-09-05 08:30:59 -0700237 {
238 return nullptr;
239 }
240 }
Ed Tanous1e439872018-05-18 11:48:52 -0700241#endif
James Feistf8aa3d22020-04-08 18:32:33 -0700242 session->cookieAuth = true;
Ed Tanous1abe55e2018-09-05 08:30:59 -0700243 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100244 }
245
Ed Tanous1abe55e2018-09-05 08:30:59 -0700246 // checks if request can be forwarded without authentication
247 bool isOnWhitelist(const crow::Request& req) const
248 {
249 // it's allowed to GET root node without authentica tion
250 if ("GET"_method == req.method())
251 {
252 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/" ||
253 req.url == "/redfish" || req.url == "/redfish/" ||
254 req.url == "/redfish/v1/odata" ||
255 req.url == "/redfish/v1/odata/")
256 {
257 return true;
258 }
259 else if (crow::webassets::routes.find(std::string(req.url)) !=
260 crow::webassets::routes.end())
261 {
262 return true;
263 }
264 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100265
Ed Tanous1abe55e2018-09-05 08:30:59 -0700266 // it's allowed to POST on session collection & login without
267 // authentication
268 if ("POST"_method == req.method())
269 {
270 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
271 (req.url == "/redfish/v1/SessionService/Sessions/") ||
272 (req.url == "/login"))
273 {
274 return true;
275 }
276 }
277
278 return false;
279 }
Ed Tanous99923322017-03-03 14:21:24 -0800280};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700281
Ed Tanousba9f9a62017-10-11 16:40:35 -0700282// TODO(ed) see if there is a better way to allow middlewares to request
283// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700284// Possibly an init function on first construction?
Gunnar Mills1214b7e2020-06-04 10:11:30 -0500285template <typename... Middlewares>
286void requestRoutes(Crow<Middlewares...>& app)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700287{
288 static_assert(
289 black_magic::Contains<persistent_data::Middleware,
290 Middlewares...>::value,
291 "token_authorization middleware must be enabled in app to use "
292 "auth routes");
Ed Tanousc7a29d32019-10-23 13:30:04 -0700293
Ed Tanous1abe55e2018-09-05 08:30:59 -0700294 BMCWEB_ROUTE(app, "/login")
295 .methods(
Ed Tanousfd4859a2019-10-23 13:31:38 -0700296 "POST"_method)([](const crow::Request& req, crow::Response& res) {
Ed Tanous39e77502019-03-04 17:35:53 -0800297 std::string_view contentType = req.getHeaderValue("content-type");
298 std::string_view username;
299 std::string_view password;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700300
Ed Tanousc7a29d32019-10-23 13:30:04 -0700301 bool looksLikePhosphorRest = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800302
Ed Tanous1abe55e2018-09-05 08:30:59 -0700303 // This object needs to be declared at this scope so the strings
304 // within it are not destroyed before we can use them
305 nlohmann::json loginCredentials;
306 // Check if auth was provided by a payload
Ed Tanousfdf43a32019-07-31 16:52:24 -0700307 if (boost::starts_with(contentType, "application/json"))
Ed Tanous1abe55e2018-09-05 08:30:59 -0700308 {
309 loginCredentials =
310 nlohmann::json::parse(req.body, nullptr, false);
311 if (loginCredentials.is_discarded())
312 {
Ed Tanousfdf43a32019-07-31 16:52:24 -0700313 BMCWEB_LOG_DEBUG << "Bad json in request";
Ed Tanous1abe55e2018-09-05 08:30:59 -0700314 res.result(boost::beast::http::status::bad_request);
315 res.end();
316 return;
317 }
318
319 // check for username/password in the root object
320 // THis method is how intel APIs authenticate
321 nlohmann::json::iterator userIt =
322 loginCredentials.find("username");
323 nlohmann::json::iterator passIt =
324 loginCredentials.find("password");
325 if (userIt != loginCredentials.end() &&
326 passIt != loginCredentials.end())
327 {
328 const std::string* userStr =
329 userIt->get_ptr<const std::string*>();
330 const std::string* passStr =
331 passIt->get_ptr<const std::string*>();
332 if (userStr != nullptr && passStr != nullptr)
333 {
334 username = *userStr;
335 password = *passStr;
336 }
337 }
338 else
339 {
340 // Openbmc appears to push a data object that contains the
341 // same keys (username and password), attempt to use that
342 auto dataIt = loginCredentials.find("data");
343 if (dataIt != loginCredentials.end())
344 {
345 // Some apis produce an array of value ["username",
346 // "password"]
347 if (dataIt->is_array())
348 {
349 if (dataIt->size() == 2)
350 {
351 nlohmann::json::iterator userIt2 =
352 dataIt->begin();
353 nlohmann::json::iterator passIt2 =
354 dataIt->begin() + 1;
Ed Tanousc7a29d32019-10-23 13:30:04 -0700355 looksLikePhosphorRest = true;
Ed Tanous1abe55e2018-09-05 08:30:59 -0700356 if (userIt2 != dataIt->end() &&
357 passIt2 != dataIt->end())
358 {
359 const std::string* userStr =
360 userIt2->get_ptr<const std::string*>();
361 const std::string* passStr =
362 passIt2->get_ptr<const std::string*>();
363 if (userStr != nullptr &&
364 passStr != nullptr)
365 {
366 username = *userStr;
367 password = *passStr;
368 }
369 }
370 }
371 }
372 else if (dataIt->is_object())
373 {
374 nlohmann::json::iterator userIt2 =
375 dataIt->find("username");
376 nlohmann::json::iterator passIt2 =
377 dataIt->find("password");
378 if (userIt2 != dataIt->end() &&
379 passIt2 != dataIt->end())
380 {
381 const std::string* userStr =
382 userIt2->get_ptr<const std::string*>();
383 const std::string* passStr =
384 passIt2->get_ptr<const std::string*>();
385 if (userStr != nullptr && passStr != nullptr)
386 {
387 username = *userStr;
388 password = *passStr;
389 }
390 }
391 }
392 }
393 }
394 }
395 else
396 {
397 // check if auth was provided as a headers
398 username = req.getHeaderValue("username");
399 password = req.getHeaderValue("password");
400 }
401
402 if (!username.empty() && !password.empty())
403 {
Joseph Reynolds3bf4e632020-02-06 14:44:32 -0600404 int pamrc = pamAuthenticateUser(username, password);
405 bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
406 if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700407 {
408 res.result(boost::beast::http::status::unauthorized);
409 }
410 else
411 {
Joseph Reynolds3bf4e632020-02-06 14:44:32 -0600412 auto session =
413 persistent_data::SessionStore::getInstance()
414 .generateUserSession(
415 username,
416 crow::persistent_data::PersistenceType::TIMEOUT,
417 isConfigureSelfOnly);
Ed Tanous1abe55e2018-09-05 08:30:59 -0700418
Ed Tanousc7a29d32019-10-23 13:30:04 -0700419 if (looksLikePhosphorRest)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700420 {
Ed Tanousc7a29d32019-10-23 13:30:04 -0700421 // Phosphor-Rest requires a very specific login
422 // structure, and doesn't actually look at the status
423 // code.
Ed Tanous1abe55e2018-09-05 08:30:59 -0700424 // TODO(ed).... Fix that upstream
425 res.jsonValue = {
426 {"data",
427 "User '" + std::string(username) + "' logged in"},
428 {"message", "200 OK"},
429 {"status", "ok"}};
430
431 // Hack alert. Boost beast by default doesn't let you
432 // declare multiple headers of the same name, and in
433 // most cases this is fine. Unfortunately here we need
434 // to set the Session cookie, which requires the
435 // httpOnly attribute, as well as the XSRF cookie, which
436 // requires it to not have an httpOnly attribute. To get
437 // the behavior we want, we simply inject the second
438 // "set-cookie" string into the value header, and get
439 // the result we want, even though we are technicaly
440 // declaring two headers here.
441 res.addHeader("Set-Cookie",
442 "XSRF-TOKEN=" + session->csrfToken +
443 "; Secure\r\nSet-Cookie: SESSION=" +
444 session->sessionToken +
445 "; Secure; HttpOnly");
446 }
447 else
448 {
449 // if content type is json, assume json token
450 res.jsonValue = {{"token", session->sessionToken}};
451 }
452 }
453 }
454 else
455 {
Ed Tanousfdf43a32019-07-31 16:52:24 -0700456 BMCWEB_LOG_DEBUG << "Couldn't interpret password";
Ed Tanous1abe55e2018-09-05 08:30:59 -0700457 res.result(boost::beast::http::status::bad_request);
458 }
459 res.end();
460 });
461
462 BMCWEB_ROUTE(app, "/logout")
Ratan Gupta6f359562019-04-03 10:39:08 +0530463 .methods("POST"_method)(
Ed Tanousfd4859a2019-10-23 13:31:38 -0700464 [](const crow::Request& req, crow::Response& res) {
Ratan Gupta6f359562019-04-03 10:39:08 +0530465 auto& session = req.session;
466 if (session != nullptr)
467 {
468 res.jsonValue = {
469 {"data", "User '" + session->username + "' logged out"},
470 {"message", "200 OK"},
471 {"status", "ok"}};
Anthony Wilsonaf713a62019-03-15 15:40:58 -0500472
Ratan Gupta6f359562019-04-03 10:39:08 +0530473 persistent_data::SessionStore::getInstance().removeSession(
474 session);
475 }
476 res.end();
477 return;
478 });
Ed Tanous911ac312017-08-15 09:37:42 -0700479}
Ed Tanous1abe55e2018-09-05 08:30:59 -0700480} // namespace token_authorization
481} // namespace crow