blob: 809902022fe5ef1969859c05d9b527f44859d759 [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 };
Ed Tanousba9f9a62017-10-11 16:40:35 -070023 template <typename AllContext>
24 void before_handle(crow::request& req, response& res, context& ctx,
25 AllContext& allctx) {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010026 auto& sessions =
27 allctx.template get<crow::PersistentData::Middleware>().sessions;
28 std::string auth_header = req.get_header_value("Authorization");
29 if (auth_header != "") {
30 // Reject any kind of auth other than basic or token
31 if (boost::starts_with(auth_header, "Basic ")) {
32 ctx.session = perform_basic_auth(auth_header, sessions);
33 } else if (boost::starts_with(auth_header, "Token ")) {
34 ctx.session = perform_token_auth(auth_header, sessions);
35 }
36 } else if (req.headers.count("X-Auth-Token") == 1) {
37 ctx.session = perform_xtoken_auth(req, sessions);
38 } else if (req.headers.count("Cookie") == 1) {
39 ctx.session = perform_cookie_auth(req, sessions);
40 }
41
42 if (ctx.session == nullptr && !is_on_whitelist(req)) {
43 CROW_LOG_WARNING << "[AuthMiddleware] authorization failed";
44 res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
45 res.add_header("WWW-Authenticate", "Basic");
Ed Tanousf3d847c2017-06-12 16:01:42 -070046 res.end();
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010047 return;
48 }
Ed Tanousf9273472017-02-28 16:05:13 -080049
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010050 // TODO get user privileges here and propagate it via MW context
51 // else let the request continue unharmed
52 }
Ed Tanousf3d847c2017-06-12 16:01:42 -070053
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010054 template <typename AllContext>
55 void after_handle(request& req, response& res, context& ctx,
56 AllContext& allctx) {
57 // TODO(ed) THis should really be handled by the persistent data middleware,
58 // but because it is upstream, it doesn't have access to the session
59 // information. Should the data middleware persist the current user
60 // session?
61 if (ctx.session != nullptr &&
62 ctx.session->persistence ==
63 crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
64 auto& session_store =
65 allctx.template get<crow::PersistentData::Middleware>().sessions;
Ed Tanousba9f9a62017-10-11 16:40:35 -070066
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +010067 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(
73 const std::string& auth_header,
74 crow::PersistentData::SessionStore* sessions) const {
75 CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
76
77 std::string auth_data;
78 std::string param = auth_header.substr(strlen("Basic "));
79 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
106 return &(sessions->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
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100110 const crow::PersistentData::UserSession* perform_token_auth(
111 const std::string& auth_header,
112 crow::PersistentData::SessionStore* sessions) const {
113 CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication";
114
115 std::string token = auth_header.substr(strlen("Token "));
116 auto session = sessions->login_session_by_token(token);
117 return session;
118 }
119
120 const crow::PersistentData::UserSession* perform_xtoken_auth(
121 const crow::request& req,
122 crow::PersistentData::SessionStore* sessions) const {
123 CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
124
125 auto& token = req.get_header_value("X-Auth-Token");
126 auto session = sessions->login_session_by_token(token);
127 return session;
128 }
129
130 const crow::PersistentData::UserSession* perform_cookie_auth(
131 const crow::request& req,
132 crow::PersistentData::SessionStore* sessions) const {
133 CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
134
135 auto& cookie_value = req.get_header_value("Cookie");
136
137 auto start_index = cookie_value.find("SESSION=");
138 if (start_index == std::string::npos) {
139 return nullptr;
140 }
Ed Tanous41ff64d2018-01-30 13:13:38 -0800141 start_index += sizeof("SESSION=") - 1;
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100142 auto end_index = cookie_value.find(";", start_index);
143 if (end_index == std::string::npos) {
144 end_index = cookie_value.size();
145 }
146 std::string auth_key =
147 cookie_value.substr(start_index, end_index - start_index);
148
149 const crow::PersistentData::UserSession* session =
150 sessions->login_session_by_token(auth_key);
151 if (session == nullptr) {
152 return nullptr;
153 }
154
155 // RFC7231 defines methods that need csrf protection
156 if (req.method != "GET"_method) {
157 const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
158 // Make sure both tokens are filled
159 if (csrf.empty() || session->csrf_token.empty()) {
160 return nullptr;
161 }
162 // Reject if csrf token not available
163 if (csrf != session->csrf_token) {
164 return nullptr;
165 }
166 }
167 return session;
168 }
169
170 // checks if request can be forwarded without authentication
171 bool is_on_whitelist(const crow::request& req) const {
172 // it's allowed to GET root node without authentication
173 if ("GET"_method == req.method) {
174 if (req.url == "/redfish/v1") {
175 return true;
176 } else if (crow::webassets::routes.find(req.url) !=
177 crow::webassets::routes.end()) {
178 return true;
179 }
180 }
181
182 // it's allowed to POST on session collection & login without authentication
183 if ("POST"_method == req.method) {
184 if ((req.url == "/redfish/v1/SessionService/Sessions") ||
185 (req.url == "/login") || (req.url == "/logout")) {
186 return true;
187 }
188 }
189
190 return false;
191 }
Ed Tanous99923322017-03-03 14:21:24 -0800192};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700193
Ed Tanousba9f9a62017-10-11 16:40:35 -0700194// TODO(ed) see if there is a better way to allow middlewares to request
195// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700196// Possibly an init function on first construction?
197template <typename... Middlewares>
198void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700199 static_assert(
200 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
201 "TokenAuthorization middleware must be enabled in app to use "
202 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700203 CROW_ROUTE(app, "/login")
204 .methods(
205 "POST"_method)([&](const crow::request& req, crow::response& res) {
206 std::string content_type;
207 auto content_type_it = req.headers.find("content-type");
208 if (content_type_it != req.headers.end()) {
209 content_type = content_type_it->second;
210 boost::algorithm::to_lower(content_type);
211 }
212 std::string username;
213 std::string password;
214 bool looks_like_ibm = false;
215 // Check if auth was provided by a payload
216 if (content_type == "application/json") {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700217 auto login_credentials =
218 nlohmann::json::parse(req.body, nullptr, false);
219 if (login_credentials.is_discarded()) {
Ed Tanous911ac312017-08-15 09:37:42 -0700220 res.code = 400;
221 res.end();
222 return;
223 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700224 // check for username/password in the root object
225 // THis method is how intel APIs authenticate
226 auto user_it = login_credentials.find("username");
227 auto pass_it = login_credentials.find("password");
228 if (user_it != login_credentials.end() &&
229 pass_it != login_credentials.end()) {
230 username = user_it->get<const std::string>();
231 password = pass_it->get<const std::string>();
232 } else {
233 // Openbmc appears to push a data object that contains the same
234 // keys (username and password), attempt to use that
235 auto data_it = login_credentials.find("data");
236 if (data_it != login_credentials.end()) {
237 // Some apis produce an array of value ["username",
238 // "password"]
239 if (data_it->is_array()) {
240 if (data_it->size() == 2) {
241 username = (*data_it)[0].get<const std::string>();
242 password = (*data_it)[1].get<const std::string>();
243 looks_like_ibm = true;
244 }
245 } else if (data_it->is_object()) {
246 auto user_it = data_it->find("username");
247 auto pass_it = data_it->find("password");
248 if (user_it != data_it->end() && pass_it != data_it->end()) {
249 username = user_it->get<const std::string>();
250 password = pass_it->get<const std::string>();
251 }
252 }
253 }
254 }
Ed Tanous911ac312017-08-15 09:37:42 -0700255 } else {
256 // check if auth was provided as a query string
257 auto user_it = req.headers.find("username");
258 auto pass_it = req.headers.find("password");
259 if (user_it != req.headers.end() && pass_it != req.headers.end()) {
260 username = user_it->second;
261 password = pass_it->second;
262 }
263 }
264
265 if (!username.empty() && !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 {
Ed Tanousc963aa42017-10-27 16:00:19 -0700269 auto& context =
270 app.template get_context<PersistentData::Middleware>(req);
271 auto& session_store = context.sessions;
272 auto& session = session_store->generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700273
Ed Tanousba9f9a62017-10-11 16:40:35 -0700274 if (looks_like_ibm) {
275 // IBM requires a very specific login structure, and doesn't
276 // actually look at the status code. TODO(ed).... Fix that
277 // upstream
278 nlohmann::json ret{{"data", "User '" + username + "' logged in"},
279 {"message", "200 OK"},
280 {"status", "ok"}};
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100281 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
Ed Tanousba9f9a62017-10-11 16:40:35 -0700282 res.add_header(
283 "Set-Cookie",
284 "SESSION=" + session.session_token + "; Secure; HttpOnly");
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100285
Ed Tanousba9f9a62017-10-11 16:40:35 -0700286 res.write(ret.dump());
287 } else {
288 // if content type is json, assume json token
289 nlohmann::json ret{{"token", session.session_token}};
290
291 res.write(ret.dump());
292 res.add_header("Content-Type", "application/json");
Ed Tanous911ac312017-08-15 09:37:42 -0700293 }
294 }
295
296 } else {
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100297 res.code = static_cast<int>(HttpRespCode::BAD_REQUEST);
Ed Tanous911ac312017-08-15 09:37:42 -0700298 }
299 res.end();
300 });
Borawski.Lukasz9d8fd302018-01-05 14:56:09 +0100301
302 CROW_ROUTE(app, "/logout")
303 .methods(
304 "POST"_method)([&](const crow::request& req, crow::response& res) {
305 auto& session_store =
306 app.template get_context<PersistentData::Middleware>(req).sessions;
307 auto& session =
308 app.template get_context<TokenAuthorization::Middleware>(req)
309 .session;
310 if (session != nullptr) {
311 session_store->remove_session(session);
312 }
313 res.code = static_cast<int>(HttpRespCode::OK);
314 res.end();
315 return;
316
317 });
Ed Tanous911ac312017-08-15 09:37:42 -0700318}
319} // namespaec TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700320} // namespace crow