blob: b186a5fb511ce3b6260b4628c895df14d081103e [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 {
25 std::shared_ptr<crow::persistent_data::UserSession> session;
26 };
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
Ed Tanous1abe55e2018-09-05 08:30:59 -070035 ctx.session = performXtokenAuth(req);
36 if (ctx.session == nullptr)
37 {
38 ctx.session = performCookieAuth(req);
Ed Tanous9bd21fc2018-04-26 16:08:56 -070039 }
Ed Tanous1abe55e2018-09-05 08:30:59 -070040 if (ctx.session == nullptr)
41 {
42 boost::string_view authHeader = req.getHeaderValue("Authorization");
43 if (!authHeader.empty())
44 {
45 // Reject any kind of auth other than basic or token
46 if (boost::starts_with(authHeader, "Token "))
47 {
48 ctx.session = performTokenAuth(authHeader);
49 }
50 else if (boost::starts_with(authHeader, "Basic "))
51 {
52 ctx.session = performBasicAuth(authHeader);
53 }
54 }
55 }
Ed Tanouse0d918b2018-03-27 17:41:04 -070056
Ed Tanous1abe55e2018-09-05 08:30:59 -070057 if (ctx.session == nullptr)
58 {
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?
96 if (ctx.session != nullptr &&
97 ctx.session->persistence ==
98 crow::persistent_data::PersistenceType::SINGLE_REQUEST)
99 {
100 persistent_data::SessionStore::getInstance().removeSession(
101 ctx.session);
102 }
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>
107 performBasicAuth(boost::string_view auth_header) const
108 {
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;
112 boost::string_view param = auth_header.substr(strlen("Basic "));
113 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
133 if (!pamAuthenticateUser(user, pass))
134 {
135 return nullptr;
136 }
137
138 // TODO(ed) generateUserSession is a little expensive for basic
139 // auth, as it generates some random identifiers that will never be
140 // used. This should have a "fast" path for when user tokens aren't
141 // needed.
142 // This whole flow needs to be revisited anyway, as we can't be
143 // calling directly into pam for every request
144 return persistent_data::SessionStore::getInstance().generateUserSession(
145 user, crow::persistent_data::PersistenceType::SINGLE_REQUEST);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100146 }
147
Ed Tanous1abe55e2018-09-05 08:30:59 -0700148 const std::shared_ptr<crow::persistent_data::UserSession>
149 performTokenAuth(boost::string_view auth_header) const
150 {
151 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Token authentication";
Ed Tanous8041f312017-04-03 09:47:01 -0700152
Ed Tanous1abe55e2018-09-05 08:30:59 -0700153 boost::string_view token = auth_header.substr(strlen("Token "));
154 auto session =
155 persistent_data::SessionStore::getInstance().loginSessionByToken(
156 token);
157 return session;
Ed Tanous1ea9f062018-03-27 17:45:20 -0700158 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100159
Ed Tanous1abe55e2018-09-05 08:30:59 -0700160 const std::shared_ptr<crow::persistent_data::UserSession>
161 performXtokenAuth(const crow::Request& req) const
162 {
163 BMCWEB_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100164
Ed Tanous1abe55e2018-09-05 08:30:59 -0700165 boost::string_view token = req.getHeaderValue("X-Auth-Token");
166 if (token.empty())
167 {
168 return nullptr;
169 }
170 auto session =
171 persistent_data::SessionStore::getInstance().loginSessionByToken(
172 token);
173 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100174 }
Ed Tanous1abe55e2018-09-05 08:30:59 -0700175
176 const std::shared_ptr<crow::persistent_data::UserSession>
177 performCookieAuth(const crow::Request& req) const
178 {
179 BMCWEB_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
180
181 boost::string_view cookieValue = req.getHeaderValue("Cookie");
182 if (cookieValue.empty())
183 {
184 return nullptr;
185 }
186
187 auto startIndex = cookieValue.find("SESSION=");
188 if (startIndex == std::string::npos)
189 {
190 return nullptr;
191 }
192 startIndex += sizeof("SESSION=") - 1;
193 auto endIndex = cookieValue.find(";", startIndex);
194 if (endIndex == std::string::npos)
195 {
196 endIndex = cookieValue.size();
197 }
198 boost::string_view authKey =
199 cookieValue.substr(startIndex, endIndex - startIndex);
200
201 const std::shared_ptr<crow::persistent_data::UserSession> session =
202 persistent_data::SessionStore::getInstance().loginSessionByToken(
203 authKey);
204 if (session == nullptr)
205 {
206 return nullptr;
207 }
Ed Tanous1e439872018-05-18 11:48:52 -0700208#ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
Ed Tanous1abe55e2018-09-05 08:30:59 -0700209 // RFC7231 defines methods that need csrf protection
210 if (req.method() != "GET"_method)
211 {
212 boost::string_view csrf = req.getHeaderValue("X-XSRF-TOKEN");
213 // Make sure both tokens are filled
214 if (csrf.empty() || session->csrfToken.empty())
215 {
216 return nullptr;
217 }
218 // Reject if csrf token not available
219 if (csrf != session->csrfToken)
220 {
221 return nullptr;
222 }
223 }
Ed Tanous1e439872018-05-18 11:48:52 -0700224#endif
Ed Tanous1abe55e2018-09-05 08:30:59 -0700225 return session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100226 }
227
Ed Tanous1abe55e2018-09-05 08:30:59 -0700228 // checks if request can be forwarded without authentication
229 bool isOnWhitelist(const crow::Request& req) const
230 {
231 // it's allowed to GET root node without authentica tion
232 if ("GET"_method == req.method())
233 {
234 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/" ||
235 req.url == "/redfish" || req.url == "/redfish/" ||
236 req.url == "/redfish/v1/odata" ||
237 req.url == "/redfish/v1/odata/")
238 {
239 return true;
240 }
241 else if (crow::webassets::routes.find(std::string(req.url)) !=
242 crow::webassets::routes.end())
243 {
244 return true;
245 }
246 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100247
Ed Tanous1abe55e2018-09-05 08:30:59 -0700248 // it's allowed to POST on session collection & login without
249 // authentication
250 if ("POST"_method == req.method())
251 {
252 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
253 (req.url == "/redfish/v1/SessionService/Sessions/") ||
254 (req.url == "/login"))
255 {
256 return true;
257 }
258 }
259
260 return false;
261 }
Ed Tanous99923322017-03-03 14:21:24 -0800262};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700263
Ed Tanousba9f9a62017-10-11 16:40:35 -0700264// TODO(ed) see if there is a better way to allow middlewares to request
265// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700266// Possibly an init function on first construction?
Ed Tanous1abe55e2018-09-05 08:30:59 -0700267template <typename... Middlewares> void requestRoutes(Crow<Middlewares...>& app)
268{
269 static_assert(
270 black_magic::Contains<persistent_data::Middleware,
271 Middlewares...>::value,
272 "token_authorization middleware must be enabled in app to use "
273 "auth routes");
274 BMCWEB_ROUTE(app, "/login")
275 .methods(
276 "POST"_method)([&](const crow::Request& req, crow::Response& res) {
277 boost::string_view contentType = req.getHeaderValue("content-type");
278 boost::string_view username;
279 boost::string_view password;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700280
Ed Tanous1abe55e2018-09-05 08:30:59 -0700281 bool looksLikeIbm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800282
Ed Tanous1abe55e2018-09-05 08:30:59 -0700283 // This object needs to be declared at this scope so the strings
284 // within it are not destroyed before we can use them
285 nlohmann::json loginCredentials;
286 // Check if auth was provided by a payload
287 if (contentType == "application/json")
288 {
289 loginCredentials =
290 nlohmann::json::parse(req.body, nullptr, false);
291 if (loginCredentials.is_discarded())
292 {
293 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 {
428 res.result(boost::beast::http::status::bad_request);
429 }
430 res.end();
431 });
432
433 BMCWEB_ROUTE(app, "/logout")
434 .methods(
435 "POST"_method)([&](const crow::Request& req, crow::Response& res) {
436 auto& session =
437 app.template getContext<token_authorization::Middleware>(req)
438 .session;
439 if (session != nullptr)
440 {
441 persistent_data::SessionStore::getInstance().removeSession(
442 session);
443 }
Ed Tanous911ac312017-08-15 09:37:42 -0700444 res.end();
445 return;
Ed Tanous1abe55e2018-09-05 08:30:59 -0700446 });
Ed Tanous911ac312017-08-15 09:37:42 -0700447}
Ed Tanous1abe55e2018-09-05 08:30:59 -0700448} // namespace token_authorization
449} // namespace crow