blob: f151e4ff63b7184ca34f745bf648ee73a02df03e [file] [log] [blame]
Ed Tanousf9273472017-02-28 16:05:13 -08001#pragma once
2
Ed Tanous911ac312017-08-15 09:37:42 -07003#include <pam_authenticate.hpp>
Ed Tanousba9f9a62017-10-11 16:40:35 -07004#include <persistent_data_middleware.hpp>
Ed Tanous911ac312017-08-15 09:37:42 -07005#include <webassets.hpp>
6#include <random>
7#include <crow/app.h>
Ed Tanouse0d918b2018-03-27 17:41:04 -07008#include <crow/common.h>
Ed Tanousf9273472017-02-28 16:05:13 -08009#include <crow/http_request.h>
10#include <crow/http_response.h>
Ed Tanous4758d5b2017-06-06 15:28:13 -070011#include <boost/container/flat_set.hpp>
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010012
Ed Tanous99923322017-03-03 14:21:24 -080013namespace crow {
Ed Tanousb4d29f42017-03-24 16:39:25 -070014
Ed Tanous911ac312017-08-15 09:37:42 -070015namespace TokenAuthorization {
Ed Tanousb4d29f42017-03-24 16:39:25 -070016
Ed Tanous911ac312017-08-15 09:37:42 -070017class Middleware {
Ed Tanous3dac7492017-08-02 13:46:20 -070018 public:
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010019 struct context {
Ed Tanouse0d918b2018-03-27 17:41:04 -070020 std::shared_ptr<crow::PersistentData::UserSession> session;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010021 };
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010022
23 void before_handle(crow::request& req, response& res, context& ctx) {
Ed Tanousbae064e2018-03-22 15:44:39 -070024 if (is_on_whitelist(req)) {
25 return;
26 }
27
Ed Tanous1ea9f062018-03-27 17:45:20 -070028 ctx.session = perform_xtoken_auth(req);
Ed Tanous1ea9f062018-03-27 17:45:20 -070029 if (ctx.session == nullptr) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010030 ctx.session = perform_cookie_auth(req);
Ed Tanous1ea9f062018-03-27 17:45:20 -070031 }
Ed Tanouse0d918b2018-03-27 17:41:04 -070032 if (ctx.session == nullptr) {
33 boost::string_view auth_header = req.get_header_value("Authorization");
34 if (!auth_header.empty()) {
35 // Reject any kind of auth other than basic or token
36 if (boost::starts_with(auth_header, "Token ")) {
37 ctx.session = perform_token_auth(auth_header);
38 } else if (boost::starts_with(auth_header, "Basic ")) {
39 ctx.session = perform_basic_auth(auth_header);
40 }
41 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010042 }
43
Ed Tanousbae064e2018-03-22 15:44:39 -070044 if (ctx.session == nullptr) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010045 CROW_LOG_WARNING << "[AuthMiddleware] authorization failed";
Ed Tanouse0d918b2018-03-27 17:41:04 -070046
Ed Tanous9bd21fc2018-04-26 16:08:56 -070047 // If it's a browser connecting, don't send the HTTP authenticate header,
48 // to avoid possible CSRF attacks with basic auth
49 if (http_helpers::request_prefers_html(req)) {
50 res.result(boost::beast::http::status::temporary_redirect);
51 res.add_header("Location", "/#/login");
52 } else {
53 res.result(boost::beast::http::status::unauthorized);
54 // only send the WWW-authenticate header if this isn't a xhr from the
55 // browser. most scripts,
56 if (req.get_header_value("User-Agent").empty()) {
57 res.add_header("WWW-Authenticate", "Basic");
58 }
59 }
Ed Tanouse0d918b2018-03-27 17:41:04 -070060
Ed Tanousf3d847c2017-06-12 16:01:42 -070061 res.end();
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010062 return;
63 }
Ed Tanousf9273472017-02-28 16:05:13 -080064
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010065 // TODO get user privileges here and propagate it via MW context
66 // else let the request continue unharmed
67 }
Ed Tanousf3d847c2017-06-12 16:01:42 -070068
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010069 template <typename AllContext>
70 void after_handle(request& req, response& res, context& ctx,
71 AllContext& allctx) {
Ed Tanouse0d918b2018-03-27 17:41:04 -070072 // TODO(ed) THis should really be handled by the persistent data
73 // middleware, but because it is upstream, it doesn't have access to the
74 // session information. Should the data middleware persist the current
75 // user session?
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010076 if (ctx.session != nullptr &&
77 ctx.session->persistence ==
78 crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +020079 PersistentData::SessionStore::getInstance().remove_session(ctx.session);
Ed Tanousf3d847c2017-06-12 16:01:42 -070080 }
81 }
82
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010083 private:
Ed Tanouse0d918b2018-03-27 17:41:04 -070084 const std::shared_ptr<crow::PersistentData::UserSession> perform_basic_auth(
85 boost::string_view auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010086 CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
87
88 std::string auth_data;
Ed Tanouse0d918b2018-03-27 17:41:04 -070089 boost::string_view param = auth_header.substr(strlen("Basic "));
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010090 if (!crow::utility::base64_decode(param, auth_data)) {
91 return nullptr;
92 }
93 std::size_t separator = auth_data.find(':');
94 if (separator == std::string::npos) {
95 return nullptr;
96 }
97
98 std::string user = auth_data.substr(0, separator);
99 separator += 1;
100 if (separator > auth_data.size()) {
101 return nullptr;
102 }
103 std::string pass = auth_data.substr(separator);
104
105 CROW_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
106
107 if (!pam_authenticate_user(user, pass)) {
108 return nullptr;
109 }
110
111 // TODO(ed) generate_user_session is a little expensive for basic
112 // auth, as it generates some random identifiers that will never be
113 // used. This should have a "fast" path for when user tokens aren't
114 // needed.
115 // This whole flow needs to be revisited anyway, as we can't be
116 // calling directly into pam for every request
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200117 return PersistentData::SessionStore::getInstance().generate_user_session(
Ed Tanouse0d918b2018-03-27 17:41:04 -0700118 user, crow::PersistentData::PersistenceType::SINGLE_REQUEST);
Ed Tanousf3d847c2017-06-12 16:01:42 -0700119 }
Ed Tanous8041f312017-04-03 09:47:01 -0700120
Ed Tanouse0d918b2018-03-27 17:41:04 -0700121 const std::shared_ptr<crow::PersistentData::UserSession> perform_token_auth(
122 boost::string_view auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100123 CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication";
124
Ed Tanouse0d918b2018-03-27 17:41:04 -0700125 boost::string_view token = auth_header.substr(strlen("Token "));
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200126 auto session =
127 PersistentData::SessionStore::getInstance().login_session_by_token(
128 token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100129 return session;
130 }
131
Ed Tanouse0d918b2018-03-27 17:41:04 -0700132 const std::shared_ptr<crow::PersistentData::UserSession> perform_xtoken_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100133 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100134 CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
135
Ed Tanouse0d918b2018-03-27 17:41:04 -0700136 boost::string_view token = req.get_header_value("X-Auth-Token");
Ed Tanous1ea9f062018-03-27 17:45:20 -0700137 if (token.empty()) {
138 return nullptr;
139 }
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200140 auto session =
141 PersistentData::SessionStore::getInstance().login_session_by_token(
142 token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100143 return session;
144 }
145
Ed Tanouse0d918b2018-03-27 17:41:04 -0700146 const std::shared_ptr<crow::PersistentData::UserSession> perform_cookie_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100147 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100148 CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
149
Ed Tanouse0d918b2018-03-27 17:41:04 -0700150 boost::string_view cookie_value = req.get_header_value("Cookie");
Ed Tanous1ea9f062018-03-27 17:45:20 -0700151 if (cookie_value.empty()) {
152 return nullptr;
153 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100154
155 auto start_index = cookie_value.find("SESSION=");
156 if (start_index == std::string::npos) {
157 return nullptr;
158 }
Ed Tanous41ff64d2018-01-30 13:13:38 -0800159 start_index += sizeof("SESSION=") - 1;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100160 auto end_index = cookie_value.find(";", start_index);
161 if (end_index == std::string::npos) {
162 end_index = cookie_value.size();
163 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700164 boost::string_view auth_key =
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100165 cookie_value.substr(start_index, end_index - start_index);
166
Ed Tanouse0d918b2018-03-27 17:41:04 -0700167 const std::shared_ptr<crow::PersistentData::UserSession> session =
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200168 PersistentData::SessionStore::getInstance().login_session_by_token(
169 auth_key);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100170 if (session == nullptr) {
171 return nullptr;
172 }
Ed Tanous1e439872018-05-18 11:48:52 -0700173#ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100174 // RFC7231 defines methods that need csrf protection
Ed Tanouse0d918b2018-03-27 17:41:04 -0700175 if (req.method() != "GET"_method) {
176 boost::string_view csrf = req.get_header_value("X-XSRF-TOKEN");
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100177 // Make sure both tokens are filled
178 if (csrf.empty() || session->csrf_token.empty()) {
179 return nullptr;
180 }
181 // Reject if csrf token not available
182 if (csrf != session->csrf_token) {
183 return nullptr;
184 }
185 }
Ed Tanous1e439872018-05-18 11:48:52 -0700186#endif
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100187 return session;
188 }
189
190 // checks if request can be forwarded without authentication
191 bool is_on_whitelist(const crow::request& req) const {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700192 // it's allowed to GET root node without authentica tion
193 if ("GET"_method == req.method()) {
Ed Tanousaa2e59c2018-04-12 12:17:20 -0700194 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/") {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100195 return true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700196 } else if (crow::webassets::routes.find(std::string(req.url)) !=
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100197 crow::webassets::routes.end()) {
198 return true;
199 }
200 }
201
Ed Tanouse0d918b2018-03-27 17:41:04 -0700202 // it's allowed to POST on session collection & login without
203 // authentication
204 if ("POST"_method == req.method()) {
205 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
206 (req.url == "/redfish/v1/SessionService/Sessions/") ||
Ed Tanous9bd21fc2018-04-26 16:08:56 -0700207 (req.url == "/login")) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100208 return true;
209 }
210 }
211
212 return false;
213 }
Ed Tanous99923322017-03-03 14:21:24 -0800214};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700215
Ed Tanousba9f9a62017-10-11 16:40:35 -0700216// TODO(ed) see if there is a better way to allow middlewares to request
217// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700218// Possibly an init function on first construction?
219template <typename... Middlewares>
220void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700221 static_assert(
222 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
223 "TokenAuthorization middleware must be enabled in app to use "
224 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700225 CROW_ROUTE(app, "/login")
226 .methods(
227 "POST"_method)([&](const crow::request& req, crow::response& res) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700228 boost::string_view content_type = req.get_header_value("content-type");
229 boost::string_view username;
230 boost::string_view password;
231
Ed Tanous911ac312017-08-15 09:37:42 -0700232 bool looks_like_ibm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800233
Ed Tanouse0d918b2018-03-27 17:41:04 -0700234 // This object needs to be declared at this scope so the strings
235 // within it are not destroyed before we can use them
Ed Tanousdb024a52018-03-06 12:50:34 -0800236 nlohmann::json login_credentials;
Ed Tanous911ac312017-08-15 09:37:42 -0700237 // Check if auth was provided by a payload
238 if (content_type == "application/json") {
Ed Tanousdb024a52018-03-06 12:50:34 -0800239 login_credentials = nlohmann::json::parse(req.body, nullptr, false);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700240 if (login_credentials.is_discarded()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700241 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700242 res.end();
243 return;
244 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700245
Ed Tanousba9f9a62017-10-11 16:40:35 -0700246 // check for username/password in the root object
247 // THis method is how intel APIs authenticate
Ed Tanouse0d918b2018-03-27 17:41:04 -0700248 nlohmann::json::iterator user_it = login_credentials.find("username");
249 nlohmann::json::iterator pass_it = login_credentials.find("password");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700250 if (user_it != login_credentials.end() &&
251 pass_it != login_credentials.end()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700252 const std::string* user_str =
253 user_it->get_ptr<const std::string*>();
254 const std::string* pass_str =
255 pass_it->get_ptr<const std::string*>();
256 if (user_str != nullptr && pass_str != nullptr) {
257 username = *user_str;
258 password = *pass_str;
259 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700260 } else {
261 // Openbmc appears to push a data object that contains the same
262 // keys (username and password), attempt to use that
263 auto data_it = login_credentials.find("data");
264 if (data_it != login_credentials.end()) {
265 // Some apis produce an array of value ["username",
266 // "password"]
267 if (data_it->is_array()) {
268 if (data_it->size() == 2) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700269 nlohmann::json::iterator user_it2 = data_it->begin();
270 nlohmann::json::iterator pass_it2 = data_it->begin() + 1;
Ed Tanousba9f9a62017-10-11 16:40:35 -0700271 looks_like_ibm = true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700272 if (user_it2 != data_it->end() &&
273 pass_it2 != data_it->end()) {
274 const std::string* user_str =
275 user_it2->get_ptr<const std::string*>();
276 const std::string* pass_str =
277 pass_it2->get_ptr<const std::string*>();
278 if (user_str != nullptr && pass_str != nullptr) {
279 username = *user_str;
280 password = *pass_str;
281 }
282 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700283 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700284
Ed Tanousba9f9a62017-10-11 16:40:35 -0700285 } else if (data_it->is_object()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700286 nlohmann::json::iterator user_it2 = data_it->find("username");
287 nlohmann::json::iterator pass_it2 = data_it->find("password");
288 if (user_it2 != data_it->end() && pass_it2 != data_it->end()) {
289 const std::string* user_str =
290 user_it2->get_ptr<const std::string*>();
291 const std::string* pass_str =
292 pass_it2->get_ptr<const std::string*>();
293 if (user_str != nullptr && pass_str != nullptr) {
294 username = *user_str;
295 password = *pass_str;
296 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700297 }
298 }
299 }
300 }
Ed Tanous911ac312017-08-15 09:37:42 -0700301 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700302 // check if auth was provided as a headers
303 username = req.get_header_value("username");
304 password = req.get_header_value("password");
Ed Tanous911ac312017-08-15 09:37:42 -0700305 }
306
Ed Tanouse0d918b2018-03-27 17:41:04 -0700307 if (!username.empty() && !password.empty()) {
308 if (!pam_authenticate_user(username, password)) {
309 res.result(boost::beast::http::status::unauthorized);
Ed Tanous911ac312017-08-15 09:37:42 -0700310 } else {
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200311 auto session = PersistentData::SessionStore::getInstance()
312 .generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700313
Ed Tanousba9f9a62017-10-11 16:40:35 -0700314 if (looks_like_ibm) {
315 // IBM requires a very specific login structure, and doesn't
316 // actually look at the status code. TODO(ed).... Fix that
317 // upstream
Ed Tanouse0d918b2018-03-27 17:41:04 -0700318 res.json_value = {
319 {"data", "User '" + std::string(username) + "' logged in"},
320 {"message", "200 OK"},
321 {"status", "ok"}};
Ed Tanous9bd21fc2018-04-26 16:08:56 -0700322
323 // Hack alert. Boost beast by default doesn't let you declare
324 // multiple headers of the same name, and in most cases this is
325 // fine. Unfortunately here we need to set the Session cookie,
326 // which requires the httpOnly attribute, as well as the XSRF
327 // cookie, which requires it to not have an httpOnly attribute.
328 // To get the behavior we want, we simply inject the second
329 // "set-cookie" string into the value header, and get the result
330 // we want, even though we are technicaly declaring two headers
331 // here.
332 res.add_header("Set-Cookie",
333 "XSRF-TOKEN=" + session->csrf_token +
334 "; Secure\r\nSet-Cookie: SESSION=" +
335 session->session_token + "; Secure; HttpOnly");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700336 } else {
337 // if content type is json, assume json token
Ed Tanouse0d918b2018-03-27 17:41:04 -0700338 res.json_value = {{"token", session->session_token}};
Ed Tanous911ac312017-08-15 09:37:42 -0700339 }
340 }
341
342 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700343 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700344 }
345 res.end();
346 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100347
348 CROW_ROUTE(app, "/logout")
Borawski.Lukasz4b1b8682018-04-04 12:50:16 +0200349 .methods(
350 "POST"_method)([&](const crow::request& req, crow::response& res) {
351 auto& session =
352 app.template get_context<TokenAuthorization::Middleware>(req)
353 .session;
354 if (session != nullptr) {
355 PersistentData::SessionStore::getInstance().remove_session(session);
356 }
357 res.end();
358 return;
359 });
Ed Tanous911ac312017-08-15 09:37:42 -0700360}
Ed Tanousdb024a52018-03-06 12:50:34 -0800361} // namespace TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700362} // namespace crow