blob: c89dcddbaf9e4e0bdc06ed161fada9db0e4310d5 [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
29 if (req.headers.count("X-Auth-Token") == 1) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010030 ctx.session = perform_xtoken_auth(req);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010031 } else if (req.headers.count("Cookie") == 1) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010032 ctx.session = perform_cookie_auth(req);
Ed Tanousbae064e2018-03-22 15:44:39 -070033 } else {
34 std::string auth_header = req.get_header_value("Authorization");
35 if (auth_header != "") {
36 // Reject any kind of auth other than basic or token
37 if (boost::starts_with(auth_header, "Token ")) {
38 ctx.session = perform_token_auth(auth_header);
39 } else if (boost::starts_with(auth_header, "Basic ")) {
40 ctx.session = perform_basic_auth(auth_header);
41 }
42 }
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
122 auto& token = req.get_header_value("X-Auth-Token");
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100123 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100124 return session;
125 }
126
127 const crow::PersistentData::UserSession* perform_cookie_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100128 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100129 CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
130
131 auto& cookie_value = req.get_header_value("Cookie");
132
133 auto start_index = cookie_value.find("SESSION=");
134 if (start_index == std::string::npos) {
135 return nullptr;
136 }
Ed Tanous41ff64d2018-01-30 13:13:38 -0800137 start_index += sizeof("SESSION=") - 1;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100138 auto end_index = cookie_value.find(";", start_index);
139 if (end_index == std::string::npos) {
140 end_index = cookie_value.size();
141 }
142 std::string auth_key =
143 cookie_value.substr(start_index, end_index - start_index);
144
145 const crow::PersistentData::UserSession* session =
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100146 PersistentData::session_store->login_session_by_token(auth_key);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100147 if (session == nullptr) {
148 return nullptr;
149 }
150
151 // RFC7231 defines methods that need csrf protection
152 if (req.method != "GET"_method) {
153 const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
154 // Make sure both tokens are filled
155 if (csrf.empty() || session->csrf_token.empty()) {
156 return nullptr;
157 }
158 // Reject if csrf token not available
159 if (csrf != session->csrf_token) {
160 return nullptr;
161 }
162 }
163 return session;
164 }
165
166 // checks if request can be forwarded without authentication
167 bool is_on_whitelist(const crow::request& req) const {
168 // it's allowed to GET root node without authentication
169 if ("GET"_method == req.method) {
170 if (req.url == "/redfish/v1") {
171 return true;
172 } else if (crow::webassets::routes.find(req.url) !=
173 crow::webassets::routes.end()) {
174 return true;
175 }
176 }
177
178 // it's allowed to POST on session collection & login without authentication
179 if ("POST"_method == req.method) {
180 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
181 (req.url == "/login") || (req.url == "/logout")) {
182 return true;
183 }
184 }
185
186 return false;
187 }
Ed Tanous99923322017-03-03 14:21:24 -0800188};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700189
Ed Tanousba9f9a62017-10-11 16:40:35 -0700190// TODO(ed) see if there is a better way to allow middlewares to request
191// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700192// Possibly an init function on first construction?
193template <typename... Middlewares>
194void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700195 static_assert(
196 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
197 "TokenAuthorization middleware must be enabled in app to use "
198 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700199 CROW_ROUTE(app, "/login")
200 .methods(
201 "POST"_method)([&](const crow::request& req, crow::response& res) {
202 std::string content_type;
203 auto content_type_it = req.headers.find("content-type");
204 if (content_type_it != req.headers.end()) {
205 content_type = content_type_it->second;
206 boost::algorithm::to_lower(content_type);
207 }
Ed Tanousdb024a52018-03-06 12:50:34 -0800208 const std::string* username;
209 const std::string* password;
Ed Tanous911ac312017-08-15 09:37:42 -0700210 bool looks_like_ibm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800211
Ed Tanousdb024a52018-03-06 12:50:34 -0800212 // This object needs to be declared at this scope so the strings within
213 // it are not destroyed before we can use them
214 nlohmann::json login_credentials;
Ed Tanous911ac312017-08-15 09:37:42 -0700215 // Check if auth was provided by a payload
216 if (content_type == "application/json") {
Ed Tanousdb024a52018-03-06 12:50:34 -0800217 login_credentials = nlohmann::json::parse(req.body, nullptr, false);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700218 if (login_credentials.is_discarded()) {
Ed Tanous911ac312017-08-15 09:37:42 -0700219 res.code = 400;
220 res.end();
221 return;
222 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700223 // check for username/password in the root object
224 // THis method is how intel APIs authenticate
225 auto user_it = login_credentials.find("username");
226 auto pass_it = login_credentials.find("password");
227 if (user_it != login_credentials.end() &&
228 pass_it != login_credentials.end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800229 username = user_it->get_ptr<const std::string*>();
230 password = pass_it->get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700231 } else {
232 // Openbmc appears to push a data object that contains the same
233 // keys (username and password), attempt to use that
234 auto data_it = login_credentials.find("data");
235 if (data_it != login_credentials.end()) {
236 // Some apis produce an array of value ["username",
237 // "password"]
238 if (data_it->is_array()) {
239 if (data_it->size() == 2) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800240 username = (*data_it)[0].get_ptr<const std::string*>();
241 password = (*data_it)[1].get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700242 looks_like_ibm = true;
243 }
244 } else if (data_it->is_object()) {
245 auto user_it = data_it->find("username");
246 auto pass_it = data_it->find("password");
247 if (user_it != data_it->end() && pass_it != data_it->end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800248 username = user_it->get_ptr<const std::string*>();
249 password = pass_it->get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700250 }
251 }
252 }
253 }
Ed Tanous911ac312017-08-15 09:37:42 -0700254 } else {
255 // check if auth was provided as a query string
256 auto user_it = req.headers.find("username");
257 auto pass_it = req.headers.find("password");
258 if (user_it != req.headers.end() && pass_it != req.headers.end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800259 username = &user_it->second;
260 password = &pass_it->second;
Ed Tanous911ac312017-08-15 09:37:42 -0700261 }
262 }
263
Ed Tanousdb024a52018-03-06 12:50:34 -0800264 if (username != nullptr && !username->empty() && password != nullptr &&
265 !password->empty()) {
266 if (!pam_authenticate_user(*username, *password)) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100267 res.code = res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
Ed Tanous911ac312017-08-15 09:37:42 -0700268 } else {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100269 auto& session =
Ed Tanousdb024a52018-03-06 12:50:34 -0800270 PersistentData::session_store->generate_user_session(*username);
Ed Tanous911ac312017-08-15 09:37:42 -0700271
Ed Tanousba9f9a62017-10-11 16:40:35 -0700272 if (looks_like_ibm) {
273 // IBM requires a very specific login structure, and doesn't
274 // actually look at the status code. TODO(ed).... Fix that
275 // upstream
Ed Tanousbae064e2018-03-22 15:44:39 -0700276 res.json_value = {{"data", "User '" + *username + "' logged in"},
277 {"message", "200 OK"},
278 {"status", "ok"}};
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100279 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100280 res.add_header("Set-Cookie", "SESSION=" + session.session_token +
281 "; Secure; HttpOnly");
Ed Tanousba9f9a62017-10-11 16:40:35 -0700282 } else {
283 // if content type is json, assume json token
Ed Tanousbae064e2018-03-22 15:44:39 -0700284 res.json_value = {{"token", session.session_token}};
Ed Tanous911ac312017-08-15 09:37:42 -0700285 }
286 }
287
288 } else {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100289 res.code = static_cast<int>(HttpRespCode::BAD_REQUEST);
Ed Tanous911ac312017-08-15 09:37:42 -0700290 }
291 res.end();
292 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100293
294 CROW_ROUTE(app, "/logout")
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100295 .methods("POST"_method)(
296 [&](const crow::request& req, crow::response& res) {
297 auto& session =
298 app.template get_context<TokenAuthorization::Middleware>(req)
299 .session;
300 if (session != nullptr) {
301 PersistentData::session_store->remove_session(session);
302 }
303 res.code = static_cast<int>(HttpRespCode::OK);
304 res.end();
305 return;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100306
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100307 });
Ed Tanous911ac312017-08-15 09:37:42 -0700308}
Ed Tanousdb024a52018-03-06 12:50:34 -0800309} // namespace TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700310} // namespace crow