blob: 59e9ccab50ac288f694d8b63cb204d7ee1a29056 [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>
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +01008#include <crow/http_codes.h>
Ed Tanousf9273472017-02-28 16:05:13 -08009#include <crow/http_request.h>
10#include <crow/http_response.h>
Ed Tanousba9f9a62017-10-11 16:40:35 -070011#include <boost/bimap.hpp>
Ed Tanous4758d5b2017-06-06 15:28:13 -070012#include <boost/container/flat_set.hpp>
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010013
Ed Tanous99923322017-03-03 14:21:24 -080014namespace crow {
Ed Tanousb4d29f42017-03-24 16:39:25 -070015
Ed Tanous911ac312017-08-15 09:37:42 -070016namespace TokenAuthorization {
Ed Tanousb4d29f42017-03-24 16:39:25 -070017
Ed Tanous911ac312017-08-15 09:37:42 -070018class Middleware {
Ed Tanous3dac7492017-08-02 13:46:20 -070019 public:
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010020 struct context {
21 const crow::PersistentData::UserSession* session;
22 };
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010023
24 void before_handle(crow::request& req, response& res, context& ctx) {
Ed Tanousbae064e2018-03-22 15:44:39 -070025 if (is_on_whitelist(req)) {
26 return;
27 }
28
Ed Tanous1ea9f062018-03-27 17:45:20 -070029 ctx.session = perform_xtoken_auth(req);
30
31 if (ctx.session == nullptr) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010032 ctx.session = perform_cookie_auth(req);
Ed Tanous1ea9f062018-03-27 17:45:20 -070033 }
34
35 const std::string& auth_header = req.get_header_value("Authorization");
36 // Reject any kind of auth other than basic or token
37 if (ctx.session == nullptr && boost::starts_with(auth_header, "Token ")) {
38 ctx.session = perform_token_auth(auth_header);
39 }
40
41 if (ctx.session == nullptr && boost::starts_with(auth_header, "Basic ")) {
42 ctx.session = perform_basic_auth(auth_header);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010043 }
44
Ed Tanousbae064e2018-03-22 15:44:39 -070045 if (ctx.session == nullptr) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010046 CROW_LOG_WARNING << "[AuthMiddleware] authorization failed";
47 res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
48 res.add_header("WWW-Authenticate", "Basic");
Ed Tanousf3d847c2017-06-12 16:01:42 -070049 res.end();
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010050 return;
51 }
Ed Tanousf9273472017-02-28 16:05:13 -080052
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010053 // TODO get user privileges here and propagate it via MW context
54 // else let the request continue unharmed
55 }
Ed Tanousf3d847c2017-06-12 16:01:42 -070056
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010057 template <typename AllContext>
58 void after_handle(request& req, response& res, context& ctx,
59 AllContext& allctx) {
60 // TODO(ed) THis should really be handled by the persistent data middleware,
61 // but because it is upstream, it doesn't have access to the session
62 // information. Should the data middleware persist the current user
63 // session?
64 if (ctx.session != nullptr &&
65 ctx.session->persistence ==
66 crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010067 PersistentData::session_store->remove_session(ctx.session);
Ed Tanousf3d847c2017-06-12 16:01:42 -070068 }
69 }
70
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010071 private:
72 const crow::PersistentData::UserSession* perform_basic_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010073 const std::string& auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010074 CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
75
76 std::string auth_data;
77 std::string param = auth_header.substr(strlen("Basic "));
78 if (!crow::utility::base64_decode(param, auth_data)) {
79 return nullptr;
80 }
81 std::size_t separator = auth_data.find(':');
82 if (separator == std::string::npos) {
83 return nullptr;
84 }
85
86 std::string user = auth_data.substr(0, separator);
87 separator += 1;
88 if (separator > auth_data.size()) {
89 return nullptr;
90 }
91 std::string pass = auth_data.substr(separator);
92
93 CROW_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
94
95 if (!pam_authenticate_user(user, pass)) {
96 return nullptr;
97 }
98
99 // TODO(ed) generate_user_session is a little expensive for basic
100 // auth, as it generates some random identifiers that will never be
101 // used. This should have a "fast" path for when user tokens aren't
102 // needed.
103 // This whole flow needs to be revisited anyway, as we can't be
104 // calling directly into pam for every request
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100105 return &(PersistentData::session_store->generate_user_session(
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100106 user, crow::PersistentData::PersistenceType::SINGLE_REQUEST));
Ed Tanousf3d847c2017-06-12 16:01:42 -0700107 }
Ed Tanous8041f312017-04-03 09:47:01 -0700108
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100109 const crow::PersistentData::UserSession* perform_token_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100110 const std::string& auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100111 CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication";
112
113 std::string token = auth_header.substr(strlen("Token "));
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100114 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100115 return session;
116 }
117
118 const crow::PersistentData::UserSession* perform_xtoken_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100119 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100120 CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
121
Ed Tanous1ea9f062018-03-27 17:45:20 -0700122 const std::string& token = req.get_header_value("X-Auth-Token");
123 if (token.empty()) {
124 return nullptr;
125 }
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
130 const crow::PersistentData::UserSession* perform_cookie_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] Cookie authentication";
133
134 auto& cookie_value = req.get_header_value("Cookie");
Ed Tanous1ea9f062018-03-27 17:45:20 -0700135 if (cookie_value.empty()) {
136 return nullptr;
137 }
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100138
139 auto start_index = cookie_value.find("SESSION=");
140 if (start_index == std::string::npos) {
141 return nullptr;
142 }
Ed Tanous41ff64d2018-01-30 13:13:38 -0800143 start_index += sizeof("SESSION=") - 1;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100144 auto end_index = cookie_value.find(";", start_index);
145 if (end_index == std::string::npos) {
146 end_index = cookie_value.size();
147 }
148 std::string auth_key =
149 cookie_value.substr(start_index, end_index - start_index);
150
151 const crow::PersistentData::UserSession* session =
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100152 PersistentData::session_store->login_session_by_token(auth_key);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100153 if (session == nullptr) {
154 return nullptr;
155 }
156
157 // RFC7231 defines methods that need csrf protection
158 if (req.method != "GET"_method) {
159 const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
160 // Make sure both tokens are filled
161 if (csrf.empty() || session->csrf_token.empty()) {
162 return nullptr;
163 }
164 // Reject if csrf token not available
165 if (csrf != session->csrf_token) {
166 return nullptr;
167 }
168 }
169 return session;
170 }
171
172 // checks if request can be forwarded without authentication
173 bool is_on_whitelist(const crow::request& req) const {
174 // it's allowed to GET root node without authentication
175 if ("GET"_method == req.method) {
176 if (req.url == "/redfish/v1") {
177 return true;
178 } else if (crow::webassets::routes.find(req.url) !=
179 crow::webassets::routes.end()) {
180 return true;
181 }
182 }
183
184 // it's allowed to POST on session collection & login without authentication
185 if ("POST"_method == req.method) {
186 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
187 (req.url == "/login") || (req.url == "/logout")) {
188 return true;
189 }
190 }
191
192 return false;
193 }
Ed Tanous99923322017-03-03 14:21:24 -0800194};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700195
Ed Tanousba9f9a62017-10-11 16:40:35 -0700196// TODO(ed) see if there is a better way to allow middlewares to request
197// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700198// Possibly an init function on first construction?
199template <typename... Middlewares>
200void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700201 static_assert(
202 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
203 "TokenAuthorization middleware must be enabled in app to use "
204 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700205 CROW_ROUTE(app, "/login")
206 .methods(
207 "POST"_method)([&](const crow::request& req, crow::response& res) {
208 std::string content_type;
209 auto content_type_it = req.headers.find("content-type");
210 if (content_type_it != req.headers.end()) {
211 content_type = content_type_it->second;
212 boost::algorithm::to_lower(content_type);
213 }
Ed Tanousdb024a52018-03-06 12:50:34 -0800214 const std::string* username;
215 const std::string* password;
Ed Tanous911ac312017-08-15 09:37:42 -0700216 bool looks_like_ibm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800217
Ed Tanousdb024a52018-03-06 12:50:34 -0800218 // This object needs to be declared at this scope so the strings within
219 // it are not destroyed before we can use them
220 nlohmann::json login_credentials;
Ed Tanous911ac312017-08-15 09:37:42 -0700221 // Check if auth was provided by a payload
222 if (content_type == "application/json") {
Ed Tanousdb024a52018-03-06 12:50:34 -0800223 login_credentials = nlohmann::json::parse(req.body, nullptr, false);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700224 if (login_credentials.is_discarded()) {
Ed Tanous911ac312017-08-15 09:37:42 -0700225 res.code = 400;
226 res.end();
227 return;
228 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700229 // check for username/password in the root object
230 // THis method is how intel APIs authenticate
231 auto user_it = login_credentials.find("username");
232 auto pass_it = login_credentials.find("password");
233 if (user_it != login_credentials.end() &&
234 pass_it != login_credentials.end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800235 username = user_it->get_ptr<const std::string*>();
236 password = pass_it->get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700237 } else {
238 // Openbmc appears to push a data object that contains the same
239 // keys (username and password), attempt to use that
240 auto data_it = login_credentials.find("data");
241 if (data_it != login_credentials.end()) {
242 // Some apis produce an array of value ["username",
243 // "password"]
244 if (data_it->is_array()) {
245 if (data_it->size() == 2) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800246 username = (*data_it)[0].get_ptr<const std::string*>();
247 password = (*data_it)[1].get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700248 looks_like_ibm = true;
249 }
250 } else if (data_it->is_object()) {
251 auto user_it = data_it->find("username");
252 auto pass_it = data_it->find("password");
253 if (user_it != data_it->end() && pass_it != data_it->end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800254 username = user_it->get_ptr<const std::string*>();
255 password = pass_it->get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700256 }
257 }
258 }
259 }
Ed Tanous911ac312017-08-15 09:37:42 -0700260 } else {
261 // check if auth was provided as a query string
262 auto user_it = req.headers.find("username");
263 auto pass_it = req.headers.find("password");
264 if (user_it != req.headers.end() && pass_it != req.headers.end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800265 username = &user_it->second;
266 password = &pass_it->second;
Ed Tanous911ac312017-08-15 09:37:42 -0700267 }
268 }
269
Ed Tanousdb024a52018-03-06 12:50:34 -0800270 if (username != nullptr && !username->empty() && password != nullptr &&
271 !password->empty()) {
272 if (!pam_authenticate_user(*username, *password)) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100273 res.code = res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
Ed Tanous911ac312017-08-15 09:37:42 -0700274 } else {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100275 auto& session =
Ed Tanousdb024a52018-03-06 12:50:34 -0800276 PersistentData::session_store->generate_user_session(*username);
Ed Tanous911ac312017-08-15 09:37:42 -0700277
Ed Tanousba9f9a62017-10-11 16:40:35 -0700278 if (looks_like_ibm) {
279 // IBM requires a very specific login structure, and doesn't
280 // actually look at the status code. TODO(ed).... Fix that
281 // upstream
Ed Tanousbae064e2018-03-22 15:44:39 -0700282 res.json_value = {{"data", "User '" + *username + "' logged in"},
283 {"message", "200 OK"},
284 {"status", "ok"}};
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100285 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100286 res.add_header("Set-Cookie", "SESSION=" + session.session_token +
287 "; Secure; HttpOnly");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700288 } else {
289 // if content type is json, assume json token
Ed Tanousbae064e2018-03-22 15:44:39 -0700290 res.json_value = {{"token", session.session_token}};
Ed Tanous911ac312017-08-15 09:37:42 -0700291 }
292 }
293
294 } else {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100295 res.code = static_cast<int>(HttpRespCode::BAD_REQUEST);
Ed Tanous911ac312017-08-15 09:37:42 -0700296 }
297 res.end();
298 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100299
300 CROW_ROUTE(app, "/logout")
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100301 .methods("POST"_method)(
302 [&](const crow::request& req, crow::response& res) {
303 auto& session =
304 app.template get_context<TokenAuthorization::Middleware>(req)
305 .session;
306 if (session != nullptr) {
307 PersistentData::session_store->remove_session(session);
308 }
309 res.code = static_cast<int>(HttpRespCode::OK);
310 res.end();
311 return;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100312
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100313 });
Ed Tanous911ac312017-08-15 09:37:42 -0700314}
Ed Tanousdb024a52018-03-06 12:50:34 -0800315} // namespace TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700316} // namespace crow