Audience: Adopter · Status: stable · Verified-against: qbm-http @ qb 3.0.0 (C++20 default, C++23 supported)
The qb::http::auth triad — Options, User, and Manager — issues and verifies JSON Web Tokens, turns a valid token into a typed principal, and feeds the two authentication middleware that gate your routes.
Prerequisites: the request context for ctx->set/ctx->get, and the middleware model for chain order. See also: standard middleware for the full JwtOptions/auth-middleware configuration tables, HTTPS for the SSL build, and the doc map README.md.
This page is the reference for the authentication system: the three value types under qb::http::auth and the security contract of the Manager. The two middleware that drive it — JwtMiddleware and AuthMiddleware — are configured in detail on the standard-middleware page; here you will see how they connect to the system and how to issue tokens yourself.
Two facts to keep in mind throughout:
- The whole auth path is SSL-gated.
src/qbm/http/auth/manager.cppis one of the SSL-only sources, and<qbm/http/middleware/all.h>andmake.hguard the JWT and auth includes behind#ifdef QB_HAS_SSL. In a build withoutQB_HAS_SSL,qb::jwtand the auth middleware are not compiled in. See feature gates. - The module is a compiled library, not header-only. You link
qbm::http; the umbrella<qbm/http/auth.h>brings the declarations in. The JWT machinery lives inqb-io's crypto library (qb::jwt), whichauth.hpulls in via<qb/io/crypto_jwt.h>.
| Type | Alias | Role |
|---|---|---|
qb::http::auth::Options |
auth::AuthOptions |
Keys, algorithm, expiration, expected claims, header/scheme, and verification-policy flags. |
qb::http::auth::User |
auth::AuthUser |
The authenticated principal: id, username, roles, metadata, plus role predicates. |
qb::http::auth::Manager |
auth::AuthManager |
Token lifecycle — generate_token, extract_token_from_header, verify_token. |
Include them all through the convenience header:
#include <qbm/http/auth.h> // qb::http::auth::Options, User, ManagerOptions is a fluent configuration object. Every setter returns Options&, so you build it in a chain. Defaults are production-safe: HMAC-SHA256, a one-hour expiry, signature verification on, and Bearer extraction from the Authorization header.
#include <qbm/http/auth.h>
#include <chrono>
qb::http::auth::Options opts;
opts.secret_key("a-strong-32-byte-minimum-hmac-secret")
.algorithm(qb::http::auth::Options::Algorithm::HMAC_SHA256)
.token_expiration(std::chrono::hours(1)) // std::chrono::seconds field
.token_issuer("my-api") // also enables issuer verification
.clock_skew_tolerance(std::chrono::seconds(30));Options::Algorithm enumerates the families qb::jwt can sign and verify. Options::algorithm_from_string maps the case-insensitive JWT header strings onto them, and returns std::nullopt for an unknown string so you can reject bad config before constructing Options.
Algorithm enum |
JWT alg string |
Key material |
|---|---|---|
HMAC_SHA256 / HMAC_SHA384 / HMAC_SHA512 |
HS256 / HS384 / HS512 |
secret_key (symmetric) |
RSA_SHA256 / RSA_SHA384 / RSA_SHA512 |
RS256 / RS384 / RS512 |
private_key to sign, public_key to verify (PEM) |
ECDSA_SHA256 / ECDSA_SHA384 / ECDSA_SHA512 |
ES256 / ES384 / ES512 |
private_key / public_key (PEM) |
ED25519 |
EdDSA |
private_key / public_key (PEM) |
For HMAC, set secret_key (a std::string is reinterpreted as raw bytes, or pass a std::vector<unsigned char>). For the asymmetric families, set private_key (PEM) for signing and public_key (PEM) for verification — a verify-only service needs only the public key.
Two of the Options durations are std::chrono::seconds, not qb::duration. This is deliberate: JWT exp, nbf, and iat are RFC 7519 NumericDate values — integer seconds since the Unix epoch from the system (wall) clock — and seconds is the unit the library reads and writes. Do not convert these to qb::duration.
token_expiration(std::chrono::seconds)— validity of an issued token; default3600s. Only emitted as anexpclaim when expiration verification is on.clock_skew_tolerance(std::chrono::seconds)— widens both theexpandnbfwindows during verification; default0.
| Setter | Default | Effect |
|---|---|---|
require_signature_verification(bool) |
true |
When false, the token is decoded without checking the signature. Unsafe — see Pitfalls. |
verify_expiration(bool) |
true |
Check the exp claim. When false, issued tokens carry no exp and never expire. |
verify_not_before(bool) |
true |
Check the nbf claim. |
token_issuer(std::string) |
— | A non-empty value auto-enables issuer verification; an empty string disables it. |
token_audience(std::string) |
— | A non-empty value auto-enables audience verification; an empty string disables it. |
auth_header_name(std::string) |
"Authorization" |
Header the manager reads the token from. |
auth_scheme(std::string) |
"Bearer" |
Scheme prefix expected before the token. |
There is no separate boolean to enable issuer or audience checks independently of the expected value: setting token_issuer("my-api") both records the expected iss and turns the check on; token_issuer("") turns it off.
User is the principal the system carries through the request. It is a plain struct with three role predicates.
struct User {
std::string id; // from the JWT "sub" claim
std::string username; // from the "username" claim
std::vector<std::string> roles;
qb::unordered_map<std::string, std::string> metadata;
bool has_role(const std::string &role) const noexcept; // case-sensitive
bool has_any_role(const std::vector<std::string> &roles) const noexcept; // empty list -> false
bool has_all_roles(const std::vector<std::string> &roles) const noexcept; // empty list -> true
};Role comparison is case-sensitive. The empty-list semantics matter for authorization gates: has_any_role({}) is false (no role can satisfy an empty allow-list), while has_all_roles({}) is true (no requirement to violate).
Manager owns an Options by value and is the workhorse for token operations. All three operations are const.
explicit Manager(const auth::Options &options = auth::Options()) noexcept;
std::string generate_token(const User &user) const; // sign
std::string extract_token_from_header(const std::string &) const; // parse scheme
std::optional<User> verify_token(const std::string &token) const; // verify + build User
const Options &get_options() const noexcept;
void set_options(const Options &) noexcept;generate_token builds a JWT payload from the User and the current Options, then signs it. The claims it writes:
| Claim | Source | Emitted when |
|---|---|---|
sub |
user.id |
always |
iat |
current epoch seconds | always |
exp |
iat + token_expiration |
verify_expiration is on |
iss |
token_issuer |
issuer verification is on |
aud |
token_audience |
audience verification is on |
username |
user.username |
always |
roles |
user.roles (JSON array) |
always |
metadata |
user.metadata (JSON object) |
when non-empty |
verify_token returns std::optional<User>. It never throws on a bad token — it returns std::nullopt for any failure (bad signature, expired, not-yet-valid, issuer/audience mismatch, malformed, or a token carrying neither sub nor username). Callers treat std::nullopt as "unauthenticated".
Two security properties are worth stating explicitly:
- The algorithm and key come from
Options, never from the token header.verify_tokenselects HMAC-secret or asymmetric-public-key based on the configured algorithm family, pinning verification and defeatingalg-confusion attacks driven by an attacker-controlled JWT header. - A verified
Usermust have a usable identity. If a signature-valid token resolves to an emptyidand emptyusername, verification still fails (logged andnullopt). Malformedroles/metadataJSON is tolerated — those fields are left empty and a warning is logged — because only a missing subject/username is fatal.
extract_token_from_header strips the configured scheme and returns the bare token, or an empty string on a format mismatch. The scheme match is case-insensitive and requires whitespace after it: "bearer <token>" is accepted, but "Bearertoken" (no separator) is rejected.
#include <qbm/http/auth.h>
#include <chrono>
qb::http::auth::Options opts;
opts.secret_key("a-strong-hmac-secret").token_issuer("my-api");
const qb::http::auth::Manager manager(opts); // const: safe to share on the request path
// Issue
qb::http::auth::User alice;
alice.id = "u-101";
alice.username = "alice";
alice.roles = {"editor"};
const std::string token = manager.generate_token(alice);
// Verify (e.g. on an incoming request)
const std::string raw = manager.extract_token_from_header("Bearer " + token);
if (!raw.empty()) {
if (auto user = manager.verify_token(raw)) {
// user->id == "u-101", user->has_role("editor") == true
}
// else: token present but invalid -> reject
}Manager is a lightweight value type with no shared mutable state, and its three operations are const, so a const Manager may be shared across the synchronous request path. There is no thread-safety contract beyond const-correctness: do not call set_options concurrently with verifications.
You rarely call the Manager from a handler. Two middleware drive it; both are SSL-gated and detailed on the standard-middleware page.
qb::http::AuthMiddleware<Session> (<qbm/http/middleware/auth.h>) wraps an auth::Manager. On each request it:
- Looks in the context (default key
"user") for a pre-authenticatedauth::User, then for a"jwt_payload"left by a precedingJwtMiddleware. - Otherwise extracts a token from the configured header and calls
verify_token. - On success, stores the
auth::Userin the context under the configured key. - If roles were required via
with_roles, checks them and answers403on failure. - Answers
401when authentication is required and no valid user could be established.
flowchart TD
REQ["incoming request"] --> CTX{"context already has<br/>auth::User ('user') or 'jwt_payload'?"}
CTX -- yes --> ROLES
CTX -- no --> EX["extract token from the configured header"]
EX --> VT["verify_token — alg + key pinned from Options<br/>(never the token header → defeats alg-confusion)"]
VT -- "invalid: bad sig / exp / nbf / iss / aud / no sub+username" --> R401["401 if auth required"]
VT -- valid --> STORE["store auth::User in context (key 'user')"]
STORE --> ROLES{"roles required (with_roles)?"}
ROLES -- "missing role" --> R403["403"]
ROLES -- "ok / none required" --> NEXT["continue the chain"]
Four factories cover the common shapes — all <Session>-templated:
| Factory | Behavior |
|---|---|
auth_middleware<S>(options, name) |
Required auth from options. |
jwt_auth_middleware<S>(secret, algo = "HS256", name) |
Required auth; secret is an HMAC secret for HS* or a public key otherwise. |
role_auth_middleware<S>(roles, require_all = false, name) |
Pure role gate; assumes an upstream middleware already populated the user. |
optional_auth_middleware<S>(options, name) |
Auth optional — proceeds when no credentials are sent, but still rejects an invalid token. |
#include <qbm/http/http.h>
#include <qbm/http/auth.h>
#include <qbm/http/middleware/auth.h>
using MySession = qb::http::DefaultSession; // the shipped server session
qb::http::Router<MySession> router;
qb::http::auth::Options opts;
opts.secret_key("a-strong-hmac-secret").token_issuer("my-api");
auto admin_gate = qb::http::auth_middleware<MySession>(opts);
admin_gate->with_auth_required(true)
.with_user_context_key("user")
.with_roles({"administrator"}); // any-of by default; pass true for all-of
router.use(admin_gate); // gate every route declared after this
router.get("/admin", [](auto ctx) {
auto user = ctx->template get<qb::http::auth::User>("user"); // std::optional<User>
ctx->json(qb::json{{"hello", user->username}}); // terminal helper
});Reach for the equivalent tag dispatch when you prefer the unified entry point. With namespace mw = qb::http::middleware;, mw::make<mw::tags::auth, MySession>(opts) forwards to the same factory; the other tags are mw::tags::jwt_auth, mw::tags::role_auth, and mw::tags::optional_auth.
qb::http::JwtMiddleware<Session> (<qbm/http/middleware/jwt.h>) verifies a JWT and stores the decoded payload as a qb::json under "jwt_payload" — it does not build an auth::User. Use it when you want the raw claims, when the token can sit in a cookie or query parameter (from_header/from_cookie/from_query), or as the verification stage in front of a role_auth_middleware gate.
#include <qbm/http/http.h>
#include <qbm/http/middleware/jwt.h>
qb::http::JwtOptions jwt_opts;
jwt_opts.secret = "a-strong-hmac-secret";
jwt_opts.algorithm = "HS256";
jwt_opts.verify_iss = true;
jwt_opts.issuer = "my-api";
auto jwt = qb::http::jwt_middleware_with_options<MySession>(jwt_opts);
jwt->require_claims({"sub", "roles"});
router.use(jwt);
router.get("/data", [](auto ctx) {
if (auto payload = ctx->template get<qb::json>("jwt_payload")) {
ctx->json(*payload);
} else {
ctx->internal_server_error(); // jwt_payload missing after auth
}
});JwtOptions::leeway is std::chrono::seconds for the same NumericDate reason as auth::Options. The full JwtOptions table and the fluent setters (from_cookie, with_validator, with_error_handler, with_success_handler) are on the standard-middleware page.
- Want a typed
auth::Userand role helpers in your handlers? UseAuthMiddleware. - Want the raw JWT payload, a non-header token location, or a custom validator over arbitrary claims? Use
JwtMiddleware. - Want both — verify once, then gate by role? Run
JwtMiddlewarefirst (it writes"jwt_payload"), thenrole_auth_middleware, which builds anauth::Userfrom that payload before checking roles.
- Never ship
require_signature_verification(false). With it off,verify_tokendecodes the payload without a signature check and validates onlyexp/nbf/iss/audagainst forgeable claims — any token with the right claims is accepted. The default istrue; keep ittruein production. (qbm/http/src/qbm/http/auth/manager.cpp:262,336-398) verify_expiration(false)mints non-expiring tokens.generate_tokenonly writes anexpclaim when expiration verification is on, so disabling it produces tokens that never expire and are never rejected for age.- The key must match the algorithm family. An HMAC secret under an
RS*/ES*/EdDSAalgorithm (or vice versa) does not throw —verify_tokensimply returnsstd::nullopt. A verify-only service still needs thepublic_keyset for asymmetric algorithms. "Bearertoken"is not a token.extract_token_from_headeralways requires a scheme prefix followed by whitespace, so a missing separator yields an empty string. It cannot extract a bare/scheme-less token:auth_scheme("")does not enable that — it makes the whitespace check fall on the token's first character and reject every header.- An empty principal fails even with a valid signature. A token whose
subandusernameboth resolve empty is rejected. When you issue tokens, setuser.id(it becomessub). - Context-helper responses are terminal.
ctx->json(...),ctx->unauthorized(),ctx->forbidden()and friends callcompleteinternally — set any custom headers or body before calling them. See the request context. - Don't mutate options under load.
set_optionsis not synchronized against in-flight verifications; configure theManager/middleware at startup and treat it as read-only on the request path.
- Standard middleware — full
JwtOptionsand auth-middleware configuration tables. - The middleware model — chain order and how
Router::usecomposes gates. - The request context —
ctx->set/ctx->gettyped slots and the terminal response helpers. - Custom middleware — building your own authentication stage.
- Enabling HTTPS (SSL/TLS) — the
QB_HAS_SSLbuild these features require.
Previous: The request context · Next: Validation system · Return to Index