blob: 5842ae5362e690166b5215ced7310d59643baf2d [file] [log] [blame]
Ed Tanousf9273472017-02-28 16:05:13 -08001#pragma once
2
Ed Tanous911ac312017-08-15 09:37:42 -07003#include <crow/app.h>
Ed Tanouse0d918b2018-03-27 17:41:04 -07004#include <crow/common.h>
Ed Tanousf9273472017-02-28 16:05:13 -08005#include <crow/http_request.h>
6#include <crow/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>
Joseph Reynolds8fd315a2019-09-12 12:02:33 -05009#include <error_messages.hpp>
Ed Tanous1abe55e2018-09-05 08:30:59 -070010#include <pam_authenticate.hpp>
11#include <persistent_data_middleware.hpp>
12#include <random>
13#include <webassets.hpp>
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010014
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
25 {
Ed Tanous1abe55e2018-09-05 08:30:59 -070026 };
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010027
Ed Tanous1abe55e2018-09-05 08:30:59 -070028 void beforeHandle(crow::Request& req, Response& res, Context& ctx)
29 {
30 if (isOnWhitelist(req))
31 {
32 return;
Ed Tanouse0d918b2018-03-27 17:41:04 -070033 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010034
Ratan Gupta6f359562019-04-03 10:39:08 +053035 req.session = performXtokenAuth(req);
36 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070037 {
Ratan Gupta6f359562019-04-03 10:39:08 +053038 req.session = performCookieAuth(req);
Ed Tanous9bd21fc2018-04-26 16:08:56 -070039 }
Ratan Gupta6f359562019-04-03 10:39:08 +053040 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070041 {
Ed Tanous39e77502019-03-04 17:35:53 -080042 std::string_view authHeader = req.getHeaderValue("Authorization");
Ed Tanous1abe55e2018-09-05 08:30:59 -070043 if (!authHeader.empty())
44 {
45 // Reject any kind of auth other than basic or token
46 if (boost::starts_with(authHeader, "Token "))
47 {
Ratan Gupta6f359562019-04-03 10:39:08 +053048 req.session = performTokenAuth(authHeader);
Ed Tanous1abe55e2018-09-05 08:30:59 -070049 }
50 else if (boost::starts_with(authHeader, "Basic "))
51 {
Ratan Gupta6f359562019-04-03 10:39:08 +053052 req.session = performBasicAuth(authHeader);
Ed Tanous1abe55e2018-09-05 08:30:59 -070053 }
54 }
55 }
Ed Tanouse0d918b2018-03-27 17:41:04 -070056
Ratan Gupta6f359562019-04-03 10:39:08 +053057 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070058 {
59 BMCWEB_LOG_WARNING << "[AuthMiddleware] authorization failed";
60
61 // If it's a browser connecting, don't send the HTTP authenticate
62 // header, to avoid possible CSRF attacks with basic auth
63 if (http_helpers::requestPrefersHtml(req))
64 {
65 res.result(boost::beast::http::status::temporary_redirect);
Ed Tanous6b5e77d2018-11-16 14:52:56 -080066 res.addHeader("Location", "/#/login?next=" +
67 http_helpers::urlEncode(req.url));
Ed Tanous1abe55e2018-09-05 08:30:59 -070068 }
69 else
70 {
71 res.result(boost::beast::http::status::unauthorized);
72 // only send the WWW-authenticate header if this isn't a xhr
73 // from the browser. most scripts,
74 if (req.getHeaderValue("User-Agent").empty())
75 {
76 res.addHeader("WWW-Authenticate", "Basic");
77 }
78 }
79
80 res.end();
81 return;
82 }
83
84 // TODO get user privileges here and propagate it via MW Context
85 // else let the request continue unharmed
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010086 }
Ed Tanousf9273472017-02-28 16:05:13 -080087
Ed Tanous1abe55e2018-09-05 08:30:59 -070088 template <typename AllContext>
89 void afterHandle(Request& req, Response& res, Context& ctx,
90 AllContext& allctx)
91 {
92 // TODO(ed) THis should really be handled by the persistent data
93 // middleware, but because it is upstream, it doesn't have access to the
94 // session information. Should the data middleware persist the current
95 // user session?
Ratan Gupta6f359562019-04-03 10:39:08 +053096 if (req.session != nullptr &&
97 req.session->persistence ==
Ed Tanous1abe55e2018-09-05 08:30:59 -070098 crow::persistent_data::PersistenceType::SINGLE_REQUEST)
99 {
100 persistent_data::SessionStore::getInstance().removeSession(
Ratan Gupta6f359562019-04-03 10:39:08 +0530101 req.session);
Ed Tanous1abe55e2018-09-05 08:30:59 -0700102 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100103 }
104
Ed Tanous1abe55e2018-09-05 08:30:59 -0700105 private:
106 const std::shared_ptr<crow::persistent_data::UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800107 performBasicAuth(std::string_view auth_header) const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700108 {
109 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100110
Ed Tanous1abe55e2018-09-05 08:30:59 -0700111 std::string authData;
Ed Tanous39e77502019-03-04 17:35:53 -0800112 std::string_view param = auth_header.substr(strlen("Basic "));
Ed Tanous1abe55e2018-09-05 08:30:59 -0700113 if (!crow::utility::base64Decode(param, authData))
114 {
115 return nullptr;
116 }
117 std::size_t separator = authData.find(':');
118 if (separator == std::string::npos)
119 {
120 return nullptr;
121 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100122
Ed Tanous1abe55e2018-09-05 08:30:59 -0700123 std::string user = authData.substr(0, separator);
124 separator += 1;
125 if (separator > authData.size())
126 {
127 return nullptr;
128 }
129 std::string pass = authData.substr(separator);
130
131 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
132
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500133 bool passwordChangeRequired = false;
134 if (!pamAuthenticateUser(user, pass, passwordChangeRequired) ||
135 passwordChangeRequired)
Ed Tanous1abe55e2018-09-05 08:30:59 -0700136 {
137 return nullptr;
138 }
139
140 // TODO(ed) generateUserSession is a little expensive for basic
141 // auth, as it generates some random identifiers that will never be
142 // used. This should have a "fast" path for when user tokens aren't
143 // needed.
144 // This whole flow needs to be revisited anyway, as we can't be
145 // calling directly into pam for every request
146 return persistent_data::SessionStore::getInstance().generateUserSession(
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500147 user, false,
148 crow::persistent_data::PersistenceType::SINGLE_REQUEST);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100149 }
150
Ed Tanous1abe55e2018-09-05 08:30:59 -0700151 const std::shared_ptr<crow::persistent_data::UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800152 performTokenAuth(std::string_view auth_header) const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700153 {
154 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Token authentication";
Ed Tanous8041f312017-04-03 09:47:01 -0700155
Ed Tanous39e77502019-03-04 17:35:53 -0800156 std::string_view token = auth_header.substr(strlen("Token "));
Ed Tanous1abe55e2018-09-05 08:30:59 -0700157 auto session =
158 persistent_data::SessionStore::getInstance().loginSessionByToken(
159 token);
160 return session;
Ed Tanous1ea9f062018-03-27 17:45:20 -0700161 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100162
Ed Tanous1abe55e2018-09-05 08:30:59 -0700163 const std::shared_ptr<crow::persistent_data::UserSession>
164 performXtokenAuth(const crow::Request& req) const
165 {
166 BMCWEB_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100167
Ed Tanous39e77502019-03-04 17:35:53 -0800168 std::string_view token = req.getHeaderValue("X-Auth-Token");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700169 if (token.empty())
170 {
171 return nullptr;
172 }
173 auto session =
174 persistent_data::SessionStore::getInstance().loginSessionByToken(
175 token);
176 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100177 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700178
179 const std::shared_ptr<crow::persistent_data::UserSession>
180 performCookieAuth(const crow::Request& req) const
181 {
182 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
183
Ed Tanous39e77502019-03-04 17:35:53 -0800184 std::string_view cookieValue = req.getHeaderValue("Cookie");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700185 if (cookieValue.empty())
186 {
187 return nullptr;
188 }
189
190 auto startIndex = cookieValue.find("SESSION=");
191 if (startIndex == std::string::npos)
192 {
193 return nullptr;
194 }
195 startIndex += sizeof("SESSION=") - 1;
196 auto endIndex = cookieValue.find(";", startIndex);
197 if (endIndex == std::string::npos)
198 {
199 endIndex = cookieValue.size();
200 }
Ed Tanous39e77502019-03-04 17:35:53 -0800201 std::string_view authKey =
Ed Tanous1abe55e2018-09-05 08:30:59 -0700202 cookieValue.substr(startIndex, endIndex - startIndex);
203
204 const std::shared_ptr<crow::persistent_data::UserSession> session =
205 persistent_data::SessionStore::getInstance().loginSessionByToken(
206 authKey);
207 if (session == nullptr)
208 {
209 return nullptr;
210 }
Ed Tanous1e439872018-05-18 11:48:52 -0700211#ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
Ed Tanous1abe55e2018-09-05 08:30:59 -0700212 // RFC7231 defines methods that need csrf protection
213 if (req.method() != "GET"_method)
214 {
Ed Tanous39e77502019-03-04 17:35:53 -0800215 std::string_view csrf = req.getHeaderValue("X-XSRF-TOKEN");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700216 // Make sure both tokens are filled
217 if (csrf.empty() || session->csrfToken.empty())
218 {
219 return nullptr;
220 }
221 // Reject if csrf token not available
222 if (csrf != session->csrfToken)
223 {
224 return nullptr;
225 }
226 }
Ed Tanous1e439872018-05-18 11:48:52 -0700227#endif
Ed Tanous1abe55e2018-09-05 08:30:59 -0700228 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100229 }
230
Ed Tanous1abe55e2018-09-05 08:30:59 -0700231 // checks if request can be forwarded without authentication
232 bool isOnWhitelist(const crow::Request& req) const
233 {
234 // it's allowed to GET root node without authentica tion
235 if ("GET"_method == req.method())
236 {
237 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/" ||
238 req.url == "/redfish" || req.url == "/redfish/" ||
239 req.url == "/redfish/v1/odata" ||
240 req.url == "/redfish/v1/odata/")
241 {
242 return true;
243 }
244 else if (crow::webassets::routes.find(std::string(req.url)) !=
245 crow::webassets::routes.end())
246 {
247 return true;
248 }
249 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100250
Ed Tanous1abe55e2018-09-05 08:30:59 -0700251 // it's allowed to POST on session collection & login without
252 // authentication
253 if ("POST"_method == req.method())
254 {
255 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
256 (req.url == "/redfish/v1/SessionService/Sessions/") ||
257 (req.url == "/login"))
258 {
259 return true;
260 }
261 }
262
263 return false;
264 }
Ed Tanous99923322017-03-03 14:21:24 -0800265};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700266
Ed Tanousba9f9a62017-10-11 16:40:35 -0700267// TODO(ed) see if there is a better way to allow middlewares to request
268// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700269// Possibly an init function on first construction?
Ed Tanous1abe55e2018-09-05 08:30:59 -0700270template <typename... Middlewares> void requestRoutes(Crow<Middlewares...>& app)
271{
272 static_assert(
273 black_magic::Contains<persistent_data::Middleware,
274 Middlewares...>::value,
275 "token_authorization middleware must be enabled in app to use "
276 "auth routes");
277 BMCWEB_ROUTE(app, "/login")
278 .methods(
279 "POST"_method)([&](const crow::Request& req, crow::Response& res) {
Ed Tanous39e77502019-03-04 17:35:53 -0800280 std::string_view contentType = req.getHeaderValue("content-type");
281 std::string_view username;
282 std::string_view password;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700283
Ed Tanous1abe55e2018-09-05 08:30:59 -0700284 bool looksLikeIbm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800285
Ed Tanous1abe55e2018-09-05 08:30:59 -0700286 // This object needs to be declared at this scope so the strings
287 // within it are not destroyed before we can use them
288 nlohmann::json loginCredentials;
289 // Check if auth was provided by a payload
Ed Tanousfdf43a32019-07-31 16:52:24 -0700290 if (boost::starts_with(contentType, "application/json"))
Ed Tanous1abe55e2018-09-05 08:30:59 -0700291 {
292 loginCredentials =
293 nlohmann::json::parse(req.body, nullptr, false);
294 if (loginCredentials.is_discarded())
295 {
Ed Tanousfdf43a32019-07-31 16:52:24 -0700296 BMCWEB_LOG_DEBUG << "Bad json in request";
Ed Tanous1abe55e2018-09-05 08:30:59 -0700297 res.result(boost::beast::http::status::bad_request);
298 res.end();
299 return;
300 }
301
302 // check for username/password in the root object
303 // THis method is how intel APIs authenticate
304 nlohmann::json::iterator userIt =
305 loginCredentials.find("username");
306 nlohmann::json::iterator passIt =
307 loginCredentials.find("password");
308 if (userIt != loginCredentials.end() &&
309 passIt != loginCredentials.end())
310 {
311 const std::string* userStr =
312 userIt->get_ptr<const std::string*>();
313 const std::string* passStr =
314 passIt->get_ptr<const std::string*>();
315 if (userStr != nullptr && passStr != nullptr)
316 {
317 username = *userStr;
318 password = *passStr;
319 }
320 }
321 else
322 {
323 // Openbmc appears to push a data object that contains the
324 // same keys (username and password), attempt to use that
325 auto dataIt = loginCredentials.find("data");
326 if (dataIt != loginCredentials.end())
327 {
328 // Some apis produce an array of value ["username",
329 // "password"]
330 if (dataIt->is_array())
331 {
332 if (dataIt->size() == 2)
333 {
334 nlohmann::json::iterator userIt2 =
335 dataIt->begin();
336 nlohmann::json::iterator passIt2 =
337 dataIt->begin() + 1;
338 looksLikeIbm = true;
339 if (userIt2 != dataIt->end() &&
340 passIt2 != dataIt->end())
341 {
342 const std::string* userStr =
343 userIt2->get_ptr<const std::string*>();
344 const std::string* passStr =
345 passIt2->get_ptr<const std::string*>();
346 if (userStr != nullptr &&
347 passStr != nullptr)
348 {
349 username = *userStr;
350 password = *passStr;
351 }
352 }
353 }
354 }
355 else if (dataIt->is_object())
356 {
357 nlohmann::json::iterator userIt2 =
358 dataIt->find("username");
359 nlohmann::json::iterator passIt2 =
360 dataIt->find("password");
361 if (userIt2 != dataIt->end() &&
362 passIt2 != dataIt->end())
363 {
364 const std::string* userStr =
365 userIt2->get_ptr<const std::string*>();
366 const std::string* passStr =
367 passIt2->get_ptr<const std::string*>();
368 if (userStr != nullptr && passStr != nullptr)
369 {
370 username = *userStr;
371 password = *passStr;
372 }
373 }
374 }
375 }
376 }
377 }
378 else
379 {
380 // check if auth was provided as a headers
381 username = req.getHeaderValue("username");
382 password = req.getHeaderValue("password");
383 }
384
385 if (!username.empty() && !password.empty())
386 {
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500387 bool passwordChangeRequired = false;
388 if (!pamAuthenticateUser(username, password,
389 passwordChangeRequired))
Ed Tanous1abe55e2018-09-05 08:30:59 -0700390 {
391 res.result(boost::beast::http::status::unauthorized);
392 }
393 else
394 {
395 auto session = persistent_data::SessionStore::getInstance()
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500396 .generateUserSession(
397 username, passwordChangeRequired);
Ed Tanous1abe55e2018-09-05 08:30:59 -0700398
399 if (looksLikeIbm)
400 {
401 // IBM requires a very specific login structure, and
402 // doesn't actually look at the status code.
403 // TODO(ed).... Fix that upstream
404 res.jsonValue = {
405 {"data",
406 "User '" + std::string(username) + "' logged in"},
407 {"message", "200 OK"},
408 {"status", "ok"}};
409
410 // Hack alert. Boost beast by default doesn't let you
411 // declare multiple headers of the same name, and in
412 // most cases this is fine. Unfortunately here we need
413 // to set the Session cookie, which requires the
414 // httpOnly attribute, as well as the XSRF cookie, which
415 // requires it to not have an httpOnly attribute. To get
416 // the behavior we want, we simply inject the second
417 // "set-cookie" string into the value header, and get
418 // the result we want, even though we are technicaly
419 // declaring two headers here.
420 res.addHeader("Set-Cookie",
421 "XSRF-TOKEN=" + session->csrfToken +
422 "; Secure\r\nSet-Cookie: SESSION=" +
423 session->sessionToken +
424 "; Secure; HttpOnly");
425 }
426 else
427 {
428 // if content type is json, assume json token
429 res.jsonValue = {{"token", session->sessionToken}};
430 }
Joseph Reynolds8fd315a2019-09-12 12:02:33 -0500431 if (passwordChangeRequired)
432 {
433 crow::setPasswordChangeRequired(res, session->username);
434 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700435 }
436 }
437 else
438 {
Ed Tanousfdf43a32019-07-31 16:52:24 -0700439 BMCWEB_LOG_DEBUG << "Couldn't interpret password";
Ed Tanous1abe55e2018-09-05 08:30:59 -0700440 res.result(boost::beast::http::status::bad_request);
441 }
442 res.end();
443 });
444
445 BMCWEB_ROUTE(app, "/logout")
Ratan Gupta6f359562019-04-03 10:39:08 +0530446 .methods("POST"_method)(
447 [&](const crow::Request& req, crow::Response& res) {
448 auto& session = req.session;
449 if (session != nullptr)
450 {
451 res.jsonValue = {
452 {"data", "User '" + session->username + "' logged out"},
453 {"message", "200 OK"},
454 {"status", "ok"}};
Anthony Wilsonaf713a62019-03-15 15:40:58 -0500455
Ratan Gupta6f359562019-04-03 10:39:08 +0530456 persistent_data::SessionStore::getInstance().removeSession(
457 session);
458 }
459 res.end();
460 return;
461 });
Ed Tanous911ac312017-08-15 09:37:42 -0700462}
Ed Tanous1abe55e2018-09-05 08:30:59 -0700463} // namespace token_authorization
464} // namespace crow