Rework Authorization flow

Currently we parse the whole message before authenticating,
allowing an attacker the ability to upload a large image,
or keep a connection open for the max amount of time easier
than it should be. This moves the authentication to the
earliest point possible, and restricts unauthenticated users
timeouts and max upload sizes. It also makes it so that
unauthenticated users cannot keep the connection alive
forever by refusing to close the connection.

Tested:
- login/logout
- firmware update
- large POST when unauthenticated
- timeouts when unauthenticated
- slowhttptest

Change-Id: Ifa02d8db04eac1821e8950eb85e71634a9e6d265
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/include/authorization.hpp b/include/authorization.hpp
new file mode 100644
index 0000000..9074377
--- /dev/null
+++ b/include/authorization.hpp
@@ -0,0 +1,267 @@
+#pragma once
+
+#include "webroutes.hpp"
+
+#include <app.h>
+#include <common.h>
+#include <http_request.h>
+#include <http_response.h>
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/container/flat_set.hpp>
+#include <http_utility.hpp>
+#include <pam_authenticate.hpp>
+#include <persistent_data_middleware.hpp>
+
+#include <random>
+
+namespace crow
+{
+
+namespace authorization
+{
+
+static void cleanupTempSession(Request& req)
+{
+    // 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 (req.session != nullptr &&
+        req.session->persistence ==
+            crow::persistent_data::PersistenceType::SINGLE_REQUEST)
+    {
+        persistent_data::SessionStore::getInstance().removeSession(req.session);
+    }
+}
+
+static const std::shared_ptr<crow::persistent_data::UserSession>
+    performBasicAuth(std::string_view auth_header)
+{
+    BMCWEB_LOG_DEBUG << "[AuthMiddleware] Basic authentication";
+
+    std::string authData;
+    std::string_view param = auth_header.substr(strlen("Basic "));
+    if (!crow::utility::base64Decode(param, authData))
+    {
+        return nullptr;
+    }
+    std::size_t separator = authData.find(':');
+    if (separator == std::string::npos)
+    {
+        return nullptr;
+    }
+
+    std::string user = authData.substr(0, separator);
+    separator += 1;
+    if (separator > authData.size())
+    {
+        return nullptr;
+    }
+    std::string pass = authData.substr(separator);
+
+    BMCWEB_LOG_DEBUG << "[AuthMiddleware] Authenticating user: " << user;
+
+    int pamrc = pamAuthenticateUser(user, pass);
+    bool isConfigureSelfOnly = pamrc == PAM_NEW_AUTHTOK_REQD;
+    if ((pamrc != PAM_SUCCESS) && !isConfigureSelfOnly)
+    {
+        return nullptr;
+    }
+
+    // TODO(ed) generateUserSession 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 persistent_data::SessionStore::getInstance().generateUserSession(
+        user, crow::persistent_data::PersistenceType::SINGLE_REQUEST,
+        isConfigureSelfOnly);
+}
+
+static const std::shared_ptr<crow::persistent_data::UserSession>
+    performTokenAuth(std::string_view auth_header)
+{
+    BMCWEB_LOG_DEBUG << "[AuthMiddleware] Token authentication";
+
+    std::string_view token = auth_header.substr(strlen("Token "));
+    auto session =
+        persistent_data::SessionStore::getInstance().loginSessionByToken(token);
+    return session;
+}
+
+static const std::shared_ptr<crow::persistent_data::UserSession>
+    performXtokenAuth(const crow::Request& req)
+{
+    BMCWEB_LOG_DEBUG << "[AuthMiddleware] X-Auth-Token authentication";
+
+    std::string_view token = req.getHeaderValue("X-Auth-Token");
+    if (token.empty())
+    {
+        return nullptr;
+    }
+    auto session =
+        persistent_data::SessionStore::getInstance().loginSessionByToken(token);
+    return session;
+}
+
+static const std::shared_ptr<crow::persistent_data::UserSession>
+    performCookieAuth(const crow::Request& req)
+{
+    BMCWEB_LOG_DEBUG << "[AuthMiddleware] Cookie authentication";
+
+    std::string_view cookieValue = req.getHeaderValue("Cookie");
+    if (cookieValue.empty())
+    {
+        return nullptr;
+    }
+
+    auto startIndex = cookieValue.find("SESSION=");
+    if (startIndex == std::string::npos)
+    {
+        return nullptr;
+    }
+    startIndex += sizeof("SESSION=") - 1;
+    auto endIndex = cookieValue.find(";", startIndex);
+    if (endIndex == std::string::npos)
+    {
+        endIndex = cookieValue.size();
+    }
+    std::string_view authKey =
+        cookieValue.substr(startIndex, endIndex - startIndex);
+
+    const std::shared_ptr<crow::persistent_data::UserSession> session =
+        persistent_data::SessionStore::getInstance().loginSessionByToken(
+            authKey);
+    if (session == nullptr)
+    {
+        return nullptr;
+    }
+#ifndef BMCWEB_INSECURE_DISABLE_CSRF_PREVENTION
+    // RFC7231 defines methods that need csrf protection
+    if (req.method() != "GET"_method)
+    {
+        std::string_view csrf = req.getHeaderValue("X-XSRF-TOKEN");
+        // Make sure both tokens are filled
+        if (csrf.empty() || session->csrfToken.empty())
+        {
+            return nullptr;
+        }
+
+        if (csrf.size() != crow::persistent_data::sessionTokenSize)
+        {
+            return nullptr;
+        }
+        // Reject if csrf token not available
+        if (!crow::utility::constantTimeStringCompare(csrf, session->csrfToken))
+        {
+            return nullptr;
+        }
+    }
+#endif
+    return session;
+}
+
+// checks if request can be forwarded without authentication
+static bool isOnWhitelist(const crow::Request& req)
+{
+    // it's allowed to GET root node without authentication
+    if ("GET"_method == req.method())
+    {
+        if (req.url == "/redfish/v1" || req.url == "/redfish/v1/" ||
+            req.url == "/redfish" || req.url == "/redfish/" ||
+            req.url == "/redfish/v1/odata" || req.url == "/redfish/v1/odata/")
+        {
+            return true;
+        }
+        else if (crow::webroutes::routes.find(std::string(req.url)) !=
+                 crow::webroutes::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 == "/redfish/v1/SessionService/Sessions/") ||
+            (req.url == "/login"))
+        {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+static void authenticate(crow::Request& req, Response& res)
+{
+    if (isOnWhitelist(req))
+    {
+        return;
+    }
+
+    const crow::persistent_data::AuthConfigMethods& authMethodsConfig =
+        crow::persistent_data::SessionStore::getInstance()
+            .getAuthMethodsConfig();
+
+    if (req.session == nullptr && authMethodsConfig.xtoken)
+    {
+        req.session = performXtokenAuth(req);
+    }
+    if (req.session == nullptr && authMethodsConfig.cookie)
+    {
+        req.session = performCookieAuth(req);
+    }
+    if (req.session == nullptr)
+    {
+        std::string_view authHeader = req.getHeaderValue("Authorization");
+        if (!authHeader.empty())
+        {
+            // Reject any kind of auth other than basic or token
+            if (boost::starts_with(authHeader, "Token ") &&
+                authMethodsConfig.sessionToken)
+            {
+                req.session = performTokenAuth(authHeader);
+            }
+            else if (boost::starts_with(authHeader, "Basic ") &&
+                     authMethodsConfig.basic)
+            {
+                req.session = performBasicAuth(authHeader);
+            }
+        }
+    }
+
+    if (req.session == nullptr)
+    {
+        BMCWEB_LOG_WARNING << "[AuthMiddleware] authorization failed";
+
+        // If it's a browser connecting, don't send the HTTP authenticate
+        // header, to avoid possible CSRF attacks with basic auth
+        if (http_helpers::requestPrefersHtml(req))
+        {
+            res.result(boost::beast::http::status::temporary_redirect);
+            res.addHeader("Location",
+                          "/#/login?next=" + http_helpers::urlEncode(req.url));
+        }
+        else
+        {
+            res.result(boost::beast::http::status::unauthorized);
+            // only send the WWW-authenticate header if this isn't a xhr
+            // from the browser.  most scripts,
+            if (req.getHeaderValue("User-Agent").empty())
+            {
+                res.addHeader("WWW-Authenticate", "Basic");
+            }
+        }
+
+        res.end();
+        return;
+    }
+}
+
+} // namespace authorization
+} // namespace crow