blob: 67f1161cca01c3dba5b0f6f73c3f53e19eb32e83 [file] [log] [blame]
Ed Tanousf9273472017-02-28 16:05:13 -08001#pragma once
2
Ed Tanous911ac312017-08-15 09:37:42 -07003#include <base64.hpp>
4#include <pam_authenticate.hpp>
Ed Tanousba9f9a62017-10-11 16:40:35 -07005#include <persistent_data_middleware.hpp>
Ed Tanous911ac312017-08-15 09:37:42 -07006#include <webassets.hpp>
7#include <random>
8#include <crow/app.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>
Ed Tanous99923322017-03-03 14:21:24 -080013namespace crow {
Ed Tanousb4d29f42017-03-24 16:39:25 -070014
Ed Tanous911ac312017-08-15 09:37:42 -070015namespace TokenAuthorization {
Ed Tanous1ff48782017-04-18 12:45:08 -070016struct User {};
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:
Ed Tanous911ac312017-08-15 09:37:42 -070020 struct context {};
Ed Tanousba9f9a62017-10-11 16:40:35 -070021 template <typename AllContext>
22 void before_handle(crow::request& req, response& res, context& ctx,
23 AllContext& allctx) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070024 auto return_unauthorized = [&req, &res]() {
25 res.code = 401;
26 res.end();
27 };
Ed Tanousf9273472017-02-28 16:05:13 -080028
Ed Tanousba9f9a62017-10-11 16:40:35 -070029 if (crow::webassets::routes.find(req.url) !=
30 crow::webassets::routes.end()) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070031 // TODO this is total hackery to allow the login page to work before the
32 // user is authenticated. Also, it will be quite slow for all pages
Ed Tanous3dac7492017-08-02 13:46:20 -070033 // instead of a one time hit for the whitelist entries. Ideally, this
Ed Tanous911ac312017-08-15 09:37:42 -070034 // should be done in the url router handler, with tagged routes for the
35 // whitelist entries. Another option would be to whitelist a minimal form
36 // based page that didn't load the full angular UI until after login
Ed Tanousba9f9a62017-10-11 16:40:35 -070037 } else if (req.url == "/login" || req.url == "/redfish/v1" ||
38 req.url == "/redfish/v1/" ||
39 req.url == "/redfish/v1/$metadata" ||
40 (req.url == "/redfish/v1/SessionService/Sessions/" &&
41 req.method == "POST"_method)) {
Ed Tanous911ac312017-08-15 09:37:42 -070042 } else {
43 // Normal, non login, non static file request
44 // Check for an authorization header, reject if not present
45 std::string auth_key;
Ed Tanousba9f9a62017-10-11 16:40:35 -070046 bool require_csrf = true;
Ed Tanous911ac312017-08-15 09:37:42 -070047 if (req.headers.count("Authorization") == 1) {
48 std::string auth_header = req.get_header_value("Authorization");
49 // If the user is attempting any kind of auth other than token, reject
50 if (!boost::starts_with(auth_header, "Token ")) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070051 return_unauthorized();
52 return;
53 }
Ed Tanous911ac312017-08-15 09:37:42 -070054 auth_key = auth_header.substr(6);
Ed Tanousba9f9a62017-10-11 16:40:35 -070055 require_csrf = false;
56 } else if (req.headers.count("X-Auth-Token") == 1) {
57 auth_key = req.get_header_value("X-Auth-Token");
58 require_csrf = false;
Ed Tanous911ac312017-08-15 09:37:42 -070059 } else {
60 int count = req.headers.count("Cookie");
61 if (count == 1) {
62 auto& cookie_value = req.get_header_value("Cookie");
63 auto start_index = cookie_value.find("SESSION=");
64 if (start_index != std::string::npos) {
65 start_index += 8;
66 auto end_index = cookie_value.find(";", start_index);
67 if (end_index == std::string::npos) {
Ed Tanousba9f9a62017-10-11 16:40:35 -070068 end_index = cookie_value.size();
Ed Tanous911ac312017-08-15 09:37:42 -070069 }
70 auth_key =
Ed Tanousba9f9a62017-10-11 16:40:35 -070071 cookie_value.substr(start_index, end_index - start_index);
Ed Tanous911ac312017-08-15 09:37:42 -070072 }
73 }
Ed Tanousba9f9a62017-10-11 16:40:35 -070074 require_csrf = true; // Cookies require CSRF
Ed Tanous911ac312017-08-15 09:37:42 -070075 }
76 if (auth_key.empty()) {
77 res.code = 400;
78 res.end();
79 return;
80 }
Ed Tanousba9f9a62017-10-11 16:40:35 -070081 auto& data_mw = allctx.template get<PersistentData::Middleware>();
Ed Tanousc963aa42017-10-27 16:00:19 -070082 const PersistentData::UserSession* session =
83 data_mw.sessions->login_session_by_token(auth_key);
84 if (session == nullptr) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070085 return_unauthorized();
86 return;
87 }
88
Ed Tanousba9f9a62017-10-11 16:40:35 -070089 if (require_csrf) {
90 // RFC7231 defines methods that need csrf protection
91 if (req.method != "GET"_method) {
92 const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
93 // Make sure both tokens are filled
Ed Tanousc963aa42017-10-27 16:00:19 -070094 if (csrf.empty() || session->csrf_token.empty()) {
Ed Tanousba9f9a62017-10-11 16:40:35 -070095 return_unauthorized();
96 return;
97 }
98 // Reject if csrf token not available
Ed Tanousc963aa42017-10-27 16:00:19 -070099 if (csrf != session->csrf_token) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700100 return_unauthorized();
101 return;
102 }
103 }
104 }
105
106 if (req.url == "/logout" && req.method == "POST"_method) {
Ed Tanousc963aa42017-10-27 16:00:19 -0700107 data_mw.sessions->remove_session(session);
Ed Tanousf3d847c2017-06-12 16:01:42 -0700108 res.code = 200;
109 res.end();
110 return;
111 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700112
Ed Tanousf3d847c2017-06-12 16:01:42 -0700113 // else let the request continue unharmed
114 }
115 }
116
117 void after_handle(request& req, response& res, context& ctx) {
118 // Do nothing
119 }
Ed Tanous8041f312017-04-03 09:47:01 -0700120
Ed Tanous911ac312017-08-15 09:37:42 -0700121 boost::container::flat_set<std::string> allowed_routes;
Ed Tanous99923322017-03-03 14:21:24 -0800122};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700123
Ed Tanousba9f9a62017-10-11 16:40:35 -0700124// TODO(ed) see if there is a better way to allow middlewares to request
125// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700126// Possibly an init function on first construction?
127template <typename... Middlewares>
128void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700129 static_assert(
130 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
131 "TokenAuthorization middleware must be enabled in app to use "
132 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700133 CROW_ROUTE(app, "/login")
134 .methods(
135 "POST"_method)([&](const crow::request& req, crow::response& res) {
136 std::string content_type;
137 auto content_type_it = req.headers.find("content-type");
138 if (content_type_it != req.headers.end()) {
139 content_type = content_type_it->second;
140 boost::algorithm::to_lower(content_type);
141 }
142 std::string username;
143 std::string password;
144 bool looks_like_ibm = false;
145 // Check if auth was provided by a payload
146 if (content_type == "application/json") {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700147 auto login_credentials =
148 nlohmann::json::parse(req.body, nullptr, false);
149 if (login_credentials.is_discarded()) {
Ed Tanous911ac312017-08-15 09:37:42 -0700150 res.code = 400;
151 res.end();
152 return;
153 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700154 // check for username/password in the root object
155 // THis method is how intel APIs authenticate
156 auto user_it = login_credentials.find("username");
157 auto pass_it = login_credentials.find("password");
158 if (user_it != login_credentials.end() &&
159 pass_it != login_credentials.end()) {
160 username = user_it->get<const std::string>();
161 password = pass_it->get<const std::string>();
162 } else {
163 // Openbmc appears to push a data object that contains the same
164 // keys (username and password), attempt to use that
165 auto data_it = login_credentials.find("data");
166 if (data_it != login_credentials.end()) {
167 // Some apis produce an array of value ["username",
168 // "password"]
169 if (data_it->is_array()) {
170 if (data_it->size() == 2) {
171 username = (*data_it)[0].get<const std::string>();
172 password = (*data_it)[1].get<const std::string>();
173 looks_like_ibm = true;
174 }
175 } else if (data_it->is_object()) {
176 auto user_it = data_it->find("username");
177 auto pass_it = data_it->find("password");
178 if (user_it != data_it->end() && pass_it != data_it->end()) {
179 username = user_it->get<const std::string>();
180 password = pass_it->get<const std::string>();
181 }
182 }
183 }
184 }
Ed Tanous911ac312017-08-15 09:37:42 -0700185 } else {
186 // check if auth was provided as a query string
187 auto user_it = req.headers.find("username");
188 auto pass_it = req.headers.find("password");
189 if (user_it != req.headers.end() && pass_it != req.headers.end()) {
190 username = user_it->second;
191 password = pass_it->second;
192 }
193 }
194
195 if (!username.empty() && !password.empty()) {
196 if (!pam_authenticate_user(username, password)) {
197 res.code = 401;
198 } else {
Ed Tanousc963aa42017-10-27 16:00:19 -0700199 auto& context =
200 app.template get_context<PersistentData::Middleware>(req);
201 auto& session_store = context.sessions;
202 auto& session = session_store->generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700203
Ed Tanousba9f9a62017-10-11 16:40:35 -0700204 if (looks_like_ibm) {
205 // IBM requires a very specific login structure, and doesn't
206 // actually look at the status code. TODO(ed).... Fix that
207 // upstream
208 nlohmann::json ret{{"data", "User '" + username + "' logged in"},
209 {"message", "200 OK"},
210 {"status", "ok"}};
211 res.add_header(
212 "Set-Cookie",
213 "SESSION=" + session.session_token + "; Secure; HttpOnly");
214 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
215 res.write(ret.dump());
216 } else {
217 // if content type is json, assume json token
218 nlohmann::json ret{{"token", session.session_token}};
219
220 res.write(ret.dump());
221 res.add_header("Content-Type", "application/json");
Ed Tanous911ac312017-08-15 09:37:42 -0700222 }
223 }
224
225 } else {
226 res.code = 400;
227 }
228 res.end();
229 });
230}
231} // namespaec TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700232} // namespace crow