From 604c1b5fc3e1d593be728364db5647eff2bc5f91 Mon Sep 17 00:00:00 2001 From: Tobias Date: Sat, 25 Aug 2018 21:39:23 +0200 Subject: [PATCH] web_service: Change authentication system to use JWT (#4041) * Change authentication system to JWT * Address review comments * Get rid of global variable, fix some documentations, fix a bug when verificating * Refactor PostJson to avoid code duplication * Rename jwt_token, add functionality to request a new JWT when getting a 401 * Take bools by value instead of const reference * Send request again when JWT is invalid and use forward declarations * Omit brackets --- src/common/announce_multiplayer_room.h | 1 + src/web_service/announce_room_json.cpp | 7 +- src/web_service/telemetry_json.cpp | 2 +- src/web_service/verify_login.cpp | 3 +- src/web_service/web_backend.cpp | 210 +++++++++++++++++-------- src/web_service/web_backend.h | 64 ++++++-- 6 files changed, 202 insertions(+), 85 deletions(-) diff --git a/src/common/announce_multiplayer_room.h b/src/common/announce_multiplayer_room.h index 00f115ca1..18b4696ba 100644 --- a/src/common/announce_multiplayer_room.h +++ b/src/common/announce_multiplayer_room.h @@ -24,6 +24,7 @@ struct WebResult { }; Code result_code; std::string result_string; + std::string returned_data; }; } // namespace Common diff --git a/src/web_service/announce_room_json.cpp b/src/web_service/announce_room_json.cpp index fa95b3f27..b53e084df 100644 --- a/src/web_service/announce_room_json.cpp +++ b/src/web_service/announce_room_json.cpp @@ -84,7 +84,7 @@ void RoomJson::AddPlayer(const std::string& nickname, std::future RoomJson::Announce() { nlohmann::json json = room; - return PostJson(endpoint_url, json.dump(), false, username, token); + return PostJson(endpoint_url, json.dump(), false); } void RoomJson::ClearPlayers() { @@ -99,14 +99,13 @@ std::future RoomJson::GetRoomList(std::functi func(); return room_list; }; - return GetJson(DeSerialize, endpoint_url, true, username, - token); + return GetJson(DeSerialize, endpoint_url, true); } void RoomJson::Delete() { nlohmann::json json; json["id"] = room.UID; - DeleteJson(endpoint_url, json.dump(), username, token); + DeleteJson(endpoint_url, json.dump()); } } // namespace WebService diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp index 28ae0d34e..0d7ff1c21 100644 --- a/src/web_service/telemetry_json.cpp +++ b/src/web_service/telemetry_json.cpp @@ -82,7 +82,7 @@ void TelemetryJson::Complete() { SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem"); // Send the telemetry async but don't handle the errors since they were written to the log - future = PostJson(endpoint_url, TopSection().dump(), true, username, token); + future = PostJson(endpoint_url, TopSection().dump(), true); } } // namespace WebService diff --git a/src/web_service/verify_login.cpp b/src/web_service/verify_login.cpp index e7c61f8af..f2e9615e8 100644 --- a/src/web_service/verify_login.cpp +++ b/src/web_service/verify_login.cpp @@ -26,7 +26,8 @@ std::future VerifyLogin(std::string& username, std::string& token, return username == *iter; }; - return GetJson(get_func, endpoint_url, false, username, token); + UpdateCoreJWT(true, username, token); + return GetJson(get_func, endpoint_url, false); } } // namespace WebService diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp index 696de6c30..b50635d7b 100644 --- a/src/web_service/web_backend.cpp +++ b/src/web_service/web_backend.cpp @@ -6,9 +6,9 @@ #include #include #include -#include #include "common/announce_multiplayer_room.h" #include "common/logging/log.h" +#include "core/settings.h" #include "web_service/web_backend.h" namespace WebService { @@ -20,6 +20,19 @@ constexpr int HTTPS_PORT = 443; constexpr int TIMEOUT_SECONDS = 30; +std::string UpdateCoreJWT(bool force_new_token, const std::string& username, + const std::string& token) { + static std::string jwt; + if (jwt.empty() || force_new_token) { + if (!username.empty() && !token.empty()) { + std::future future = + PostJson("https://api.citra-emu.org/jwt/internal", username, token); + jwt = future.get().returned_data; + } + } + return jwt; +} + std::unique_ptr GetClientFor(const LUrlParser::clParseURL& parsedUrl) { namespace hl = httplib; @@ -43,8 +56,102 @@ std::unique_ptr GetClientFor(const LUrlParser::clParseURL& pars } } +static Common::WebResult PostJsonAsyncFn(const std::string& url, + const LUrlParser::clParseURL& parsed_url, + const httplib::Headers& params, const std::string& data, + bool is_jwt_requested) { + static bool is_first_attempt = true; + + namespace hl = httplib; + std::unique_ptr cli = GetClientFor(parsed_url); + + if (cli == nullptr) { + return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; + } + + hl::Request request; + request.method = "POST"; + request.path = "/" + parsed_url.m_Path; + request.headers = params; + request.body = data; + + hl::Response response; + + if (!cli->send(request, response)) { + LOG_ERROR(WebService, "POST to {} returned null", url); + return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; + } + + if (response.status >= 400) { + LOG_ERROR(WebService, "POST to {} returned error status code: {}", url, response.status); + if (response.status == 401 && !is_jwt_requested && is_first_attempt) { + LOG_WARNING(WebService, "Requesting new JWT"); + UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token); + is_first_attempt = false; + PostJsonAsyncFn(url, parsed_url, params, data, is_jwt_requested); + is_first_attempt = true; + } + return Common::WebResult{Common::WebResult::Code::HttpError, + std::to_string(response.status)}; + } + + auto content_type = response.headers.find("content-type"); + + if (content_type == response.headers.end() || + (content_type->second.find("application/json") == std::string::npos && + content_type->second.find("text/html; charset=utf-8") == std::string::npos)) { + LOG_ERROR(WebService, "POST to {} returned wrong content: {}", url, content_type->second); + return Common::WebResult{Common::WebResult::Code::WrongContent, ""}; + } + + return Common::WebResult{Common::WebResult::Code::Success, "", response.body}; +} + std::future PostJson(const std::string& url, const std::string& data, - bool allow_anonymous, const std::string& username, + bool allow_anonymous) { + + using lup = LUrlParser::clParseURL; + namespace hl = httplib; + + lup parsedUrl = lup::ParseURL(url); + + if (url.empty() || !parsedUrl.IsValid()) { + LOG_ERROR(WebService, "URL is invalid"); + return std::async(std::launch::deferred, [] { + return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; + }); + } + + const std::string jwt = + UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token); + + const bool are_credentials_provided{!jwt.empty()}; + if (!allow_anonymous && !are_credentials_provided) { + LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); + return std::async(std::launch::deferred, [] { + return Common::WebResult{Common::WebResult::Code::CredentialsMissing, + "Credentials needed"}; + }); + } + + // Built request header + hl::Headers params; + if (are_credentials_provided) { + // Authenticated request if credentials are provided + params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, + {std::string("api-version"), std::string(API_VERSION)}, + {std::string("Content-Type"), std::string("application/json")}}; + } else { + // Otherwise, anonymous request + params = {{std::string("api-version"), std::string(API_VERSION)}, + {std::string("Content-Type"), std::string("application/json")}}; + } + + // Post JSON asynchronously + return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, data, false); +} + +std::future PostJson(const std::string& url, const std::string& username, const std::string& token) { using lup = LUrlParser::clParseURL; namespace hl = httplib; @@ -53,17 +160,16 @@ std::future PostJson(const std::string& url, const std::strin if (url.empty() || !parsedUrl.IsValid()) { LOG_ERROR(WebService, "URL is invalid"); - return std::async(std::launch::deferred, []() { - return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; + return std::async(std::launch::deferred, [] { + return Common::WebResult{Common::WebResult::Code::InvalidURL, ""}; }); } const bool are_credentials_provided{!token.empty() && !username.empty()}; - if (!allow_anonymous && !are_credentials_provided) { + if (!are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); - return std::async(std::launch::deferred, []() { - return Common::WebResult{Common::WebResult::Code::CredentialsMissing, - "Credentials needed"}; + return std::async(std::launch::deferred, [] { + return Common::WebResult{Common::WebResult::Code::CredentialsMissing, ""}; }); } @@ -82,50 +188,14 @@ std::future PostJson(const std::string& url, const std::strin } // Post JSON asynchronously - return std::async(std::launch::async, [url, parsedUrl, params, data] { - std::unique_ptr cli = GetClientFor(parsedUrl); - - if (cli == nullptr) { - return Common::WebResult{Common::WebResult::Code::InvalidURL, "URL is invalid"}; - } - - hl::Request request; - request.method = "POST"; - request.path = "/" + parsedUrl.m_Path; - request.headers = params; - request.body = data; - - hl::Response response; - - if (!cli->send(request, response)) { - LOG_ERROR(WebService, "POST to {} returned null", url); - return Common::WebResult{Common::WebResult::Code::LibError, "Null response"}; - } - - if (response.status >= 400) { - LOG_ERROR(WebService, "POST to {} returned error status code: {}", url, - response.status); - return Common::WebResult{Common::WebResult::Code::HttpError, - std::to_string(response.status)}; - } - - auto content_type = response.headers.find("content-type"); - - if (content_type == response.headers.end() || - content_type->second.find("application/json") == std::string::npos) { - LOG_ERROR(WebService, "POST to {} returned wrong content: {}", url, - content_type->second); - return Common::WebResult{Common::WebResult::Code::WrongContent, content_type->second}; - } - - return Common::WebResult{Common::WebResult::Code::Success, ""}; - }); + return std::async(std::launch::async, PostJsonAsyncFn, url, parsedUrl, params, "", true); } template std::future GetJson(std::function func, const std::string& url, - bool allow_anonymous, const std::string& username, - const std::string& token) { + bool allow_anonymous) { + static bool is_first_attempt = true; + using lup = LUrlParser::clParseURL; namespace hl = httplib; @@ -136,7 +206,10 @@ std::future GetJson(std::function func, const std::str return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); } - const bool are_credentials_provided{!token.empty() && !username.empty()}; + const std::string jwt = + UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token); + + const bool are_credentials_provided{!jwt.empty()}; if (!allow_anonymous && !are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); return std::async(std::launch::deferred, [func{std::move(func)}]() { return func(""); }); @@ -145,9 +218,7 @@ std::future GetJson(std::function func, const std::str // Built request header hl::Headers params; if (are_credentials_provided) { - // Authenticated request if credentials are provided - params = {{std::string("x-username"), username}, - {std::string("x-token"), token}, + params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, {std::string("api-version"), std::string(API_VERSION)}}; } else { // Otherwise, anonymous request @@ -155,7 +226,7 @@ std::future GetJson(std::function func, const std::str } // Get JSON asynchronously - return std::async(std::launch::async, [func, url, parsedUrl, params] { + return std::async(std::launch::async, [func, url, parsedUrl, params, allow_anonymous] { std::unique_ptr cli = GetClientFor(parsedUrl); if (cli == nullptr) { @@ -176,6 +247,13 @@ std::future GetJson(std::function func, const std::str if (response.status >= 400) { LOG_ERROR(WebService, "GET to {} returned error status code: {}", url, response.status); + if (response.status == 401 && is_first_attempt) { + LOG_WARNING(WebService, "Requesting new JWT"); + UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token); + is_first_attempt = false; + GetJson(func, url, allow_anonymous); + is_first_attempt = true; + } return func(""); } @@ -193,15 +271,14 @@ std::future GetJson(std::function func, const std::str } template std::future GetJson(std::function func, - const std::string& url, bool allow_anonymous, - const std::string& username, const std::string& token); + const std::string& url, bool allow_anonymous); template std::future GetJson( std::function func, - const std::string& url, bool allow_anonymous, const std::string& username, - const std::string& token); + const std::string& url, bool allow_anonymous); + +void DeleteJson(const std::string& url, const std::string& data) { + static bool is_first_attempt = true; -void DeleteJson(const std::string& url, const std::string& data, const std::string& username, - const std::string& token) { using lup = LUrlParser::clParseURL; namespace hl = httplib; @@ -212,15 +289,17 @@ void DeleteJson(const std::string& url, const std::string& data, const std::stri return; } - const bool are_credentials_provided{!token.empty() && !username.empty()}; + const std::string jwt = + UpdateCoreJWT(false, Settings::values.citra_username, Settings::values.citra_token); + + const bool are_credentials_provided{!jwt.empty()}; if (!are_credentials_provided) { LOG_ERROR(WebService, "Credentials must be provided for authenticated requests"); return; } // Built request header - hl::Headers params = {{std::string("x-username"), username}, - {std::string("x-token"), token}, + hl::Headers params = {{std::string("Authorization"), fmt::format("Bearer {}", jwt)}, {std::string("api-version"), std::string(API_VERSION)}, {std::string("Content-Type"), std::string("application/json")}}; @@ -248,6 +327,13 @@ void DeleteJson(const std::string& url, const std::string& data, const std::stri if (response.status >= 400) { LOG_ERROR(WebService, "DELETE to {} returned error status code: {}", url, response.status); + if (response.status == 401 && is_first_attempt) { + LOG_WARNING(WebService, "Requesting new JWT"); + UpdateCoreJWT(true, Settings::values.citra_username, Settings::values.citra_token); + is_first_attempt = false; + DeleteJson(url, data); + is_first_attempt = true; + } return; } diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h index 29aab00cb..d00095c91 100644 --- a/src/web_service/web_backend.h +++ b/src/web_service/web_backend.h @@ -8,46 +8,76 @@ #include #include #include +#include #include "common/announce_multiplayer_room.h" #include "common/common_types.h" +namespace LUrlParser { +class clParseURL; +} + namespace WebService { /** - * Posts JSON to services.citra-emu.org. - * @param url URL of the services.citra-emu.org endpoint to post data to. + * Requests a new JWT if necessary + * @param force_new_token If true, force to request a new token from the server. + * @param username Citra username to use for authentication. + * @param token Citra token to use for authentication. + * @return string with the current JWT toke + */ +std::string UpdateCoreJWT(bool force_new_token, const std::string& username, + const std::string& token); + +/** + * Posts JSON to a api.citra-emu.org. + * @param url URL of the api.citra-emu.org endpoint to post data to. + * @param parsed_url Parsed URL used for the POST request. + * @param params Headers sent for the POST request. + * @param data String of JSON data to use for the body of the POST request. + * @param data If true, a JWT is requested in the function + * @return future with the returned value of the POST + */ +static Common::WebResult PostJsonAsyncFn(const std::string& url, + const LUrlParser::clParseURL& parsed_url, + const httplib::Headers& params, const std::string& data, + bool is_jwt_requested); + +/** + * Posts JSON to api.citra-emu.org. + * @param url URL of the api.citra-emu.org endpoint to post data to. * @param data String of JSON data to use for the body of the POST request. * @param allow_anonymous If true, allow anonymous unauthenticated requests. + * @return future with the returned value of the POST + */ +std::future PostJson(const std::string& url, const std::string& data, + bool allow_anonymous); + +/** + * Posts JSON to api.citra-emu.org. + * @param url URL of the api.citra-emu.org endpoint to post data to. * @param username Citra username to use for authentication. * @param token Citra token to use for authentication. * @return future with the error or result of the POST */ -std::future PostJson(const std::string& url, const std::string& data, - bool allow_anonymous, const std::string& username = {}, - const std::string& token = {}); +std::future PostJson(const std::string& url, const std::string& username, + const std::string& token); /** - * Gets JSON from services.citra-emu.org. + * Gets JSON from api.citra-emu.org. * @param func A function that gets exectued when the json as a string is received - * @param url URL of the services.citra-emu.org endpoint to post data to. + * @param url URL of the api.citra-emu.org endpoint to post data to. * @param allow_anonymous If true, allow anonymous unauthenticated requests. - * @param username Citra username to use for authentication. - * @param token Citra token to use for authentication. * @return future that holds the return value T of the func */ template std::future GetJson(std::function func, const std::string& url, - bool allow_anonymous, const std::string& username = {}, - const std::string& token = {}); + bool allow_anonymous); /** - * Delete JSON to services.citra-emu.org. - * @param url URL of the services.citra-emu.org endpoint to post data to. + * Delete JSON to api.citra-emu.org. + * @param url URL of the api.citra-emu.org endpoint to post data to. * @param data String of JSON data to use for the body of the DELETE request. - * @param username Citra username to use for authentication. - * @param token Citra token to use for authentication. */ -void DeleteJson(const std::string& url, const std::string& data, const std::string& username = {}, - const std::string& token = {}); +void DeleteJson(const std::string& url, const std::string& data); } // namespace WebService