blob: a40469fea2fa3273c4e5fe540c45c8353fedadda [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) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010079 PersistentData::session_store->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
Ed Tanouse0d918b2018-03-27 17:41:04 -0700117 return PersistentData::session_store->generate_user_session(
118 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 "));
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100126 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100127 return session;
128 }
129
Ed Tanouse0d918b2018-03-27 17:41:04 -0700130 const std::shared_ptr<crow::PersistentData::UserSession> perform_xtoken_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100131 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100132 CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
133
Ed Tanouse0d918b2018-03-27 17:41:04 -0700134 boost::string_view token = req.get_header_value("X-Auth-Token");
Ed Tanous1ea9f062018-03-27 17:45:20 -0700135 if (token.empty()) {
136 return nullptr;
137 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100138 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100139 return session;
140 }
141
Ed Tanouse0d918b2018-03-27 17:41:04 -0700142 const std::shared_ptr<crow::PersistentData::UserSession> perform_cookie_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100143 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100144 CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
145
Ed Tanouse0d918b2018-03-27 17:41:04 -0700146 boost::string_view cookie_value = req.get_header_value("Cookie");
Ed Tanous1ea9f062018-03-27 17:45:20 -0700147 if (cookie_value.empty()) {
148 return nullptr;
149 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100150
151 auto start_index = cookie_value.find("SESSION=");
152 if (start_index == std::string::npos) {
153 return nullptr;
154 }
Ed Tanous41ff64d2018-01-30 13:13:38 -0800155 start_index += sizeof("SESSION=") - 1;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100156 auto end_index = cookie_value.find(";", start_index);
157 if (end_index == std::string::npos) {
158 end_index = cookie_value.size();
159 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700160 boost::string_view auth_key =
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100161 cookie_value.substr(start_index, end_index - start_index);
162
Ed Tanouse0d918b2018-03-27 17:41:04 -0700163 const std::shared_ptr<crow::PersistentData::UserSession> session =
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100164 PersistentData::session_store->login_session_by_token(auth_key);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100165 if (session == nullptr) {
166 return nullptr;
167 }
Ed Tanous1e439872018-05-18 11:48:52 -0700168#ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100169 // RFC7231 defines methods that need csrf protection
Ed Tanouse0d918b2018-03-27 17:41:04 -0700170 if (req.method() != "GET"_method) {
171 boost::string_view csrf = req.get_header_value("X-XSRF-TOKEN");
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100172 // Make sure both tokens are filled
173 if (csrf.empty() || session->csrf_token.empty()) {
174 return nullptr;
175 }
176 // Reject if csrf token not available
177 if (csrf != session->csrf_token) {
178 return nullptr;
179 }
180 }
Ed Tanous1e439872018-05-18 11:48:52 -0700181#endif
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100182 return session;
183 }
184
185 // checks if request can be forwarded without authentication
186 bool is_on_whitelist(const crow::request& req) const {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700187 // it's allowed to GET root node without authentica tion
188 if ("GET"_method == req.method()) {
Ed Tanousaa2e59c2018-04-12 12:17:20 -0700189 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/") {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100190 return true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700191 } else if (crow::webassets::routes.find(std::string(req.url)) !=
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100192 crow::webassets::routes.end()) {
193 return true;
194 }
195 }
196
Ed Tanouse0d918b2018-03-27 17:41:04 -0700197 // it's allowed to POST on session collection & login without
198 // authentication
199 if ("POST"_method == req.method()) {
200 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
201 (req.url == "/redfish/v1/SessionService/Sessions/") ||
Ed Tanous9bd21fc2018-04-26 16:08:56 -0700202 (req.url == "/login")) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100203 return true;
204 }
205 }
206
207 return false;
208 }
Ed Tanous99923322017-03-03 14:21:24 -0800209};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700210
Ed Tanousba9f9a62017-10-11 16:40:35 -0700211// TODO(ed) see if there is a better way to allow middlewares to request
212// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700213// Possibly an init function on first construction?
214template <typename... Middlewares>
215void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700216 static_assert(
217 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
218 "TokenAuthorization middleware must be enabled in app to use "
219 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700220 CROW_ROUTE(app, "/login")
221 .methods(
222 "POST"_method)([&](const crow::request& req, crow::response& res) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700223 boost::string_view content_type = req.get_header_value("content-type");
224 boost::string_view username;
225 boost::string_view password;
226
Ed Tanous911ac312017-08-15 09:37:42 -0700227 bool looks_like_ibm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800228
Ed Tanouse0d918b2018-03-27 17:41:04 -0700229 // This object needs to be declared at this scope so the strings
230 // within it are not destroyed before we can use them
Ed Tanousdb024a52018-03-06 12:50:34 -0800231 nlohmann::json login_credentials;
Ed Tanous911ac312017-08-15 09:37:42 -0700232 // Check if auth was provided by a payload
233 if (content_type == "application/json") {
Ed Tanousdb024a52018-03-06 12:50:34 -0800234 login_credentials = nlohmann::json::parse(req.body, nullptr, false);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700235 if (login_credentials.is_discarded()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700236 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700237 res.end();
238 return;
239 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700240
Ed Tanousba9f9a62017-10-11 16:40:35 -0700241 // check for username/password in the root object
242 // THis method is how intel APIs authenticate
Ed Tanouse0d918b2018-03-27 17:41:04 -0700243 nlohmann::json::iterator user_it = login_credentials.find("username");
244 nlohmann::json::iterator pass_it = login_credentials.find("password");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700245 if (user_it != login_credentials.end() &&
246 pass_it != login_credentials.end()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700247 const std::string* user_str =
248 user_it->get_ptr<const std::string*>();
249 const std::string* pass_str =
250 pass_it->get_ptr<const std::string*>();
251 if (user_str != nullptr && pass_str != nullptr) {
252 username = *user_str;
253 password = *pass_str;
254 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700255 } else {
256 // Openbmc appears to push a data object that contains the same
257 // keys (username and password), attempt to use that
258 auto data_it = login_credentials.find("data");
259 if (data_it != login_credentials.end()) {
260 // Some apis produce an array of value ["username",
261 // "password"]
262 if (data_it->is_array()) {
263 if (data_it->size() == 2) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700264 nlohmann::json::iterator user_it2 = data_it->begin();
265 nlohmann::json::iterator pass_it2 = data_it->begin() + 1;
Ed Tanousba9f9a62017-10-11 16:40:35 -0700266 looks_like_ibm = true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700267 if (user_it2 != data_it->end() &&
268 pass_it2 != data_it->end()) {
269 const std::string* user_str =
270 user_it2->get_ptr<const std::string*>();
271 const std::string* pass_str =
272 pass_it2->get_ptr<const std::string*>();
273 if (user_str != nullptr && pass_str != nullptr) {
274 username = *user_str;
275 password = *pass_str;
276 }
277 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700278 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700279
Ed Tanousba9f9a62017-10-11 16:40:35 -0700280 } else if (data_it->is_object()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700281 nlohmann::json::iterator user_it2 = data_it->find("username");
282 nlohmann::json::iterator pass_it2 = data_it->find("password");
283 if (user_it2 != data_it->end() && pass_it2 != data_it->end()) {
284 const std::string* user_str =
285 user_it2->get_ptr<const std::string*>();
286 const std::string* pass_str =
287 pass_it2->get_ptr<const std::string*>();
288 if (user_str != nullptr && pass_str != nullptr) {
289 username = *user_str;
290 password = *pass_str;
291 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700292 }
293 }
294 }
295 }
Ed Tanous911ac312017-08-15 09:37:42 -0700296 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700297 // check if auth was provided as a headers
298 username = req.get_header_value("username");
299 password = req.get_header_value("password");
Ed Tanous911ac312017-08-15 09:37:42 -0700300 }
301
Ed Tanouse0d918b2018-03-27 17:41:04 -0700302 if (!username.empty() && !password.empty()) {
303 if (!pam_authenticate_user(username, password)) {
304 res.result(boost::beast::http::status::unauthorized);
Ed Tanous911ac312017-08-15 09:37:42 -0700305 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700306 auto session =
307 PersistentData::session_store->generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700308
Ed Tanousba9f9a62017-10-11 16:40:35 -0700309 if (looks_like_ibm) {
310 // IBM requires a very specific login structure, and doesn't
311 // actually look at the status code. TODO(ed).... Fix that
312 // upstream
Ed Tanouse0d918b2018-03-27 17:41:04 -0700313 res.json_value = {
314 {"data", "User '" + std::string(username) + "' logged in"},
315 {"message", "200 OK"},
316 {"status", "ok"}};
Ed Tanous9bd21fc2018-04-26 16:08:56 -0700317
318 // Hack alert. Boost beast by default doesn't let you declare
319 // multiple headers of the same name, and in most cases this is
320 // fine. Unfortunately here we need to set the Session cookie,
321 // which requires the httpOnly attribute, as well as the XSRF
322 // cookie, which requires it to not have an httpOnly attribute.
323 // To get the behavior we want, we simply inject the second
324 // "set-cookie" string into the value header, and get the result
325 // we want, even though we are technicaly declaring two headers
326 // here.
327 res.add_header("Set-Cookie",
328 "XSRF-TOKEN=" + session->csrf_token +
329 "; Secure\r\nSet-Cookie: SESSION=" +
330 session->session_token + "; Secure; HttpOnly");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700331 } else {
332 // if content type is json, assume json token
Ed Tanouse0d918b2018-03-27 17:41:04 -0700333 res.json_value = {{"token", session->session_token}};
Ed Tanous911ac312017-08-15 09:37:42 -0700334 }
335 }
336
337 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700338 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700339 }
340 res.end();
341 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100342
343 CROW_ROUTE(app, "/logout")
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100344 .methods("POST"_method)(
345 [&](const crow::request& req, crow::response& res) {
346 auto& session =
347 app.template get_context<TokenAuthorization::Middleware>(req)
348 .session;
349 if (session != nullptr) {
350 PersistentData::session_store->remove_session(session);
351 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100352 res.end();
353 return;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100354
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100355 });
Ed Tanous911ac312017-08-15 09:37:42 -0700356}
Ed Tanousdb024a52018-03-06 12:50:34 -0800357} // namespace TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700358} // namespace crow