blob: 2ff3879bec46d433944094916f89a1a56873c5ac [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>
Ed Tanous1abe55e2018-09-05 08:30:59 -07009#include <pam_authenticate.hpp>
10#include <persistent_data_middleware.hpp>
11#include <random>
12#include <webassets.hpp>
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010013
Ed Tanous1abe55e2018-09-05 08:30:59 -070014namespace crow
15{
Ed Tanousb4d29f42017-03-24 16:39:25 -070016
Ed Tanous1abe55e2018-09-05 08:30:59 -070017namespace token_authorization
18{
Ed Tanousb4d29f42017-03-24 16:39:25 -070019
Ed Tanous1abe55e2018-09-05 08:30:59 -070020class Middleware
21{
22 public:
23 struct Context
24 {
Ed Tanous1abe55e2018-09-05 08:30:59 -070025 };
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
Ratan Gupta6f359562019-04-03 10:39:08 +053034 req.session = performXtokenAuth(req);
35 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070036 {
Ratan Gupta6f359562019-04-03 10:39:08 +053037 req.session = performCookieAuth(req);
Ed Tanous9bd21fc2018-04-26 16:08:56 -070038 }
Ratan Gupta6f359562019-04-03 10:39:08 +053039 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070040 {
Ed Tanous39e77502019-03-04 17:35:53 -080041 std::string_view authHeader = req.getHeaderValue("Authorization");
Ed Tanous1abe55e2018-09-05 08:30:59 -070042 if (!authHeader.empty())
43 {
44 // Reject any kind of auth other than basic or token
45 if (boost::starts_with(authHeader, "Token "))
46 {
Ratan Gupta6f359562019-04-03 10:39:08 +053047 req.session = performTokenAuth(authHeader);
Ed Tanous1abe55e2018-09-05 08:30:59 -070048 }
49 else if (boost::starts_with(authHeader, "Basic "))
50 {
Ratan Gupta6f359562019-04-03 10:39:08 +053051 req.session = performBasicAuth(authHeader);
Ed Tanous1abe55e2018-09-05 08:30:59 -070052 }
53 }
54 }
Ed Tanouse0d918b2018-03-27 17:41:04 -070055
Ratan Gupta6f359562019-04-03 10:39:08 +053056 if (req.session == nullptr)
Ed Tanous1abe55e2018-09-05 08:30:59 -070057 {
58 BMCWEB_LOG_WARNING << "[AuthMiddleware] authorization failed";
59
60 // If it's a browser connecting, don't send the HTTP authenticate
61 // header, to avoid possible CSRF attacks with basic auth
62 if (http_helpers::requestPrefersHtml(req))
63 {
64 res.result(boost::beast::http::status::temporary_redirect);
Ed Tanous6b5e77d2018-11-16 14:52:56 -080065 res.addHeader("Location", "/#/login?next=" +
66 http_helpers::urlEncode(req.url));
Ed Tanous1abe55e2018-09-05 08:30:59 -070067 }
68 else
69 {
70 res.result(boost::beast::http::status::unauthorized);
71 // only send the WWW-authenticate header if this isn't a xhr
72 // from the browser. most scripts,
73 if (req.getHeaderValue("User-Agent").empty())
74 {
75 res.addHeader("WWW-Authenticate", "Basic");
76 }
77 }
78
79 res.end();
80 return;
81 }
82
83 // TODO get user privileges here and propagate it via MW Context
84 // else let the request continue unharmed
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010085 }
Ed Tanousf9273472017-02-28 16:05:13 -080086
Ed Tanous1abe55e2018-09-05 08:30:59 -070087 template <typename AllContext>
88 void afterHandle(Request& req, Response& res, Context& ctx,
89 AllContext& allctx)
90 {
91 // TODO(ed) THis should really be handled by the persistent data
92 // middleware, but because it is upstream, it doesn't have access to the
93 // session information. Should the data middleware persist the current
94 // user session?
Ratan Gupta6f359562019-04-03 10:39:08 +053095 if (req.session != nullptr &&
96 req.session->persistence ==
Ed Tanous1abe55e2018-09-05 08:30:59 -070097 crow::persistent_data::PersistenceType::SINGLE_REQUEST)
98 {
99 persistent_data::SessionStore::getInstance().removeSession(
Ratan Gupta6f359562019-04-03 10:39:08 +0530100 req.session);
Ed Tanous1abe55e2018-09-05 08:30:59 -0700101 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100102 }
103
Ed Tanous1abe55e2018-09-05 08:30:59 -0700104 private:
105 const std::shared_ptr<crow::persistent_data::UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800106 performBasicAuth(std::string_view auth_header) const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700107 {
108 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100109
Ed Tanous1abe55e2018-09-05 08:30:59 -0700110 std::string authData;
Ed Tanous39e77502019-03-04 17:35:53 -0800111 std::string_view param = auth_header.substr(strlen("Basic "));
Ed Tanous1abe55e2018-09-05 08:30:59 -0700112 if (!crow::utility::base64Decode(param, authData))
113 {
114 return nullptr;
115 }
116 std::size_t separator = authData.find(':');
117 if (separator == std::string::npos)
118 {
119 return nullptr;
120 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100121
Ed Tanous1abe55e2018-09-05 08:30:59 -0700122 std::string user = authData.substr(0, separator);
123 separator += 1;
124 if (separator > authData.size())
125 {
126 return nullptr;
127 }
128 std::string pass = authData.substr(separator);
129
130 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
131
132 if (!pamAuthenticateUser(user, pass))
133 {
134 return nullptr;
135 }
136
137 // TODO(ed) generateUserSession is a little expensive for basic
138 // auth, as it generates some random identifiers that will never be
139 // used. This should have a "fast" path for when user tokens aren't
140 // needed.
141 // This whole flow needs to be revisited anyway, as we can't be
142 // calling directly into pam for every request
143 return persistent_data::SessionStore::getInstance().generateUserSession(
144 user, crow::persistent_data::PersistenceType::SINGLE_REQUEST);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100145 }
146
Ed Tanous1abe55e2018-09-05 08:30:59 -0700147 const std::shared_ptr<crow::persistent_data::UserSession>
Ed Tanous39e77502019-03-04 17:35:53 -0800148 performTokenAuth(std::string_view auth_header) const
Ed Tanous1abe55e2018-09-05 08:30:59 -0700149 {
150 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Token authentication";
Ed Tanous8041f312017-04-03 09:47:01 -0700151
Ed Tanous39e77502019-03-04 17:35:53 -0800152 std::string_view token = auth_header.substr(strlen("Token "));
Ed Tanous1abe55e2018-09-05 08:30:59 -0700153 auto session =
154 persistent_data::SessionStore::getInstance().loginSessionByToken(
155 token);
156 return session;
Ed Tanous1ea9f062018-03-27 17:45:20 -0700157 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100158
Ed Tanous1abe55e2018-09-05 08:30:59 -0700159 const std::shared_ptr<crow::persistent_data::UserSession>
160 performXtokenAuth(const crow::Request& req) const
161 {
162 BMCWEB_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100163
Ed Tanous39e77502019-03-04 17:35:53 -0800164 std::string_view token = req.getHeaderValue("X-Auth-Token");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700165 if (token.empty())
166 {
167 return nullptr;
168 }
169 auto session =
170 persistent_data::SessionStore::getInstance().loginSessionByToken(
171 token);
172 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100173 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700174
175 const std::shared_ptr<crow::persistent_data::UserSession>
176 performCookieAuth(const crow::Request& req) const
177 {
178 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
179
Ed Tanous39e77502019-03-04 17:35:53 -0800180 std::string_view cookieValue = req.getHeaderValue("Cookie");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700181 if (cookieValue.empty())
182 {
183 return nullptr;
184 }
185
186 auto startIndex = cookieValue.find("SESSION=");
187 if (startIndex == std::string::npos)
188 {
189 return nullptr;
190 }
191 startIndex += sizeof("SESSION=") - 1;
192 auto endIndex = cookieValue.find(";", startIndex);
193 if (endIndex == std::string::npos)
194 {
195 endIndex = cookieValue.size();
196 }
Ed Tanous39e77502019-03-04 17:35:53 -0800197 std::string_view authKey =
Ed Tanous1abe55e2018-09-05 08:30:59 -0700198 cookieValue.substr(startIndex, endIndex - startIndex);
199
200 const std::shared_ptr<crow::persistent_data::UserSession> session =
201 persistent_data::SessionStore::getInstance().loginSessionByToken(
202 authKey);
203 if (session == nullptr)
204 {
205 return nullptr;
206 }
Ed Tanous1e439872018-05-18 11:48:52 -0700207#ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
Ed Tanous1abe55e2018-09-05 08:30:59 -0700208 // RFC7231 defines methods that need csrf protection
209 if (req.method() != "GET"_method)
210 {
Ed Tanous39e77502019-03-04 17:35:53 -0800211 std::string_view csrf = req.getHeaderValue("X-XSRF-TOKEN");
Ed Tanous1abe55e2018-09-05 08:30:59 -0700212 // Make sure both tokens are filled
213 if (csrf.empty() || session->csrfToken.empty())
214 {
215 return nullptr;
216 }
217 // Reject if csrf token not available
218 if (csrf != session->csrfToken)
219 {
220 return nullptr;
221 }
222 }
Ed Tanous1e439872018-05-18 11:48:52 -0700223#endif
Ed Tanous1abe55e2018-09-05 08:30:59 -0700224 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100225 }
226
Ed Tanous1abe55e2018-09-05 08:30:59 -0700227 // checks if request can be forwarded without authentication
228 bool isOnWhitelist(const crow::Request& req) const
229 {
230 // it's allowed to GET root node without authentica tion
231 if ("GET"_method == req.method())
232 {
233 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/" ||
234 req.url == "/redfish" || req.url == "/redfish/" ||
235 req.url == "/redfish/v1/odata" ||
236 req.url == "/redfish/v1/odata/")
237 {
238 return true;
239 }
240 else if (crow::webassets::routes.find(std::string(req.url)) !=
241 crow::webassets::routes.end())
242 {
243 return true;
244 }
245 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100246
Ed Tanous1abe55e2018-09-05 08:30:59 -0700247 // it's allowed to POST on session collection & login without
248 // authentication
249 if ("POST"_method == req.method())
250 {
251 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
252 (req.url == "/redfish/v1/SessionService/Sessions/") ||
253 (req.url == "/login"))
254 {
255 return true;
256 }
257 }
258
259 return false;
260 }
Ed Tanous99923322017-03-03 14:21:24 -0800261};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700262
Ed Tanousba9f9a62017-10-11 16:40:35 -0700263// TODO(ed) see if there is a better way to allow middlewares to request
264// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700265// Possibly an init function on first construction?
Ed Tanous1abe55e2018-09-05 08:30:59 -0700266template <typename... Middlewares> void requestRoutes(Crow<Middlewares...>& app)
267{
268 static_assert(
269 black_magic::Contains<persistent_data::Middleware,
270 Middlewares...>::value,
271 "token_authorization middleware must be enabled in app to use "
272 "auth routes");
273 BMCWEB_ROUTE(app, "/login")
274 .methods(
275 "POST"_method)([&](const crow::Request& req, crow::Response& res) {
Ed Tanous39e77502019-03-04 17:35:53 -0800276 std::string_view contentType = req.getHeaderValue("content-type");
277 std::string_view username;
278 std::string_view password;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700279
Ed Tanous1abe55e2018-09-05 08:30:59 -0700280 bool looksLikeIbm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800281
Ed Tanous1abe55e2018-09-05 08:30:59 -0700282 // This object needs to be declared at this scope so the strings
283 // within it are not destroyed before we can use them
284 nlohmann::json loginCredentials;
285 // Check if auth was provided by a payload
Ed Tanousfdf43a32019-07-31 16:52:24 -0700286 if (boost::starts_with(contentType, "application/json"))
Ed Tanous1abe55e2018-09-05 08:30:59 -0700287 {
288 loginCredentials =
289 nlohmann::json::parse(req.body, nullptr, false);
290 if (loginCredentials.is_discarded())
291 {
Ed Tanousfdf43a32019-07-31 16:52:24 -0700292 BMCWEB_LOG_DEBUG << "Bad json in request";
Ed Tanous1abe55e2018-09-05 08:30:59 -0700293 res.result(boost::beast::http::status::bad_request);
294 res.end();
295 return;
296 }
297
298 // check for username/password in the root object
299 // THis method is how intel APIs authenticate
300 nlohmann::json::iterator userIt =
301 loginCredentials.find("username");
302 nlohmann::json::iterator passIt =
303 loginCredentials.find("password");
304 if (userIt != loginCredentials.end() &&
305 passIt != loginCredentials.end())
306 {
307 const std::string* userStr =
308 userIt->get_ptr<const std::string*>();
309 const std::string* passStr =
310 passIt->get_ptr<const std::string*>();
311 if (userStr != nullptr && passStr != nullptr)
312 {
313 username = *userStr;
314 password = *passStr;
315 }
316 }
317 else
318 {
319 // Openbmc appears to push a data object that contains the
320 // same keys (username and password), attempt to use that
321 auto dataIt = loginCredentials.find("data");
322 if (dataIt != loginCredentials.end())
323 {
324 // Some apis produce an array of value ["username",
325 // "password"]
326 if (dataIt->is_array())
327 {
328 if (dataIt->size() == 2)
329 {
330 nlohmann::json::iterator userIt2 =
331 dataIt->begin();
332 nlohmann::json::iterator passIt2 =
333 dataIt->begin() + 1;
334 looksLikeIbm = true;
335 if (userIt2 != dataIt->end() &&
336 passIt2 != dataIt->end())
337 {
338 const std::string* userStr =
339 userIt2->get_ptr<const std::string*>();
340 const std::string* passStr =
341 passIt2->get_ptr<const std::string*>();
342 if (userStr != nullptr &&
343 passStr != nullptr)
344 {
345 username = *userStr;
346 password = *passStr;
347 }
348 }
349 }
350 }
351 else if (dataIt->is_object())
352 {
353 nlohmann::json::iterator userIt2 =
354 dataIt->find("username");
355 nlohmann::json::iterator passIt2 =
356 dataIt->find("password");
357 if (userIt2 != dataIt->end() &&
358 passIt2 != dataIt->end())
359 {
360 const std::string* userStr =
361 userIt2->get_ptr<const std::string*>();
362 const std::string* passStr =
363 passIt2->get_ptr<const std::string*>();
364 if (userStr != nullptr && passStr != nullptr)
365 {
366 username = *userStr;
367 password = *passStr;
368 }
369 }
370 }
371 }
372 }
373 }
374 else
375 {
376 // check if auth was provided as a headers
377 username = req.getHeaderValue("username");
378 password = req.getHeaderValue("password");
379 }
380
381 if (!username.empty() && !password.empty())
382 {
383 if (!pamAuthenticateUser(username, password))
384 {
385 res.result(boost::beast::http::status::unauthorized);
386 }
387 else
388 {
389 auto session = persistent_data::SessionStore::getInstance()
390 .generateUserSession(username);
391
392 if (looksLikeIbm)
393 {
394 // IBM requires a very specific login structure, and
395 // doesn't actually look at the status code.
396 // TODO(ed).... Fix that upstream
397 res.jsonValue = {
398 {"data",
399 "User '" + std::string(username) + "' logged in"},
400 {"message", "200 OK"},
401 {"status", "ok"}};
402
403 // Hack alert. Boost beast by default doesn't let you
404 // declare multiple headers of the same name, and in
405 // most cases this is fine. Unfortunately here we need
406 // to set the Session cookie, which requires the
407 // httpOnly attribute, as well as the XSRF cookie, which
408 // requires it to not have an httpOnly attribute. To get
409 // the behavior we want, we simply inject the second
410 // "set-cookie" string into the value header, and get
411 // the result we want, even though we are technicaly
412 // declaring two headers here.
413 res.addHeader("Set-Cookie",
414 "XSRF-TOKEN=" + session->csrfToken +
415 "; Secure\r\nSet-Cookie: SESSION=" +
416 session->sessionToken +
417 "; Secure; HttpOnly");
418 }
419 else
420 {
421 // if content type is json, assume json token
422 res.jsonValue = {{"token", session->sessionToken}};
423 }
424 }
425 }
426 else
427 {
Ed Tanousfdf43a32019-07-31 16:52:24 -0700428 BMCWEB_LOG_DEBUG << "Couldn't interpret password";
Ed Tanous1abe55e2018-09-05 08:30:59 -0700429 res.result(boost::beast::http::status::bad_request);
430 }
431 res.end();
432 });
433
434 BMCWEB_ROUTE(app, "/logout")
Ratan Gupta6f359562019-04-03 10:39:08 +0530435 .methods("POST"_method)(
436 [&](const crow::Request& req, crow::Response& res) {
437 auto& session = req.session;
438 if (session != nullptr)
439 {
440 res.jsonValue = {
441 {"data", "User '" + session->username + "' logged out"},
442 {"message", "200 OK"},
443 {"status", "ok"}};
Anthony Wilsonaf713a62019-03-15 15:40:58 -0500444
Ratan Gupta6f359562019-04-03 10:39:08 +0530445 persistent_data::SessionStore::getInstance().removeSession(
446 session);
447 }
448 res.end();
449 return;
450 });
Ed Tanous911ac312017-08-15 09:37:42 -0700451}
Ed Tanous1abe55e2018-09-05 08:30:59 -0700452} // namespace token_authorization
453} // namespace crow