blob: 964228240ba4542638f324498ae6cafe810e2481 [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>();
82 auto session_it = data_mw.auth_tokens->find(auth_key);
83 if (session_it == data_mw.auth_tokens->end()) {
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
93 if (csrf.empty() || session_it->second.csrf_token.empty()) {
94 return_unauthorized();
95 return;
96 }
97 // Reject if csrf token not available
98 if (csrf != session_it->second.csrf_token) {
99 return_unauthorized();
100 return;
101 }
102 }
103 }
104
105 if (req.url == "/logout" && req.method == "POST"_method) {
106 data_mw.auth_tokens->erase(auth_key);
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 Tanousba9f9a62017-10-11 16:40:35 -0700198 auto& auth_middleware =
199 app.template get_middleware<PersistentData::Middleware>();
200 auto session = auth_middleware.generate_user_session(username);
Ed Tanous911ac312017-08-15 09:37:42 -0700201
Ed Tanousba9f9a62017-10-11 16:40:35 -0700202 if (looks_like_ibm) {
203 // IBM requires a very specific login structure, and doesn't
204 // actually look at the status code. TODO(ed).... Fix that
205 // upstream
206 nlohmann::json ret{{"data", "User '" + username + "' logged in"},
207 {"message", "200 OK"},
208 {"status", "ok"}};
209 res.add_header(
210 "Set-Cookie",
211 "SESSION=" + session.session_token + "; Secure; HttpOnly");
212 res.add_header("Set-Cookie", "XSRF-TOKEN=" + session.csrf_token);
213 res.write(ret.dump());
214 } else {
215 // if content type is json, assume json token
216 nlohmann::json ret{{"token", session.session_token}};
217
218 res.write(ret.dump());
219 res.add_header("Content-Type", "application/json");
Ed Tanous911ac312017-08-15 09:37:42 -0700220 }
221 }
222
223 } else {
224 res.code = 400;
225 }
226 res.end();
227 });
228}
229} // namespaec TokenAuthorization
Ed Tanousba9f9a62017-10-11 16:40:35 -0700230} // namespace crow