blob: a5c3ef851e70e7e2a8b2f49870488e93aa7fe9ab [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) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010025 std::string auth_header = req.get_header_value("Authorization");
26 if (auth_header != "") {
27 // Reject any kind of auth other than basic or token
28 if (boost::starts_with(auth_header, "Basic ")) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010029 ctx.session = perform_basic_auth(auth_header);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010030 } else if (boost::starts_with(auth_header, "Token ")) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010031 ctx.session = perform_token_auth(auth_header);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010032 }
33 } else if (req.headers.count("X-Auth-Token") == 1) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010034 ctx.session = perform_xtoken_auth(req);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010035 } else if (req.headers.count("Cookie") == 1) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010036 ctx.session = perform_cookie_auth(req);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010037 }
38
39 if (ctx.session == nullptr && !is_on_whitelist(req)) {
40 CROW_LOG_WARNING << "[AuthMiddleware] authorization failed";
41 res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
42 res.add_header("WWW-Authenticate", "Basic");
Ed Tanousf3d847c2017-06-12 16:01:42 -070043 res.end();
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010044 return;
45 }
Ed Tanousf9273472017-02-28 16:05:13 -080046
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010047 // TODO get user privileges here and propagate it via MW context
48 // else let the request continue unharmed
49 }
Ed Tanousf3d847c2017-06-12 16:01:42 -070050
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010051 template <typename AllContext>
52 void after_handle(request& req, response& res, context& ctx,
53 AllContext& allctx) {
54 // TODO(ed) THis should really be handled by the persistent data middleware,
55 // but because it is upstream, it doesn't have access to the session
56 // information. Should the data middleware persist the current user
57 // session?
58 if (ctx.session != nullptr &&
59 ctx.session->persistence ==
60 crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010061 PersistentData::session_store->remove_session(ctx.session);
Ed Tanousf3d847c2017-06-12 16:01:42 -070062 }
63 }
64
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010065 private:
66 const crow::PersistentData::UserSession* perform_basic_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010067 const std::string& auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010068 CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
69
70 std::string auth_data;
71 std::string param = auth_header.substr(strlen("Basic "));
72 if (!crow::utility::base64_decode(param, auth_data)) {
73 return nullptr;
74 }
75 std::size_t separator = auth_data.find(':');
76 if (separator == std::string::npos) {
77 return nullptr;
78 }
79
80 std::string user = auth_data.substr(0, separator);
81 separator += 1;
82 if (separator > auth_data.size()) {
83 return nullptr;
84 }
85 std::string pass = auth_data.substr(separator);
86
87 CROW_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
88
89 if (!pam_authenticate_user(user, pass)) {
90 return nullptr;
91 }
92
93 // TODO(ed) generate_user_session is a little expensive for basic
94 // auth, as it generates some random identifiers that will never be
95 // used. This should have a "fast" path for when user tokens aren't
96 // needed.
97 // This whole flow needs to be revisited anyway, as we can't be
98 // calling directly into pam for every request
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +010099 return &(PersistentData::session_store->generate_user_session(
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100100 user, crow::PersistentData::PersistenceType::SINGLE_REQUEST));
Ed Tanousf3d847c2017-06-12 16:01:42 -0700101 }
Ed Tanous8041f312017-04-03 09:47:01 -0700102
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100103 const crow::PersistentData::UserSession* perform_token_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100104 const std::string& auth_header) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100105 CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication";
106
107 std::string token = auth_header.substr(strlen("Token "));
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100108 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100109 return session;
110 }
111
112 const crow::PersistentData::UserSession* perform_xtoken_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100113 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100114 CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
115
116 auto& token = req.get_header_value("X-Auth-Token");
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100117 auto session = PersistentData::session_store->login_session_by_token(token);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100118 return session;
119 }
120
121 const crow::PersistentData::UserSession* perform_cookie_auth(
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100122 const crow::request& req) const {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100123 CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
124
125 auto& cookie_value = req.get_header_value("Cookie");
126
127 auto start_index = cookie_value.find("SESSION=");
128 if (start_index == std::string::npos) {
129 return nullptr;
130 }
Ed Tanous41ff64d2018-01-30 13:13:38 -0800131 start_index += sizeof("SESSION=") - 1;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100132 auto end_index = cookie_value.find(";", start_index);
133 if (end_index == std::string::npos) {
134 end_index = cookie_value.size();
135 }
136 std::string auth_key =
137 cookie_value.substr(start_index, end_index - start_index);
138
139 const crow::PersistentData::UserSession* session =
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100140 PersistentData::session_store->login_session_by_token(auth_key);
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100141 if (session == nullptr) {
142 return nullptr;
143 }
144
145 // RFC7231 defines methods that need csrf protection
146 if (req.method != "GET"_method) {
147 const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
148 // Make sure both tokens are filled
149 if (csrf.empty() || session->csrf_token.empty()) {
150 return nullptr;
151 }
152 // Reject if csrf token not available
153 if (csrf != session->csrf_token) {
154 return nullptr;
155 }
156 }
157 return session;
158 }
159
160 // checks if request can be forwarded without authentication
161 bool is_on_whitelist(const crow::request& req) const {
162 // it's allowed to GET root node without authentication
163 if ("GET"_method == req.method) {
164 if (req.url == "/redfish/v1") {
165 return true;
166 } else if (crow::webassets::routes.find(req.url) !=
167 crow::webassets::routes.end()) {
168 return true;
169 }
170 }
171
172 // it's allowed to POST on session collection & login without authentication
173 if ("POST"_method == req.method) {
174 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
175 (req.url == "/login") || (req.url == "/logout")) {
176 return true;
177 }
178 }
179
180 return false;
181 }
Ed Tanous99923322017-03-03 14:21:24 -0800182};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700183
Ed Tanousba9f9a62017-10-11 16:40:35 -0700184// TODO(ed) see if there is a better way to allow middlewares to request
185// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700186// Possibly an init function on first construction?
187template <typename... Middlewares>
188void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700189 static_assert(
190 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
191 "TokenAuthorization middleware must be enabled in app to use "
192 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700193 CROW_ROUTE(app, "/login")
194 .methods(
195 "POST"_method)([&](const crow::request& req, crow::response& res) {
196 std::string content_type;
197 auto content_type_it = req.headers.find("content-type");
198 if (content_type_it != req.headers.end()) {
199 content_type = content_type_it->second;
200 boost::algorithm::to_lower(content_type);
201 }
Ed Tanousdb024a52018-03-06 12:50:34 -0800202 const std::string* username;
203 const std::string* password;
Ed Tanous911ac312017-08-15 09:37:42 -0700204 bool looks_like_ibm = false;
Ed Tanousdb024a52018-03-06 12:50:34 -0800205
206
207 // This object needs to be declared at this scope so the strings within
208 // it are not destroyed before we can use them
209 nlohmann::json login_credentials;
Ed Tanous911ac312017-08-15 09:37:42 -0700210 // Check if auth was provided by a payload
211 if (content_type == "application/json") {
Ed Tanousdb024a52018-03-06 12:50:34 -0800212 login_credentials = nlohmann::json::parse(req.body, nullptr, false);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700213 if (login_credentials.is_discarded()) {
Ed Tanous911ac312017-08-15 09:37:42 -0700214 res.code = 400;
215 res.end();
216 return;
217 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700218 // check for username/password in the root object
219 // THis method is how intel APIs authenticate
220 auto user_it = login_credentials.find("username");
221 auto pass_it = login_credentials.find("password");
222 if (user_it != login_credentials.end() &&
223 pass_it != login_credentials.end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800224 username = user_it->get_ptr<const std::string*>();
225 password = pass_it->get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700226 } else {
227 // Openbmc appears to push a data object that contains the same
228 // keys (username and password), attempt to use that
229 auto data_it = login_credentials.find("data");
230 if (data_it != login_credentials.end()) {
231 // Some apis produce an array of value ["username",
232 // "password"]
233 if (data_it->is_array()) {
234 if (data_it->size() == 2) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800235 username = (*data_it)[0].get_ptr<const std::string*>();
236 password = (*data_it)[1].get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700237 looks_like_ibm = true;
238 }
239 } else if (data_it->is_object()) {
240 auto user_it = data_it->find("username");
241 auto pass_it = data_it->find("password");
242 if (user_it != data_it->end() && pass_it != data_it->end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800243 username = user_it->get_ptr<const std::string*>();
244 password = pass_it->get_ptr<const std::string*>();
Ed Tanousba9f9a62017-10-11 16:40:35 -0700245 }
246 }
247 }
248 }
Ed Tanous911ac312017-08-15 09:37:42 -0700249 } else {
250 // check if auth was provided as a query string
251 auto user_it = req.headers.find("username");
252 auto pass_it = req.headers.find("password");
253 if (user_it != req.headers.end() && pass_it != req.headers.end()) {
Ed Tanousdb024a52018-03-06 12:50:34 -0800254 username = &user_it->second;
255 password = &pass_it->second;
Ed Tanous911ac312017-08-15 09:37:42 -0700256 }
257 }
258
Ed Tanousdb024a52018-03-06 12:50:34 -0800259 if (username != nullptr && !username->empty() && password != nullptr &&
260 !password->empty()) {
261 if (!pam_authenticate_user(*username, *password)) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100262 res.code = res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
Ed Tanous911ac312017-08-15 09:37:42 -0700263 } else {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100264 auto& session =
Ed Tanousdb024a52018-03-06 12:50:34 -0800265 PersistentData::session_store->generate_user_session(*username);
Ed Tanous911ac312017-08-15 09:37:42 -0700266
Ed Tanousba9f9a62017-10-11 16:40:35 -0700267 if (looks_like_ibm) {
268 // IBM requires a very specific login structure, and doesn't
269 // actually look at the status code. TODO(ed).... Fix that
270 // upstream
Ed Tanousdb024a52018-03-06 12:50:34 -0800271 nlohmann::json ret{{"data", "User '" + *username + "' logged in"},
Ed Tanousba9f9a62017-10-11 16:40:35 -0700272 {"message", "200 OK"},
273 {"status", "ok"}};
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100274 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100275 res.add_header("Set-Cookie", "SESSION=" + session.session_token +
276 "; Secure; HttpOnly");
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100277
Ed Tanousba9f9a62017-10-11 16:40:35 -0700278 res.write(ret.dump());
279 } else {
280 // if content type is json, assume json token
281 nlohmann::json ret{{"token", session.session_token}};
282
283 res.write(ret.dump());
284 res.add_header("Content-Type", "application/json");
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