blob: 7497b43f9fe32179200c150d76e9320059f14cfe [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 }
202 std::string username;
203 std::string password;
204 bool looks_like_ibm = false;
205 // Check if auth was provided by a payload
206 if (content_type == "application/json") {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700207 auto login_credentials =
208 nlohmann::json::parse(req.body, nullptr, false);
209 if (login_credentials.is_discarded()) {
Ed Tanous911ac312017-08-15 09:37:42 -0700210 res.code = 400;
211 res.end();
212 return;
213 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700214 // check for username/password in the root object
215 // THis method is how intel APIs authenticate
216 auto user_it = login_credentials.find("username");
217 auto pass_it = login_credentials.find("password");
218 if (user_it != login_credentials.end() &&
219 pass_it != login_credentials.end()) {
220 username = user_it->get<const std::string>();
221 password = pass_it->get<const std::string>();
222 } else {
223 // Openbmc appears to push a data object that contains the same
224 // keys (username and password), attempt to use that
225 auto data_it = login_credentials.find("data");
226 if (data_it != login_credentials.end()) {
227 // Some apis produce an array of value ["username",
228 // "password"]
229 if (data_it->is_array()) {
230 if (data_it->size() == 2) {
231 username = (*data_it)[0].get<const std::string>();
232 password = (*data_it)[1].get<const std::string>();
233 looks_like_ibm = true;
234 }
235 } else if (data_it->is_object()) {
236 auto user_it = data_it->find("username");
237 auto pass_it = data_it->find("password");
238 if (user_it != data_it->end() && pass_it != data_it->end()) {
239 username = user_it->get<const std::string>();
240 password = pass_it->get<const std::string>();
241 }
242 }
243 }
244 }
Ed Tanous911ac312017-08-15 09:37:42 -0700245 } else {
246 // check if auth was provided as a query string
247 auto user_it = req.headers.find("username");
248 auto pass_it = req.headers.find("password");
249 if (user_it != req.headers.end() && pass_it != req.headers.end()) {
250 username = user_it->second;
251 password = pass_it->second;
252 }
253 }
254
255 if (!username.empty() && !password.empty()) {
256 if (!pam_authenticate_user(username, password)) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100257 res.code = res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
Ed Tanous911ac312017-08-15 09:37:42 -0700258 } else {
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100259 auto& session =
260 PersistentData::session_store->generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700261
Ed Tanousba9f9a62017-10-11 16:40:35 -0700262 if (looks_like_ibm) {
263 // IBM requires a very specific login structure, and doesn't
264 // actually look at the status code. TODO(ed).... Fix that
265 // upstream
266 nlohmann::json ret{{"data", "User '" + username + "' logged in"},
267 {"message", "200 OK"},
268 {"status", "ok"}};
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100269 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100270 res.add_header("Set-Cookie", "SESSION=" + session.session_token +
271 "; Secure; HttpOnly");
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100272
Ed Tanousba9f9a62017-10-11 16:40:35 -0700273 res.write(ret.dump());
274 } else {
275 // if content type is json, assume json token
276 nlohmann::json ret{{"token", session.session_token}};
277
278 res.write(ret.dump());
279 res.add_header("Content-Type", "application/json");
Ed Tanous911ac312017-08-15 09:37:42 -0700280 }
281 }
282
283 } else {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100284 res.code = static_cast<int>(HttpRespCode::BAD_REQUEST);
Ed Tanous911ac312017-08-15 09:37:42 -0700285 }
286 res.end();
287 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100288
289 CROW_ROUTE(app, "/logout")
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100290 .methods("POST"_method)(
291 [&](const crow::request& req, crow::response& res) {
292 auto& session =
293 app.template get_context<TokenAuthorization::Middleware>(req)
294 .session;
295 if (session != nullptr) {
296 PersistentData::session_store->remove_session(session);
297 }
298 res.code = static_cast<int>(HttpRespCode::OK);
299 res.end();
300 return;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100301
Kowalski, Kamil2b7981f2018-01-31 13:24:59 +0100302 });
Ed Tanous911ac312017-08-15 09:37:42 -0700303}
304} // namespaec TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700305} // namespace crow