blob: 81d9e58033cef5577488034526fb0e9924c80105 [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>
Ed Tanousf9273472017-02-28 16:05:13 -08008#include <crow/http_request.h>
9#include <crow/http_response.h>
Ed Tanousba9f9a62017-10-11 16:40:35 -070010#include <boost/bimap.hpp>
Ed Tanous4758d5b2017-06-06 15:28:13 -070011#include <boost/container/flat_set.hpp>
Ed Tanous99923322017-03-03 14:21:24 -080012namespace crow {
Ed Tanousb4d29f42017-03-24 16:39:25 -070013
Ed Tanous911ac312017-08-15 09:37:42 -070014namespace TokenAuthorization {
Ed Tanous1ff48782017-04-18 12:45:08 -070015struct User {};
Ed Tanousb4d29f42017-03-24 16:39:25 -070016
Ed Tanous911ac312017-08-15 09:37:42 -070017class Middleware {
Ed Tanous3dac7492017-08-02 13:46:20 -070018 public:
Ed Tanous911ac312017-08-15 09:37:42 -070019 struct context {};
Ed Tanousba9f9a62017-10-11 16:40:35 -070020 template <typename AllContext>
21 void before_handle(crow::request& req, response& res, context& ctx,
22 AllContext& allctx) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070023 auto return_unauthorized = [&req, &res]() {
24 res.code = 401;
25 res.end();
26 };
Ed Tanousf9273472017-02-28 16:05:13 -080027
Ed Tanousba9f9a62017-10-11 16:40:35 -070028 if (crow::webassets::routes.find(req.url) !=
29 crow::webassets::routes.end()) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070030 // TODO this is total hackery to allow the login page to work before the
31 // user is authenticated. Also, it will be quite slow for all pages
Ed Tanous3dac7492017-08-02 13:46:20 -070032 // instead of a one time hit for the whitelist entries. Ideally, this
Ed Tanous911ac312017-08-15 09:37:42 -070033 // should be done in the url router handler, with tagged routes for the
34 // whitelist entries. Another option would be to whitelist a minimal form
35 // based page that didn't load the full angular UI until after login
Ed Tanousba9f9a62017-10-11 16:40:35 -070036 } else if (req.url == "/login" || req.url == "/redfish/v1" ||
37 req.url == "/redfish/v1/" ||
38 req.url == "/redfish/v1/$metadata" ||
39 (req.url == "/redfish/v1/SessionService/Sessions/" &&
40 req.method == "POST"_method)) {
Ed Tanous911ac312017-08-15 09:37:42 -070041 } else {
42 // Normal, non login, non static file request
43 // Check for an authorization header, reject if not present
44 std::string auth_key;
Ed Tanousba9f9a62017-10-11 16:40:35 -070045 bool require_csrf = true;
Ed Tanous911ac312017-08-15 09:37:42 -070046 if (req.headers.count("Authorization") == 1) {
47 std::string auth_header = req.get_header_value("Authorization");
48 // If the user is attempting any kind of auth other than token, reject
49 if (!boost::starts_with(auth_header, "Token ")) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070050 return_unauthorized();
51 return;
52 }
Ed Tanous911ac312017-08-15 09:37:42 -070053 auth_key = auth_header.substr(6);
Ed Tanousba9f9a62017-10-11 16:40:35 -070054 require_csrf = false;
55 } else if (req.headers.count("X-Auth-Token") == 1) {
56 auth_key = req.get_header_value("X-Auth-Token");
57 require_csrf = false;
Ed Tanous911ac312017-08-15 09:37:42 -070058 } else {
59 int count = req.headers.count("Cookie");
60 if (count == 1) {
61 auto& cookie_value = req.get_header_value("Cookie");
62 auto start_index = cookie_value.find("SESSION=");
63 if (start_index != std::string::npos) {
64 start_index += 8;
65 auto end_index = cookie_value.find(";", start_index);
66 if (end_index == std::string::npos) {
Ed Tanousba9f9a62017-10-11 16:40:35 -070067 end_index = cookie_value.size();
Ed Tanous911ac312017-08-15 09:37:42 -070068 }
69 auth_key =
Ed Tanousba9f9a62017-10-11 16:40:35 -070070 cookie_value.substr(start_index, end_index - start_index);
Ed Tanous911ac312017-08-15 09:37:42 -070071 }
72 }
Ed Tanousba9f9a62017-10-11 16:40:35 -070073 require_csrf = true; // Cookies require CSRF
Ed Tanous911ac312017-08-15 09:37:42 -070074 }
75 if (auth_key.empty()) {
76 res.code = 400;
77 res.end();
78 return;
79 }
Ed Tanousba9f9a62017-10-11 16:40:35 -070080 auto& data_mw = allctx.template get<PersistentData::Middleware>();
Ed Tanousc963aa42017-10-27 16:00:19 -070081 const PersistentData::UserSession* session =
82 data_mw.sessions->login_session_by_token(auth_key);
83 if (session == nullptr) {
Ed Tanousf3d847c2017-06-12 16:01:42 -070084 return_unauthorized();
85 return;
86 }
87
Ed Tanousba9f9a62017-10-11 16:40:35 -070088 if (require_csrf) {
89 // RFC7231 defines methods that need csrf protection
90 if (req.method != "GET"_method) {
91 const std::string& csrf = req.get_header_value("X-XSRF-TOKEN");
92 // Make sure both tokens are filled
Ed Tanousc963aa42017-10-27 16:00:19 -070093 if (csrf.empty() || session->csrf_token.empty()) {
Ed Tanousba9f9a62017-10-11 16:40:35 -070094 return_unauthorized();
95 return;
96 }
97 // Reject if csrf token not available
Ed Tanousc963aa42017-10-27 16:00:19 -070098 if (csrf != session->csrf_token) {
Ed Tanousba9f9a62017-10-11 16:40:35 -070099 return_unauthorized();
100 return;
101 }
102 }
103 }
104
105 if (req.url == "/logout" && req.method == "POST"_method) {
Ed Tanousc963aa42017-10-27 16:00:19 -0700106 data_mw.sessions->remove_session(session);
Ed Tanousf3d847c2017-06-12 16:01:42 -0700107 res.code = 200;
108 res.end();
109 return;
110 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700111
Ed Tanousf3d847c2017-06-12 16:01:42 -0700112 // else let the request continue unharmed
113 }
114 }
115
116 void after_handle(request& req, response& res, context& ctx) {
117 // Do nothing
118 }
Ed Tanous8041f312017-04-03 09:47:01 -0700119
Ed Tanous911ac312017-08-15 09:37:42 -0700120 boost::container::flat_set<std::string> allowed_routes;
Ed Tanous99923322017-03-03 14:21:24 -0800121};
Ed Tanousf3d847c2017-06-12 16:01:42 -0700122
Ed Tanousba9f9a62017-10-11 16:40:35 -0700123// TODO(ed) see if there is a better way to allow middlewares to request
124// routes.
Ed Tanous911ac312017-08-15 09:37:42 -0700125// Possibly an init function on first construction?
126template <typename... Middlewares>
127void request_routes(Crow<Middlewares...>& app) {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700128 static_assert(
129 black_magic::contains<PersistentData::Middleware, Middlewares...>::value,
130 "TokenAuthorization middleware must be enabled in app to use "
131 "auth routes");
Ed Tanous911ac312017-08-15 09:37:42 -0700132 CROW_ROUTE(app, "/login")
133 .methods(
134 "POST"_method)([&](const crow::request& req, crow::response& res) {
135 std::string content_type;
136 auto content_type_it = req.headers.find("content-type");
137 if (content_type_it != req.headers.end()) {
138 content_type = content_type_it->second;
139 boost::algorithm::to_lower(content_type);
140 }
141 std::string username;
142 std::string password;
143 bool looks_like_ibm = false;
144 // Check if auth was provided by a payload
145 if (content_type == "application/json") {
Ed Tanousba9f9a62017-10-11 16:40:35 -0700146 auto login_credentials =
147 nlohmann::json::parse(req.body, nullptr, false);
148 if (login_credentials.is_discarded()) {
Ed Tanous911ac312017-08-15 09:37:42 -0700149 res.code = 400;
150 res.end();
151 return;
152 }
Ed Tanousba9f9a62017-10-11 16:40:35 -0700153 // check for username/password in the root object
154 // THis method is how intel APIs authenticate
155 auto user_it = login_credentials.find("username");
156 auto pass_it = login_credentials.find("password");
157 if (user_it != login_credentials.end() &&
158 pass_it != login_credentials.end()) {
159 username = user_it->get<const std::string>();
160 password = pass_it->get<const std::string>();
161 } else {
162 // Openbmc appears to push a data object that contains the same
163 // keys (username and password), attempt to use that
164 auto data_it = login_credentials.find("data");
165 if (data_it != login_credentials.end()) {
166 // Some apis produce an array of value ["username",
167 // "password"]
168 if (data_it->is_array()) {
169 if (data_it->size() == 2) {
170 username = (*data_it)[0].get<const std::string>();
171 password = (*data_it)[1].get<const std::string>();
172 looks_like_ibm = true;
173 }
174 } else if (data_it->is_object()) {
175 auto user_it = data_it->find("username");
176 auto pass_it = data_it->find("password");
177 if (user_it != data_it->end() && pass_it != data_it->end()) {
178 username = user_it->get<const std::string>();
179 password = pass_it->get<const std::string>();
180 }
181 }
182 }
183 }
Ed Tanous911ac312017-08-15 09:37:42 -0700184 } else {
185 // check if auth was provided as a query string
186 auto user_it = req.headers.find("username");
187 auto pass_it = req.headers.find("password");
188 if (user_it != req.headers.end() && pass_it != req.headers.end()) {
189 username = user_it->second;
190 password = pass_it->second;
191 }
192 }
193
194 if (!username.empty() && !password.empty()) {
195 if (!pam_authenticate_user(username, password)) {
196 res.code = 401;
197 } else {
Ed Tanousc963aa42017-10-27 16:00:19 -0700198 auto& context =
199 app.template get_context<PersistentData::Middleware>(req);
200 auto& session_store = context.sessions;
201 auto& session = session_store->generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700202
Ed Tanousba9f9a62017-10-11 16:40:35 -0700203 if (looks_like_ibm) {
204 // IBM requires a very specific login structure, and doesn't
205 // actually look at the status code. TODO(ed).... Fix that
206 // upstream
207 nlohmann::json ret{{"data", "User '" + username + "' logged in"},
208 {"message", "200 OK"},
209 {"status", "ok"}};
210 res.add_header(
211 "Set-Cookie",
212 "SESSION=" + session.session_token + "; Secure; HttpOnly");
213 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
214 res.write(ret.dump());
215 } else {
216 // if content type is json, assume json token
217 nlohmann::json ret{{"token", session.session_token}};
218
219 res.write(ret.dump());
220 res.add_header("Content-Type", "application/json");
Ed Tanous911ac312017-08-15 09:37:42 -0700221 }
222 }
223
224 } else {
225 res.code = 400;
226 }
227 res.end();
228 });
229}
230} // namespaec TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700231} // namespace crow