Skip to content

Commit

Permalink
feat!: implement bearer token support
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
LordTermor committed Jun 7, 2024
1 parent 710f506 commit 55d70ef
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 10 deletions.
7 changes: 7 additions & 0 deletions daemon/presentation/messages/AuthMessages.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 36 additions & 8 deletions daemon/presentation/web-controllers/AuthController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ namespace bxt::Presentation {
drogon::Task<drogon::HttpResponsePtr>
AuthController::auth(drogon::HttpRequestPtr req) {
const auto auth_request =
drogon_helpers::get_request_json<AuthRequest>(req);
drogon_helpers::get_request_json<AuthRequest>(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);

Expand All @@ -38,21 +38,39 @@ drogon::Task<drogon::HttpResponsePtr>

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<drogon::HttpResponsePtr>
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);
Expand All @@ -67,6 +85,16 @@ drogon::Task<drogon::HttpResponsePtr>

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) {
Expand Down
20 changes: 20 additions & 0 deletions daemon/presentation/web-filters/JwtFilter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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());
Expand Down
32 changes: 31 additions & 1 deletion daemon/swagger/openapi.yml.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ info:
version: "MVP"
servers:
- url: "/"

security:
- cookieAuth: []
- bearerAuth: []

paths:
/api/auth:
post:
Expand All @@ -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":
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit 55d70ef

Please sign in to comment.