Skip to content

Commit

Permalink
feat!: implement token refreshment
Browse files Browse the repository at this point in the history
This changes how tokens are used by introducing a two different tokens: short-living access token and long-living refresh token.

BREAKING CHANGES: Endpoints now require `access_token` field passed either in cookie or as bearer. `/api/auth` returns `refresh_token` and `access_token`. New `/api/auth/refresh` and `/api/auth/revoke` endpoints are added while `/api/verify` is deleted.
  • Loading branch information
LordTermor committed Jun 16, 2024
1 parent 3bb387b commit 0468955
Show file tree
Hide file tree
Showing 15 changed files with 566 additions and 127 deletions.
4 changes: 3 additions & 1 deletion daemon/di.h
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,9 @@ namespace Presentation {
: kgr::shared_service<
bxt::Presentation::AuthController,
kgr::dependency<di::Presentation::JwtOptions,
di::Core::Application::AuthService>> {};
di::Core::Application::AuthService,
di::Utilities::LMDB::Environment,
di::Core::Domain::UnitOfWorkBaseFactory>> {};

struct UserController
: kgr::shared_service<
Expand Down
3 changes: 3 additions & 0 deletions daemon/presentation/JwtOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ namespace bxt::Presentation {

struct JwtOptions {
std::string secret = "secret";
std::string issuer = "bxt";

void serialize(Utilities::Configuration &config) {
config.set("jwt-secret", secret);
config.set("jwt-issuer", issuer);
}
void deserialize(const Utilities::Configuration &config) {
secret = config.get<std::string>("jwt-secret").value_or(secret);
issuer = config.get<std::string>("jwt-issuer").value_or(issuer);
}
};
} // namespace bxt::Presentation
19 changes: 19 additions & 0 deletions daemon/presentation/Names.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* === This file is part of bxt ===
*
* SPDX-FileCopyrightText: 2024 Artem Grinev <agrinev@manjaro.org>
* SPDX-License-Identifier: AGPL-3.0-or-later
*
*/

#pragma once

namespace bxt::Presentation::Names {
constexpr inline auto TokenType = "token_type";
constexpr inline auto AccessToken = "access_token";
constexpr inline auto RefreshToken = "refresh_token";
constexpr inline auto Storage = "storage";
constexpr inline auto UserName = "username";
constexpr inline auto CookieStorage = "cookie";
constexpr inline auto BearerStorage = "bearer";
constexpr inline auto TokenKind = "kind";
} // namespace bxt::Presentation::Names
99 changes: 99 additions & 0 deletions daemon/presentation/Token.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* === This file is part of bxt ===
*
* SPDX-FileCopyrightText: 2024 Artem Grinev <agrinev@manjaro.org>
* SPDX-License-Identifier: AGPL-3.0-or-later
*
*/

#include "Token.h"

#include <fmt/format.h>
#include <jwt-cpp/traits/nlohmann-json/defaults.h>

namespace bxt::Presentation {

constexpr auto AccessTokenExpiration = std::chrono::minutes {15};
constexpr auto RefreshTokenExpiration = std::chrono::weeks {2};

std::expected<Token, std::string>
bxt::Presentation::Token::verify_jwt(const std::string &jwt,
const std::string &issuer,
const std::string &secret) {
try {
const auto decoded = jwt::decode(jwt);
const auto verifier =
jwt::verify()
.allow_algorithm(jwt::algorithm::hs256 {secret})
.with_issuer(issuer);

verifier.verify(decoded);

if (!decoded.has_payload_claim(Names::Storage)) {
return std::unexpected("No token storage provided");
}

if (!decoded.has_payload_claim(Names::TokenKind)
|| !decoded.has_payload_claim(Names::UserName)) {
return std::unexpected("No token kind or username provided");
}

auto storage = decoded.get_payload_claim(Names::Storage).as_string();
auto kind = decoded.get_payload_claim(Names::TokenKind).as_string();
auto username = decoded.get_payload_claim(Names::UserName).as_string();

return Token {username,
kind == Names::AccessToken ? Token::Kind::Access
: Token::Kind::Refresh,
storage == Names::CookieStorage ? Token::Storage::Cookie
: Token::Storage::Bearer,
decoded.get_issued_at(),
decoded.get_expires_at(),
jwt};
} catch (const std::exception &exception) {
return std::unexpected(fmt::format(
"Token is invalid, the error is: \"{}\"", exception.what()));
}
}

bxt::Presentation::Token::Token(std::string name, Kind kind, Storage storage)
: m_kind(kind),
m_storage(storage),
m_name(std::move(name)),
m_issued_at(std::chrono::system_clock::now()) {
using namespace std::chrono_literals;
if (kind == Kind::Access) {
m_expires_at = m_issued_at + AccessTokenExpiration;
} else {
m_expires_at = m_issued_at + RefreshTokenExpiration;
}
}

std::string bxt::Presentation::Token::generate_jwt(const std::string &issuer,
const std::string &secret) {
if (m_cached_jwt.has_value()) {
try {
const auto decoded = jwt::decode(*m_cached_jwt);
const auto verifier =
jwt::verify()
.allow_algorithm(jwt::algorithm::hs256 {secret})
.with_issuer(issuer);

verifier.verify(decoded);
return *m_cached_jwt;
} catch (const std::exception &exception) {}
}

m_cached_jwt =
jwt::create()
.set_payload_claim(Names::UserName, m_name)
.set_payload_claim(Names::Storage, bxt::to_string(m_storage))
.set_payload_claim(Names::TokenKind, bxt::to_string(m_kind))
.set_issuer(issuer)
.set_type("JWS")
.set_issued_at(m_issued_at)
.set_expires_at(m_expires_at)
.sign(jwt::algorithm::hs256 {secret});

return *m_cached_jwt;
}
} // namespace bxt::Presentation
87 changes: 87 additions & 0 deletions daemon/presentation/Token.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* === This file is part of bxt ===
*
* SPDX-FileCopyrightText: 2024 Artem Grinev <agrinev@manjaro.org>
* SPDX-License-Identifier: AGPL-3.0-or-later
*
*/
#pragma once

#include "presentation/Names.h"
#include "utilities/to_string.h"

#include <chrono>
#include <expected>
#include <optional>
#include <string>
#include <utility>

namespace bxt::Presentation {

class Token {
public:
using time_point = std::chrono::system_clock::time_point;
enum Kind { Access, Refresh };
enum Storage { Cookie, Bearer };
Token(std::string name, Kind kind, Storage storage);

std::string generate_jwt(const std::string &issuer,
const std::string &secret);

static std::expected<Token, std::string>
verify_jwt(const std::string &jwt,
const std::string &issuer,
const std::string &secret);

Kind kind() const { return m_kind; }

Storage storage() const { return m_storage; }

std::string name() const { return m_name; }

time_point issued_at() const { return m_issued_at; }

time_point expires_at() const { return m_expires_at; }

private:
Token(std::string name,
Kind kind,
Storage storage,
time_point issued_at,
time_point expires_at,
std::optional<std::string> cached_jwt = std::nullopt)
: m_kind(kind),
m_storage(storage),
m_name(std::move(name)),
m_issued_at(issued_at),
m_expires_at(expires_at),
m_cached_jwt(std::move(cached_jwt)) {}
Kind m_kind = Kind::Access;
Storage m_storage = Storage::Cookie;
std::string m_name;
time_point m_issued_at;
time_point m_expires_at;
std::optional<std::string> m_cached_jwt = std::nullopt;
};
} // namespace bxt::Presentation

template<>
inline std::string bxt::to_string(const Presentation::Token::Storage &storage) {
switch (storage) {
case Presentation::Token::Storage::Cookie:
return Presentation::Names::CookieStorage;
case Presentation::Token::Storage::Bearer:
return Presentation::Names::BearerStorage;
default: return "Unknown";
}
}

template<>
inline std::string bxt::to_string(const Presentation::Token::Kind &kind) {
switch (kind) {
case Presentation::Token::Kind::Access:
return Presentation::Names::AccessToken;
case Presentation::Token::Kind::Refresh:
return Presentation::Names::RefreshToken;
default: return "Unknown";
}
}
1 change: 1 addition & 0 deletions daemon/presentation/messages/AuthMessages.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct AuthRequest {
// for CLI usage
struct AuthResponse {
std::string access_token;
std::string refresh_token;
std::string token_type;
};
} // namespace bxt::Presentation
Loading

0 comments on commit 0468955

Please sign in to comment.