From 55d70ef1cbaf47d00667dec53aa1494a8c822e6e Mon Sep 17 00:00:00 2001 From: Artem Grinev Date: Fri, 7 Jun 2024 18:36:58 +0000 Subject: [PATCH] feat!: implement bearer token support This commit introduces a way to authenticate via bearer token in addition to cookie-based storage. BREAKING CHANGE: /api/auth now requires a token_type parameter which is either `bearer` or `cookie` --- daemon/presentation/messages/AuthMessages.h | 7 +++ .../web-controllers/AuthController.cpp | 44 +++++++++++++++---- daemon/presentation/web-filters/JwtFilter.cpp | 20 +++++++++ daemon/swagger/openapi.yml.in | 32 +++++++++++++- frontend/src/pages/LoginPage.tsx | 3 +- 5 files changed, 96 insertions(+), 10 deletions(-) diff --git a/daemon/presentation/messages/AuthMessages.h b/daemon/presentation/messages/AuthMessages.h index 46e9175c..3260c852 100644 --- a/daemon/presentation/messages/AuthMessages.h +++ b/daemon/presentation/messages/AuthMessages.h @@ -12,5 +12,12 @@ namespace bxt::Presentation { struct AuthRequest { std::string name; std::string password; + std::string token_type; +}; + +// for CLI usage +struct AuthResponse { + std::string access_token; + std::string token_type; }; } // namespace bxt::Presentation diff --git a/daemon/presentation/web-controllers/AuthController.cpp b/daemon/presentation/web-controllers/AuthController.cpp index 8e32c148..ef76b2f8 100644 --- a/daemon/presentation/web-controllers/AuthController.cpp +++ b/daemon/presentation/web-controllers/AuthController.cpp @@ -20,14 +20,14 @@ namespace bxt::Presentation { drogon::Task AuthController::auth(drogon::HttpRequestPtr req) { const auto auth_request = - drogon_helpers::get_request_json(req); + drogon_helpers::get_request_json(req, true); if (!auth_request) { co_return drogon_helpers::make_error_response( fmt::format("Invalid request: {}", auth_request.error()->what())); } - const auto& [name, password] = *auth_request; + const auto& [name, password, token_type] = *auth_request; const auto check_ok = co_await m_service.auth(name, password); @@ -38,21 +38,39 @@ drogon::Task const auto token = jwt::create() .set_payload_claim("username", name) + .set_payload_claim("token_type", token_type) .set_issuer("auth0") .set_type("JWS") .sign(jwt::algorithm::hs256 {m_options.secret}); - drogon::Cookie jwt_cookie("token", token); - jwt_cookie.setHttpOnly(true); - auto response = drogon::HttpResponse::newHttpResponse(); - response->addCookie(jwt_cookie); + if (token_type == "bearer") { + co_return drogon_helpers::make_json_response( + AuthResponse {.access_token = token, .token_type = "bearer"}, true); + } else if (token_type == "cookie") { + drogon::Cookie jwt_cookie("token", token); + jwt_cookie.setHttpOnly(true); - co_return response; + auto response = drogon_helpers::make_ok_response(); + response->addCookie(jwt_cookie); + co_return response; + } + co_return drogon_helpers::make_error_response("Invalid token store type", + drogon::k401Unauthorized); } drogon::Task AuthController::verify(drogon::HttpRequestPtr req) { - const auto token = req->getCookie("token"); + auto token = req->getCookie("token"); + auto token_storage = "cookie"; + + if (token.empty()) { + auto bearer = req->getHeader("Authorization"); + token_storage = "bearer"; + if (!bearer.empty() && bearer.substr(0, 7) == "Bearer ") { + token = bearer.substr(7); + } + } + if (token.empty()) { co_return drogon_helpers::make_error_response("Token is missing", drogon::k401Unauthorized); @@ -67,6 +85,16 @@ drogon::Task verifier.verify(decoded); + auto token_type = decoded.get_payload_claim("token_type").as_string(); + + if (token_type != token_storage) { + co_return drogon_helpers::make_error_response( + fmt::format("Token type is invalid, expected: \"{}\", " + "got: \"{}\"", + token_storage, token_type), + drogon::k401Unauthorized); + } + co_return drogon_helpers::make_ok_response(); } catch (const std::exception& exception) { diff --git a/daemon/presentation/web-filters/JwtFilter.cpp b/daemon/presentation/web-filters/JwtFilter.cpp index 6e048d47..bcb63e6c 100644 --- a/daemon/presentation/web-filters/JwtFilter.cpp +++ b/daemon/presentation/web-filters/JwtFilter.cpp @@ -27,6 +27,15 @@ void JwtFilter::doFilter(const HttpRequestPtr &request, if (request->getMethod() == HttpMethod::Options) return fccb(); std::string token = request->getCookie("token"); + std::string token_storage = "cookie"; + + if (token.empty()) { + auto auth_header = request->getHeader("Authorization"); + if (!auth_header.empty() && auth_header.find("Bearer ") == 0) { + token = auth_header.substr(7); + token_storage = "bearer"; + } + } if (token.empty()) { return fcb(drogon_helpers::make_error_response( @@ -55,8 +64,19 @@ void JwtFilter::doFilter(const HttpRequestPtr &request, return fcb(drogon_helpers::make_error_response( "Authentification token is invalid", k401Unauthorized)); } + auto claims = decoded->get_payload_claims(); + auto token_type = claims["token_type"].as_string(); + + if (token_type != token_storage) { + return fcb(drogon_helpers::make_error_response( + fmt::format("Token type is invalid, expected: \"{}\", " + "got: \"{}\"", + token_storage, token_type), + k401Unauthorized)); + } + for (auto &claim : claims) request->getAttributes()->insert("jwt_" + claim.first, claim.second.as_string()); diff --git a/daemon/swagger/openapi.yml.in b/daemon/swagger/openapi.yml.in index 4d2a7523..c3c0fb5d 100644 --- a/daemon/swagger/openapi.yml.in +++ b/daemon/swagger/openapi.yml.in @@ -5,6 +5,11 @@ info: version: "MVP" servers: - url: "/" + +security: + - cookieAuth: [] + - bearerAuth: [] + paths: /api/auth: post: @@ -18,7 +23,11 @@ paths: $ref: "#/components/schemas/AuthRequest" responses: "200": - description: Authentication successful, JWT token set in cookie + description: Authentication successful, JWT token set in cookie or returned as bearer token + content: + application/json: + schema: + $ref: "#/components/schemas/AuthResponse" "400": description: Invalid request "401": @@ -244,6 +253,16 @@ paths: description: No permissions components: + securitySchemes: + cookieAuth: + type: apiKey + in: cookie + name: token + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: AuthRequest: type: object @@ -252,9 +271,20 @@ components: type: string password: type: string + token_type: + type: string required: - name - password + - token_type + + AuthResponse: + type: object + properties: + access_token: + type: string + token_type: + type: string CompareRequest: type: array diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 9e59376d..34ed61e0 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -24,7 +24,8 @@ export default (props: any) => { const result = await axios .post("/api/auth", { name: name, - password: password + password: password, + token_type: "cookie" }) .catch((err) => { toast.error("Login failed");