blob: 49649dd978cff1d9165745280e2c1b1cdab37749 [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
47 res.result(boost::beast::http::status::unauthorized);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010048 res.add_header("WWW-Authenticate", "Basic");
Ed Tanouse0d918b2018-03-27 17:41:04 -070049
Ed Tanousf3d847c2017-06-12 16:01:42 -070050 res.end();
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010051 return;
52 }
Ed Tanousf9273472017-02-28 16:05:13 -080053
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010054 // TODO get user privileges here and propagate it via MW context
55 // else let the request continue unharmed
56 }
Ed Tanousf3d847c2017-06-12 16:01:42 -070057
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010058 template <typename AllContext>
59 void after_handle(request& req, response& res, context& ctx,
60 AllContext& allctx) {
Ed Tanouse0d918b2018-03-27 17:41:04 -070061 // TODO(ed) THis should really be handled by the persistent data
62 // middleware, but because it is upstream, it doesn't have access to the
63 // session information. Should the data middleware persist the current
64 // user session?
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010065 if (ctx.session != nullptr &&
66 ctx.session->persistence ==
67 crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010068 PersistentData::session_store->remove_session(ctx.session);
Ed Tanousf3d847c2017-06-12 16:01:42 -070069 }
70 }
71
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010072 private:
Ed Tanouse0d918b2018-03-27 17:41:04 -070073 const std::shared_ptr<crow::PersistentData::UserSession> perform_basic_auth(
74 boost::string_view auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010075 CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
76
77 std::string auth_data;
Ed Tanouse0d918b2018-03-27 17:41:04 -070078 boost::string_view param = auth_header.substr(strlen("Basic "));
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010079 if (!crow::utility::base64_decode(param, auth_data)) {
80 return nullptr;
81 }
82 std::size_t separator = auth_data.find(':');
83 if (separator == std::string::npos) {
84 return nullptr;
85 }
86
87 std::string user = auth_data.substr(0, separator);
88 separator += 1;
89 if (separator > auth_data.size()) {
90 return nullptr;
91 }
92 std::string pass = auth_data.substr(separator);
93
94 CROW_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
95
96 if (!pam_authenticate_user(user, pass)) {
97 return nullptr;
98 }
99
100 // TODO(ed) generate_user_session is a little expensive for basic
101 // auth, as it generates some random identifiers that will never be
102 // used. This should have a "fast" path for when user tokens aren't
103 // needed.
104 // This whole flow needs to be revisited anyway, as we can't be
105 // calling directly into pam for every request
Ed Tanouse0d918b2018-03-27 17:41:04 -0700106 return PersistentData::session_store->generate_user_session(
107 user, crow::PersistentData::PersistenceType::SINGLE_REQUEST);
Ed Tanousf3d847c2017-06-12 16:01:42 -0700108 }
Ed Tanous8041f312017-04-03 09:47:01 -0700109
Ed Tanouse0d918b2018-03-27 17:41:04 -0700110 const std::shared_ptr<crow::PersistentData::UserSession> perform_token_auth(
111 boost::string_view auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100112 CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication";
113
Ed Tanouse0d918b2018-03-27 17:41:04 -0700114 boost::string_view token = auth_header.substr(strlen("Token "));
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100115 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100116 return session;
117 }
118
Ed Tanouse0d918b2018-03-27 17:41:04 -0700119 const std::shared_ptr<crow::PersistentData::UserSession> perform_xtoken_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100120 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100121 CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
122
Ed Tanouse0d918b2018-03-27 17:41:04 -0700123 boost::string_view token = req.get_header_value("X-Auth-Token");
Ed Tanous1ea9f062018-03-27 17:45:20 -0700124 if (token.empty()) {
125 return nullptr;
126 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100127 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100128 return session;
129 }
130
Ed Tanouse0d918b2018-03-27 17:41:04 -0700131 const std::shared_ptr<crow::PersistentData::UserSession> perform_cookie_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100132 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100133 CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
134
Ed Tanouse0d918b2018-03-27 17:41:04 -0700135 boost::string_view cookie_value = req.get_header_value("Cookie");
Ed Tanous1ea9f062018-03-27 17:45:20 -0700136 if (cookie_value.empty()) {
137 return nullptr;
138 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100139
140 auto start_index = cookie_value.find("SESSION=");
141 if (start_index == std::string::npos) {
142 return nullptr;
143 }
Ed Tanous41ff64d2018-01-30 13:13:38 -0800144 start_index += sizeof("SESSION=") - 1;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100145 auto end_index = cookie_value.find(";", start_index);
146 if (end_index == std::string::npos) {
147 end_index = cookie_value.size();
148 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700149 boost::string_view auth_key =
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100150 cookie_value.substr(start_index, end_index - start_index);
151
Ed Tanouse0d918b2018-03-27 17:41:04 -0700152 const std::shared_ptr<crow::PersistentData::UserSession> session =
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100153 PersistentData::session_store->login_session_by_token(auth_key);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100154 if (session == nullptr) {
155 return nullptr;
156 }
157
158 // RFC7231 defines methods that need csrf protection
Ed Tanouse0d918b2018-03-27 17:41:04 -0700159 if (req.method() != "GET"_method) {
160 boost::string_view csrf = req.get_header_value("X-XSRF-TOKEN");
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100161 // Make sure both tokens are filled
162 if (csrf.empty() || session->csrf_token.empty()) {
163 return nullptr;
164 }
165 // Reject if csrf token not available
166 if (csrf != session->csrf_token) {
167 return nullptr;
168 }
169 }
170 return session;
171 }
172
173 // checks if request can be forwarded without authentication
174 bool is_on_whitelist(const crow::request& req) const {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700175 // it's allowed to GET root node without authentica tion
176 if ("GET"_method == req.method()) {
Ed Tanousaa2e59c2018-04-12 12:17:20 -0700177 if (req.url == "/redfish/v1" || req.url == "/redfish/v1/") {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100178 return true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700179 } else if (crow::webassets::routes.find(std::string(req.url)) !=
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100180 crow::webassets::routes.end()) {
181 return true;
182 }
183 }
184
Ed Tanouse0d918b2018-03-27 17:41:04 -0700185 // it's allowed to POST on session collection & login without
186 // authentication
187 if ("POST"_method == req.method()) {
188 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
189 (req.url == "/redfish/v1/SessionService/Sessions/") ||
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100190 (req.url == "/login") || (req.url == "/logout")) {
191 return true;
192 }
193 }
194
195 return false;
196 }
Ed Tanous99923322017-03-03 14:21:24 -0800197};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700198
Ed Tanousba9f9a62017-10-11 16:40:35 -0700199// TODO(ed) see if there is a better way to allow middlewares to request
200// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700201// Possibly an init function on first construction?
202template <typename... Middlewares>
203void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700204 static_assert(
205 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
206 "TokenAuthorization middleware must be enabled in app to use "
207 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700208 CROW_ROUTE(app, "/login")
209 .methods(
210 "POST"_method)([&](const crow::request& req, crow::response& res) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700211 boost::string_view content_type = req.get_header_value("content-type");
212 boost::string_view username;
213 boost::string_view password;
214
Ed Tanous911ac312017-08-15 09:37:42 -0700215 bool looks_like_ibm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800216
Ed Tanouse0d918b2018-03-27 17:41:04 -0700217 // This object needs to be declared at this scope so the strings
218 // within it are not destroyed before we can use them
Ed Tanousdb024a52018-03-06 12:50:34 -0800219 nlohmann::json login_credentials;
Ed Tanous911ac312017-08-15 09:37:42 -0700220 // Check if auth was provided by a payload
221 if (content_type == "application/json") {
Ed Tanousdb024a52018-03-06 12:50:34 -0800222 login_credentials = nlohmann::json::parse(req.body, nullptr, false);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700223 if (login_credentials.is_discarded()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700224 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700225 res.end();
226 return;
227 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700228
Ed Tanousba9f9a62017-10-11 16:40:35 -0700229 // check for username/password in the root object
230 // THis method is how intel APIs authenticate
Ed Tanouse0d918b2018-03-27 17:41:04 -0700231 nlohmann::json::iterator user_it = login_credentials.find("username");
232 nlohmann::json::iterator pass_it = login_credentials.find("password");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700233 if (user_it != login_credentials.end() &&
234 pass_it != login_credentials.end()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700235 const std::string* user_str =
236 user_it->get_ptr<const std::string*>();
237 const std::string* pass_str =
238 pass_it->get_ptr<const std::string*>();
239 if (user_str != nullptr && pass_str != nullptr) {
240 username = *user_str;
241 password = *pass_str;
242 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700243 } else {
244 // Openbmc appears to push a data object that contains the same
245 // keys (username and password), attempt to use that
246 auto data_it = login_credentials.find("data");
247 if (data_it != login_credentials.end()) {
248 // Some apis produce an array of value ["username",
249 // "password"]
250 if (data_it->is_array()) {
251 if (data_it->size() == 2) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700252 nlohmann::json::iterator user_it2 = data_it->begin();
253 nlohmann::json::iterator pass_it2 = data_it->begin() + 1;
Ed Tanousba9f9a62017-10-11 16:40:35 -0700254 looks_like_ibm = true;
Ed Tanouse0d918b2018-03-27 17:41:04 -0700255 if (user_it2 != data_it->end() &&
256 pass_it2 != data_it->end()) {
257 const std::string* user_str =
258 user_it2->get_ptr<const std::string*>();
259 const std::string* pass_str =
260 pass_it2->get_ptr<const std::string*>();
261 if (user_str != nullptr && pass_str != nullptr) {
262 username = *user_str;
263 password = *pass_str;
264 }
265 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700266 }
Ed Tanouse0d918b2018-03-27 17:41:04 -0700267
Ed Tanousba9f9a62017-10-11 16:40:35 -0700268 } else if (data_it->is_object()) {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700269 nlohmann::json::iterator user_it2 = data_it->find("username");
270 nlohmann::json::iterator pass_it2 = data_it->find("password");
271 if (user_it2 != data_it->end() && pass_it2 != data_it->end()) {
272 const std::string* user_str =
273 user_it2->get_ptr<const std::string*>();
274 const std::string* pass_str =
275 pass_it2->get_ptr<const std::string*>();
276 if (user_str != nullptr && pass_str != nullptr) {
277 username = *user_str;
278 password = *pass_str;
279 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700280 }
281 }
282 }
283 }
Ed Tanous911ac312017-08-15 09:37:42 -0700284 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700285 // check if auth was provided as a headers
286 username = req.get_header_value("username");
287 password = req.get_header_value("password");
Ed Tanous911ac312017-08-15 09:37:42 -0700288 }
289
Ed Tanouse0d918b2018-03-27 17:41:04 -0700290 if (!username.empty() && !password.empty()) {
291 if (!pam_authenticate_user(username, password)) {
292 res.result(boost::beast::http::status::unauthorized);
Ed Tanous911ac312017-08-15 09:37:42 -0700293 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700294 auto session =
295 PersistentData::session_store->generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700296
Ed Tanousba9f9a62017-10-11 16:40:35 -0700297 if (looks_like_ibm) {
298 // IBM requires a very specific login structure, and doesn't
299 // actually look at the status code. TODO(ed).... Fix that
300 // upstream
Ed Tanouse0d918b2018-03-27 17:41:04 -0700301 res.json_value = {
302 {"data", "User '" + std::string(username) + "' logged in"},
303 {"message", "200 OK"},
304 {"status", "ok"}};
305 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session->csrf_token);
306 res.add_header("Set-Cookie", "SESSION=" + session->session_token +
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100307 "; Secure; HttpOnly");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700308 } else {
309 // if content type is json, assume json token
Ed Tanouse0d918b2018-03-27 17:41:04 -0700310 res.json_value = {{"token", session->session_token}};
Ed Tanous911ac312017-08-15 09:37:42 -0700311 }
312 }
313
314 } else {
Ed Tanouse0d918b2018-03-27 17:41:04 -0700315 res.result(boost::beast::http::status::bad_request);
Ed Tanous911ac312017-08-15 09:37:42 -0700316 }
317 res.end();
318 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100319
320 CROW_ROUTE(app, "/logout")
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100321 .methods("POST"_method)(
322 [&](const crow::request& req, crow::response& res) {
323 auto& session =
324 app.template get_context<TokenAuthorization::Middleware>(req)
325 .session;
326 if (session != nullptr) {
327 PersistentData::session_store->remove_session(session);
328 }
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100329 res.end();
330 return;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100331
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100332 });
Ed Tanous911ac312017-08-15 09:37:42 -0700333}
Ed Tanousdb024a52018-03-06 12:50:34 -0800334} // namespace TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700335} // namespace crow