blob: 0c0c474a1922ff49965378b357bc3c6e71b56b02 [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 }
168
169 // 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 }
181 return session;
182 }
183
184 // checks if request can be forwarded without authentication
185 bool is_on_whitelist(const crow::request& req) const {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700186 // it's allowed to GET root node without authentica tion
187 if ("GET"_method == req.method()) {
Ed Tanousaa2e59c2018-04-12 12:17:20 -0700188 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/") {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100189 return true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700190 } else if (crow::webassets::routes.find(std::string(req.url)) !=
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100191 crow::webassets::routes.end()) {
192 return true;
193 }
194 }
195
Ed Tanouse0d918b2018-03-27 17:41:04 -0700196 // it's allowed to POST on session collection & login without
197 // authentication
198 if ("POST"_method == req.method()) {
199 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
200 (req.url == "/redfish/v1/SessionService/Sessions/") ||
Ed Tanous9bd21fc2018-04-26 16:08:56 -0700201 (req.url == "/login")) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100202 return true;
203 }
204 }
205
206 return false;
207 }
Ed Tanous99923322017-03-03 14:21:24 -0800208};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700209
Ed Tanousba9f9a62017-10-11 16:40:35 -0700210// TODO(ed) see if there is a better way to allow middlewares to request
211// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700212// Possibly an init function on first construction?
213template <typename... Middlewares>
214void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700215 static_assert(
216 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
217 "TokenAuthorization middleware must be enabled in app to use "
218 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700219 CROW_ROUTE(app, "/login")
220 .methods(
221 "POST"_method)([&](const crow::request& req, crow::response& res) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700222 boost::string_view content_type = req.get_header_value("content-type");
223 boost::string_view username;
224 boost::string_view password;
225
Ed Tanous911ac312017-08-15 09:37:42 -0700226 bool looks_like_ibm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800227
Ed Tanouse0d918b2018-03-27 17:41:04 -0700228 // This object needs to be declared at this scope so the strings
229 // within it are not destroyed before we can use them
Ed Tanousdb024a52018-03-06 12:50:34 -0800230 nlohmann::json login_credentials;
Ed Tanous911ac312017-08-15 09:37:42 -0700231 // Check if auth was provided by a payload
232 if (content_type == "application/json") {
Ed Tanousdb024a52018-03-06 12:50:34 -0800233 login_credentials = nlohmann::json::parse(req.body, nullptr, false);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700234 if (login_credentials.is_discarded()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700235 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700236 res.end();
237 return;
238 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700239
Ed Tanousba9f9a62017-10-11 16:40:35 -0700240 // check for username/password in the root object
241 // THis method is how intel APIs authenticate
Ed Tanouse0d918b2018-03-27 17:41:04 -0700242 nlohmann::json::iterator user_it = login_credentials.find("username");
243 nlohmann::json::iterator pass_it = login_credentials.find("password");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700244 if (user_it != login_credentials.end() &&
245 pass_it != login_credentials.end()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700246 const std::string* user_str =
247 user_it->get_ptr<const std::string*>();
248 const std::string* pass_str =
249 pass_it->get_ptr<const std::string*>();
250 if (user_str != nullptr && pass_str != nullptr) {
251 username = *user_str;
252 password = *pass_str;
253 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700254 } else {
255 // Openbmc appears to push a data object that contains the same
256 // keys (username and password), attempt to use that
257 auto data_it = login_credentials.find("data");
258 if (data_it != login_credentials.end()) {
259 // Some apis produce an array of value ["username",
260 // "password"]
261 if (data_it->is_array()) {
262 if (data_it->size() == 2) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700263 nlohmann::json::iterator user_it2 = data_it->begin();
264 nlohmann::json::iterator pass_it2 = data_it->begin() + 1;
Ed Tanousba9f9a62017-10-11 16:40:35 -0700265 looks_like_ibm = true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700266 if (user_it2 != data_it->end() &&
267 pass_it2 != data_it->end()) {
268 const std::string* user_str =
269 user_it2->get_ptr<const std::string*>();
270 const std::string* pass_str =
271 pass_it2->get_ptr<const std::string*>();
272 if (user_str != nullptr && pass_str != nullptr) {
273 username = *user_str;
274 password = *pass_str;
275 }
276 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700277 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700278
Ed Tanousba9f9a62017-10-11 16:40:35 -0700279 } else if (data_it->is_object()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700280 nlohmann::json::iterator user_it2 = data_it->find("username");
281 nlohmann::json::iterator pass_it2 = data_it->find("password");
282 if (user_it2 != data_it->end() && pass_it2 != data_it->end()) {
283 const std::string* user_str =
284 user_it2->get_ptr<const std::string*>();
285 const std::string* pass_str =
286 pass_it2->get_ptr<const std::string*>();
287 if (user_str != nullptr && pass_str != nullptr) {
288 username = *user_str;
289 password = *pass_str;
290 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700291 }
292 }
293 }
294 }
Ed Tanous911ac312017-08-15 09:37:42 -0700295 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700296 // check if auth was provided as a headers
297 username = req.get_header_value("username");
298 password = req.get_header_value("password");
Ed Tanous911ac312017-08-15 09:37:42 -0700299 }
300
Ed Tanouse0d918b2018-03-27 17:41:04 -0700301 if (!username.empty() && !password.empty()) {
302 if (!pam_authenticate_user(username, password)) {
303 res.result(boost::beast::http::status::unauthorized);
Ed Tanous911ac312017-08-15 09:37:42 -0700304 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700305 auto session =
306 PersistentData::session_store->generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700307
Ed Tanousba9f9a62017-10-11 16:40:35 -0700308 if (looks_like_ibm) {
309 // IBM requires a very specific login structure, and doesn't
310 // actually look at the status code. TODO(ed).... Fix that
311 // upstream
Ed Tanouse0d918b2018-03-27 17:41:04 -0700312 res.json_value = {
313 {"data", "User '" + std::string(username) + "' logged in"},
314 {"message", "200 OK"},
315 {"status", "ok"}};
Ed Tanous9bd21fc2018-04-26 16:08:56 -0700316
317 // Hack alert. Boost beast by default doesn't let you declare
318 // multiple headers of the same name, and in most cases this is
319 // fine. Unfortunately here we need to set the Session cookie,
320 // which requires the httpOnly attribute, as well as the XSRF
321 // cookie, which requires it to not have an httpOnly attribute.
322 // To get the behavior we want, we simply inject the second
323 // "set-cookie" string into the value header, and get the result
324 // we want, even though we are technicaly declaring two headers
325 // here.
326 res.add_header("Set-Cookie",
327 "XSRF-TOKEN=" + session->csrf_token +
328 "; Secure\r\nSet-Cookie: SESSION=" +
329 session->session_token + "; Secure; HttpOnly");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700330 } else {
331 // if content type is json, assume json token
Ed Tanouse0d918b2018-03-27 17:41:04 -0700332 res.json_value = {{"token", session->session_token}};
Ed Tanous911ac312017-08-15 09:37:42 -0700333 }
334 }
335
336 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700337 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700338 }
339 res.end();
340 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100341
342 CROW_ROUTE(app, "/logout")
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100343 .methods("POST"_method)(
344 [&](const crow::request& req, crow::response& res) {
345 auto& session =
346 app.template get_context<TokenAuthorization::Middleware>(req)
347 .session;
348 if (session != nullptr) {
349 PersistentData::session_store->remove_session(session);
350 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100351 res.end();
352 return;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100353
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100354 });
Ed Tanous911ac312017-08-15 09:37:42 -0700355}
Ed Tanousdb024a52018-03-06 12:50:34 -0800356} // namespace TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700357} // namespace crow