From 911ac31759cb7b77a856af8806b4e064d50d7422 Mon Sep 17 00:00:00 2001 From: Ed Tanous Date: Tue, 15 Aug 2017 09:37:42 -0700 Subject: Large updates to webserver Do not merge yet Change-Id: I38c56844c1b0e3e8e5493c2705e62e6db7ee2102 --- include/token_authorization_middleware.hpp | 265 +++++++++++++++++++---------- 1 file changed, 174 insertions(+), 91 deletions(-) (limited to 'include/token_authorization_middleware.hpp') diff --git a/include/token_authorization_middleware.hpp b/include/token_authorization_middleware.hpp index 2d1edcda16..eebd4f0c3d 100644 --- a/include/token_authorization_middleware.hpp +++ b/include/token_authorization_middleware.hpp @@ -1,32 +1,32 @@ #pragma once +#include +#include +#include +#include +#include #include #include #include -#include - -#include - namespace crow { +namespace TokenAuthorization { struct User {}; using random_bytes_engine = - std::independent_bits_engine; - -template -struct TokenAuthorization { - private: - random_bytes_engine rbe; + std::independent_bits_engine; +class Middleware { public: - struct context { - // std::string auth_token; + Middleware() { + for (auto& route : crow::webassets::routes) { + allowed_routes.emplace(route); + } + allowed_routes.emplace("/login"); }; - TokenAuthorization(){}; + struct context {}; void before_handle(crow::request& req, response& res, context& ctx) { auto return_unauthorized = [&req, &res]() { @@ -34,100 +34,64 @@ struct TokenAuthorization { res.end(); }; - auto return_bad_request = [&req, &res]() { - res.code = 400; - res.end(); - }; - - auto return_internal_error = [&req, &res]() { - res.code = 500; - res.end(); - }; - - if (req.url == "/" || boost::starts_with(req.url, "/static/")) { + if (allowed_routes.find(req.url.c_str()) != allowed_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 for based page - // that didn't load the full angular UI until after login - return; - } - - if (req.url == "/login") { - if (req.method != HTTPMethod::POST) { - return_unauthorized(); - return; - } else { - std::string username; - std::string password; - try { - auto login_credentials = nlohmann::json::parse(req.body); - username = login_credentials["username"]; - password = login_credentials["password"]; - } catch (...) { - return_bad_request(); - return; - } - - auto p = AuthenticationFunction(); - if (p.authenticate(username, password)) { - nlohmann::json x; - - std::string token('a', 20); - // TODO(ed) for some reason clang-tidy finds a divide by zero error in - // cstdlibc here commented out for now. Needs investigation - std::generate(std::begin(token), std::end(token), - std::ref(rbe)); // NOLINT - std::string encoded_token; - base64::base64_encode(token, encoded_token); - // ctx.auth_token = encoded_token; - this->auth_token2.insert(encoded_token); - - nlohmann::json ret{{"token", encoded_token}}; - - res.write(ret.dump()); - res.add_header("Content-Type", "application/json"); - res.end(); - } else { + // 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 { + // Normal, non login, non static file request + // Check for an authorization header, reject if not present + std::string auth_key; + 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); + } 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() - 1; + } + auth_key = + cookie_value.substr(start_index, end_index - start_index + 1); + } + } } - - } else { // Normal, non login, non static file request - // Check to make sure we're logged in - if (this->auth_token2.empty()) { - return_unauthorized(); - return; - } - // Check for an authorization header, reject if not present - if (req.headers.count("Authorization") != 1) { - return_unauthorized(); + if (auth_key.empty()) { + res.code = 400; + res.end(); return; } + std::cout << "auth_key=" << auth_key << "\n"; - 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; + for (auto& token : this->auth_tokens) { + std::cout << "token=" << token << "\n"; } - std::string auth_key = auth_header.substr(6); + // TODO(ed), use span here instead of constructing a new string - if (this->auth_token2.find(auth_key) == this->auth_token2.end()) { + if (this->auth_tokens.find(auth_key) == this->auth_tokens.end()) { return_unauthorized(); return; } if (req.url == "/logout") { - this->auth_token2.erase(auth_key); + this->auth_tokens.erase(auth_key); res.code = 200; res.end(); return; } - // else let the request continue unharmed } } @@ -136,9 +100,128 @@ struct TokenAuthorization { // Do nothing } - private: - boost::container::flat_set auth_token2; + boost::container::flat_set auth_tokens; + boost::container::flat_set allowed_routes; + random_bytes_engine rbe; }; -using TokenAuthorizationMiddleware = TokenAuthorization; -} \ No newline at end of file +// TODO(ed) see if there is a better way to allow middlewares to request routes. +// Possibly an init function on first construction? +template +void request_routes(Crow& app) { + static_assert(black_magic::contains::value, + "TokenAuthorization middleware must be enabled in app to use " + "auth routes"); + CROW_ROUTE(app, "/login") + .methods( + "POST"_method)([&](const crow::request& req, crow::response& res) { + std::string content_type; + auto content_type_it = req.headers.find("content-type"); + if (content_type_it != req.headers.end()) { + content_type = content_type_it->second; + boost::algorithm::to_lower(content_type); + } + std::string username; + std::string password; + bool looks_like_ibm = false; + // Check if auth was provided by a payload + if (content_type == "application/json") { + try { + auto login_credentials = nlohmann::json::parse(req.body); + // check for username/password in the root object + // THis method is how intel APIs authenticate + auto user_it = login_credentials.find("username"); + auto pass_it = login_credentials.find("password"); + if (user_it != login_credentials.end() && + pass_it != login_credentials.end()) { + username = user_it->get(); + password = pass_it->get(); + } else { + // Openbmc appears to push a data object that contains the same + // keys (username and password), attempt to use that + auto data_it = login_credentials.find("data"); + if (data_it != login_credentials.end()) { + // Some apis produce an array of value ["username", "password"] + if (data_it->is_array()) { + if (data_it->size() == 2) { + username = (*data_it)[0].get(); + password = (*data_it)[1].get(); + looks_like_ibm = true; + } + } else if (data_it->is_object()) { + auto user_it = data_it->find("username"); + auto pass_it = data_it->find("password"); + if (user_it != data_it->end() && pass_it != data_it->end()) { + username = user_it->get(); + password = pass_it->get(); + } + } + } + } + } catch (...) { + // TODO(ed) figure out how to not throw on a bad json parse + res.code = 400; + res.end(); + return; + } + } else { + // check if auth was provided as a query string + auto user_it = req.headers.find("username"); + auto pass_it = req.headers.find("password"); + if (user_it != req.headers.end() && pass_it != req.headers.end()) { + username = user_it->second; + password = pass_it->second; + } + } + + if (!username.empty() && !password.empty()) { + if (!pam_authenticate_user(username, password)) { + res.code = 401; + } else { + // THis should be a multiple of 3 to make sure that base64 doesn't + // end with an equals sign at the end. we could strip it off + // afterward + std::string token(30, 'a'); + // TODO(ed) for some reason clang-tidy finds a divide by zero + // error in cstdlibc here commented out for now. + // Needs investigation + auto& m = app.template get_middleware(); + std::generate(std::begin(token), std::end(token), + std::ref(m.rbe)); // NOLINT + std::string encoded_token; + if (!base64::base64_encode(token, encoded_token)) { + res.code = 500; + } else { + + m.auth_tokens.insert(encoded_token); + if (looks_like_ibm) { + // IBM requires a very specific login structure, and doesn't + // actually look at the status code. TODO(ed).... Fix that + // upstream + nlohmann::json ret{ + {"data", "User '" + username + "' logged in"}, + {"message", "200 OK"}, + {"status", "ok"}}; + res.add_header( + "Set-Cookie", + "SESSION=" + encoded_token + "; Secure; HttpOnly"); + res.write(ret.dump()); + } else { + // if content type is json, assume json token + nlohmann::json ret{{"token", encoded_token}}; + + res.write(ret.dump()); + res.add_header("Content-Type", "application/json"); + } + } + } + + } else { + res.code = 400; + } + res.end(); + }); +} +} // namespaec TokenAuthorization +} // namespace crow \ No newline at end of file -- cgit v1.2.3