Merge "Enable PATCH method in crow"
diff --git a/crow/include/crow/http_codes.h b/crow/include/crow/http_codes.h
new file mode 100644
index 0000000..9eff020
--- /dev/null
+++ b/crow/include/crow/http_codes.h
@@ -0,0 +1,14 @@
+#pragma once
+
+enum class HttpRespCode{
+ OK = 200,
+ CREATED = 201,
+ ACCEPTED = 202,
+ NO_CONTENT = 204,
+ BAD_REQUEST = 400,
+ UNAUTHORIZED = 401,
+ FORBIDDEN = 403,
+ NOT_FOUND = 404,
+ METHOD_NOT_ALLOWED = 405,
+ INTERNAL_ERROR = 500
+};
diff --git a/crow/include/crow/http_response.h b/crow/include/crow/http_response.h
index 83684cc..e90bfdc 100644
--- a/crow/include/crow/http_response.h
+++ b/crow/include/crow/http_response.h
@@ -9,6 +9,7 @@
namespace crow {
template <typename Adaptor, typename Handler, typename... Middlewares>
class Connection;
+
struct response {
template <typename Adaptor, typename Handler, typename... Middlewares>
friend class crow::Connection;
diff --git a/crow/include/crow/utility.h b/crow/include/crow/utility.h
index 6046909..e2e9c16 100644
--- a/crow/include/crow/utility.h
+++ b/crow/include/crow/utility.h
@@ -516,5 +516,90 @@
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
}
+// TODO this is temporary and should be deleted once base64 is refactored out of
+// crow
+inline bool base64_decode(const std::string& input, std::string& output) {
+ static const char nop = -1;
+ // See note on encoding_data[] in above function
+ static const char decoding_data[] = {
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, 62, nop,
+ nop, nop, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, nop, nop,
+ nop, nop, nop, nop, nop, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
+ 25, nop, nop, nop, nop, nop, nop, 26, 27, 28, 29, 30, 31, 32, 33,
+ 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
+ 49, 50, 51, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop, nop,
+ nop};
+
+ size_t input_length = input.size();
+
+ // allocate space for output string
+ output.clear();
+ output.reserve(((input_length + 2) / 3) * 4);
+
+ // for each 4-bytes sequence from the input, extract 4 6-bits sequences by
+ // droping first two bits
+ // and regenerate into 3 8-bits sequences
+
+ for (size_t i = 0; i < input_length; i++) {
+ char base64code0;
+ char base64code1;
+ char base64code2 = 0; // initialized to 0 to suppress warnings
+ char base64code3;
+
+ base64code0 = decoding_data[static_cast<int>(input[i])]; // NOLINT
+ if (base64code0 == nop) { // non base64 character
+ return false;
+ }
+ if (!(++i < input_length)) { // we need at least two input bytes for first
+ // byte output
+ return false;
+ }
+ base64code1 = decoding_data[static_cast<int>(input[i])]; // NOLINT
+ if (base64code1 == nop) { // non base64 character
+ return false;
+ }
+ output +=
+ static_cast<char>((base64code0 << 2) | ((base64code1 >> 4) & 0x3));
+
+ if (++i < input_length) {
+ char c = input[i];
+ if (c == '=') { // padding , end of input
+ return (base64code1 & 0x0f) == 0;
+ }
+ base64code2 = decoding_data[static_cast<int>(input[i])]; // NOLINT
+ if (base64code2 == nop) { // non base64 character
+ return false;
+ }
+ output += static_cast<char>(((base64code1 << 4) & 0xf0) |
+ ((base64code2 >> 2) & 0x0f));
+ }
+
+ if (++i < input_length) {
+ char c = input[i];
+ if (c == '=') { // padding , end of input
+ return (base64code2 & 0x03) == 0;
+ }
+ base64code3 = decoding_data[static_cast<int>(input[i])]; // NOLINT
+ if (base64code3 == nop) { // non base64 character
+ return false;
+ }
+ output += static_cast<char>((((base64code2 << 6) & 0xc0) | base64code3));
+ }
+ }
+
+ return true;
+}
+
} // namespace utility
} // namespace crow
diff --git a/include/persistent_data_middleware.hpp b/include/persistent_data_middleware.hpp
index aee2407..b52e225 100644
--- a/include/persistent_data_middleware.hpp
+++ b/include/persistent_data_middleware.hpp
@@ -15,19 +15,28 @@
namespace crow {
namespace PersistentData {
+
+enum class PersistenceType {
+ TIMEOUT, // User session times out after a predetermined amount of time
+ SINGLE_REQUEST // User times out once this request is completed.
+};
+
struct UserSession {
std::string unique_id;
std::string session_token;
std::string username;
std::string csrf_token;
std::chrono::time_point<std::chrono::steady_clock> last_updated;
+ PersistenceType persistence;
};
void to_json(nlohmann::json& j, const UserSession& p) {
- j = nlohmann::json{{"unique_id", p.unique_id},
- {"session_token", p.session_token},
- {"username", p.username},
- {"csrf_token", p.csrf_token}};
+ if (p.persistence != PersistenceType::SINGLE_REQUEST) {
+ j = nlohmann::json{{"unique_id", p.unique_id},
+ {"session_token", p.session_token},
+ {"username", p.username},
+ {"csrf_token", p.csrf_token}};
+ }
}
void from_json(const nlohmann::json& j, UserSession& p) {
@@ -51,7 +60,11 @@
class SessionStore {
public:
- const UserSession& generate_user_session(const std::string& username) {
+ const UserSession& generate_user_session(
+ const std::string& username,
+ PersistenceType persistence = PersistenceType::TIMEOUT) {
+ // TODO(ed) find a secure way to not generate session identifiers if
+ // persistence is set to SINGLE_REQUEST
static constexpr std::array<char, 62> alphanum = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C',
'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
@@ -59,7 +72,7 @@
'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};
- // entropy: 30 characters, 62 possibilies. log2(62^30) = 178 bits of
+ // entropy: 30 characters, 62 possibilities. log2(62^30) = 178 bits of
// entropy. OWASP recommends at least 60
// https://www.owasp.org/index.php/Session_Management_Cheat_Sheet#Session_ID_Entropy
std::string session_token;
@@ -80,12 +93,14 @@
for (int i = 0; i < unique_id.size(); ++i) {
unique_id[i] = alphanum[dist(rd)];
}
+
const auto session_it = auth_tokens.emplace(
session_token,
std::move(UserSession{unique_id, session_token, username, csrf_token,
- std::chrono::steady_clock::now()}));
+ std::chrono::steady_clock::now(), persistence}));
const UserSession& user = (session_it).first->second;
- need_write_ = true;
+ // Only need to write to disk if session isn't about to be destroyed.
+ need_write_ = persistence == PersistenceType::TIMEOUT;
return user;
}
diff --git a/include/token_authorization_middleware.hpp b/include/token_authorization_middleware.hpp
index 81d9e58..bbbaa15 100644
--- a/include/token_authorization_middleware.hpp
+++ b/include/token_authorization_middleware.hpp
@@ -5,119 +5,190 @@
#include <webassets.hpp>
#include <random>
#include <crow/app.h>
+#include <crow/http_codes.h>
#include <crow/http_request.h>
#include <crow/http_response.h>
#include <boost/bimap.hpp>
#include <boost/container/flat_set.hpp>
+
namespace crow {
namespace TokenAuthorization {
-struct User {};
class Middleware {
public:
- struct context {};
+ struct context {
+ const crow::PersistentData::UserSession* session;
+ };
template <typename AllContext>
void before_handle(crow::request& req, response& res, context& ctx,
AllContext& allctx) {
- auto return_unauthorized = [&req, &res]() {
- res.code = 401;
+ auto& sessions =
+ allctx.template get<crow::PersistentData::Middleware>().sessions;
+ std::string auth_header = req.get_header_value("Authorization");
+ if (auth_header != "") {
+ // Reject any kind of auth other than basic or token
+ if (boost::starts_with(auth_header, "Basic ")) {
+ ctx.session = perform_basic_auth(auth_header, sessions);
+ } else if (boost::starts_with(auth_header, "Token ")) {
+ ctx.session = perform_token_auth(auth_header, sessions);
+ }
+ } else if (req.headers.count("X-Auth-Token") == 1) {
+ ctx.session = perform_xtoken_auth(req, sessions);
+ } else if (req.headers.count("Cookie") == 1) {
+ ctx.session = perform_cookie_auth(req, sessions);
+ }
+
+ if (ctx.session == nullptr && !is_on_whitelist(req)) {
+ CROW_LOG_WARNING << "[AuthMiddleware] authorization failed";
+ res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
+ res.add_header("WWW-Authenticate", "Basic");
res.end();
- };
+ return;
+ }
- if (crow::webassets::routes.find(req.url) !=
- crow::webassets::routes.end()) {
- // TODO this is total hackery to allow the login page to work before the
- // user is authenticated. Also, it will be quite slow for all pages
- // instead of a one time hit for the whitelist entries. Ideally, this
- // should be done in the url router handler, with tagged routes for the
- // whitelist entries. Another option would be to whitelist a minimal form
- // based page that didn't load the full angular UI until after login
- } else if (req.url == "/login" || req.url == "/redfish/v1" ||
- req.url == "/redfish/v1/" ||
- req.url == "/redfish/v1/$metadata" ||
- (req.url == "/redfish/v1/SessionService/Sessions/" &&
- req.method == "POST"_method)) {
- } else {
- // Normal, non login, non static file request
- // Check for an authorization header, reject if not present
- std::string auth_key;
- bool require_csrf = true;
- if (req.headers.count("Authorization") == 1) {
- std::string auth_header = req.get_header_value("Authorization");
- // If the user is attempting any kind of auth other than token, reject
- if (!boost::starts_with(auth_header, "Token ")) {
- return_unauthorized();
- return;
- }
- auth_key = auth_header.substr(6);
- require_csrf = false;
- } else if (req.headers.count("X-Auth-Token") == 1) {
- auth_key = req.get_header_value("X-Auth-Token");
- require_csrf = false;
- } else {
- int count = req.headers.count("Cookie");
- if (count == 1) {
- auto& cookie_value = req.get_header_value("Cookie");
- auto start_index = cookie_value.find("SESSION=");
- if (start_index != std::string::npos) {
- start_index += 8;
- auto end_index = cookie_value.find(";", start_index);
- if (end_index == std::string::npos) {
- end_index = cookie_value.size();
- }
- auth_key =
- cookie_value.substr(start_index, end_index - start_index);
- }
- }
- require_csrf = true; // Cookies require CSRF
- }
- if (auth_key.empty()) {
- res.code = 400;
- res.end();
- return;
- }
- auto& data_mw = allctx.template get<PersistentData::Middleware>();
- const PersistentData::UserSession* session =
- data_mw.sessions->login_session_by_token(auth_key);
- if (session == nullptr) {
- return_unauthorized();
- return;
- }
+ // TODO get user privileges here and propagate it via MW context
+ // else let the request continue unharmed
+ }
- if (require_csrf) {
- // RFC7231 defines methods that need csrf protection
- if (req.method != "GET"_method) {
- const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
- // Make sure both tokens are filled
- if (csrf.empty() || session->csrf_token.empty()) {
- return_unauthorized();
- return;
- }
- // Reject if csrf token not available
- if (csrf != session->csrf_token) {
- return_unauthorized();
- return;
- }
- }
- }
+ template <typename AllContext>
+ void after_handle(request& req, response& res, context& ctx,
+ AllContext& allctx) {
+ // TODO(ed) THis should really be handled by the persistent data middleware,
+ // but because it is upstream, it doesn't have access to the session
+ // information. Should the data middleware persist the current user
+ // session?
+ if (ctx.session != nullptr &&
+ ctx.session->persistence ==
+ crow::PersistentData::PersistenceType::SINGLE_REQUEST) {
+ auto& session_store =
+ allctx.template get<crow::PersistentData::Middleware>().sessions;
- if (req.url == "/logout" && req.method == "POST"_method) {
- data_mw.sessions->remove_session(session);
- res.code = 200;
- res.end();
- return;
- }
-
- // else let the request continue unharmed
+ session_store->remove_session(ctx.session);
}
}
- void after_handle(request& req, response& res, context& ctx) {
- // Do nothing
+ private:
+ const crow::PersistentData::UserSession* perform_basic_auth(
+ const std::string& auth_header,
+ crow::PersistentData::SessionStore* sessions) const {
+ CROW_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
+
+ std::string auth_data;
+ std::string param = auth_header.substr(strlen("Basic "));
+ if (!crow::utility::base64_decode(param, auth_data)) {
+ return nullptr;
+ }
+ std::size_t separator = auth_data.find(':');
+ if (separator == std::string::npos) {
+ return nullptr;
+ }
+
+ std::string user = auth_data.substr(0, separator);
+ separator += 1;
+ if (separator > auth_data.size()) {
+ return nullptr;
+ }
+ std::string pass = auth_data.substr(separator);
+
+ CROW_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
+
+ if (!pam_authenticate_user(user, pass)) {
+ return nullptr;
+ }
+
+ // TODO(ed) generate_user_session is a little expensive for basic
+ // auth, as it generates some random identifiers that will never be
+ // used. This should have a "fast" path for when user tokens aren't
+ // needed.
+ // This whole flow needs to be revisited anyway, as we can't be
+ // calling directly into pam for every request
+ return &(sessions->generate_user_session(
+ user, crow::PersistentData::PersistenceType::SINGLE_REQUEST));
}
- boost::container::flat_set<std::string> allowed_routes;
+ const crow::PersistentData::UserSession* perform_token_auth(
+ const std::string& auth_header,
+ crow::PersistentData::SessionStore* sessions) const {
+ CROW_LOG_DEBUG << "[AuthMiddleware] Token authentication";
+
+ std::string token = auth_header.substr(strlen("Token "));
+ auto session = sessions->login_session_by_token(token);
+ return session;
+ }
+
+ const crow::PersistentData::UserSession* perform_xtoken_auth(
+ const crow::request& req,
+ crow::PersistentData::SessionStore* sessions) const {
+ CROW_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
+
+ auto& token = req.get_header_value("X-Auth-Token");
+ auto session = sessions->login_session_by_token(token);
+ return session;
+ }
+
+ const crow::PersistentData::UserSession* perform_cookie_auth(
+ const crow::request& req,
+ crow::PersistentData::SessionStore* sessions) const {
+ CROW_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
+
+ auto& cookie_value = req.get_header_value("Cookie");
+
+ auto start_index = cookie_value.find("SESSION=");
+ if (start_index == std::string::npos) {
+ return nullptr;
+ }
+ start_index += sizeof("SESSION=");
+ auto end_index = cookie_value.find(";", start_index);
+ if (end_index == std::string::npos) {
+ end_index = cookie_value.size();
+ }
+ std::string auth_key =
+ cookie_value.substr(start_index, end_index - start_index);
+
+ const crow::PersistentData::UserSession* session =
+ sessions->login_session_by_token(auth_key);
+ if (session == nullptr) {
+ return nullptr;
+ }
+
+ // RFC7231 defines methods that need csrf protection
+ if (req.method != "GET"_method) {
+ const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
+ // Make sure both tokens are filled
+ if (csrf.empty() || session->csrf_token.empty()) {
+ return nullptr;
+ }
+ // Reject if csrf token not available
+ if (csrf != session->csrf_token) {
+ return nullptr;
+ }
+ }
+ return session;
+ }
+
+ // checks if request can be forwarded without authentication
+ bool is_on_whitelist(const crow::request& req) const {
+ // it's allowed to GET root node without authentication
+ if ("GET"_method == req.method) {
+ if (req.url == "/redfish/v1") {
+ return true;
+ } else if (crow::webassets::routes.find(req.url) !=
+ crow::webassets::routes.end()) {
+ return true;
+ }
+ }
+
+ // it's allowed to POST on session collection & login without authentication
+ if ("POST"_method == req.method) {
+ if ((req.url == "/redfish/v1/SessionService/Sessions") ||
+ (req.url == "/login") || (req.url == "/logout")) {
+ return true;
+ }
+ }
+
+ return false;
+ }
};
// TODO(ed) see if there is a better way to allow middlewares to request
@@ -193,7 +264,7 @@
if (!username.empty() && !password.empty()) {
if (!pam_authenticate_user(username, password)) {
- res.code = 401;
+ res.code = res.code = static_cast<int>(HttpRespCode::UNAUTHORIZED);
} else {
auto& context =
app.template get_context<PersistentData::Middleware>(req);
@@ -207,10 +278,11 @@
nlohmann::json ret{{"data", "User '" + username + "' logged in"},
{"message", "200 OK"},
{"status", "ok"}};
+ res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
res.add_header(
"Set-Cookie",
"SESSION=" + session.session_token + "; Secure; HttpOnly");
- res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
+
res.write(ret.dump());
} else {
// if content type is json, assume json token
@@ -222,10 +294,27 @@
}
} else {
- res.code = 400;
+ res.code = static_cast<int>(HttpRespCode::BAD_REQUEST);
}
res.end();
});
+
+ CROW_ROUTE(app, "/logout")
+ .methods(
+ "POST"_method)([&](const crow::request& req, crow::response& res) {
+ auto& session_store =
+ app.template get_context<PersistentData::Middleware>(req).sessions;
+ auto& session =
+ app.template get_context<TokenAuthorization::Middleware>(req)
+ .session;
+ if (session != nullptr) {
+ session_store->remove_session(session);
+ }
+ res.code = static_cast<int>(HttpRespCode::OK);
+ res.end();
+ return;
+
+ });
}
} // namespaec TokenAuthorization
} // namespace crow