JWT Auth
This example shows how to protect Vix routes with JWT authentication.
Use JWT when your API receives a signed token from a client and needs to read claims such as:
subject
roles
permissions
expiration
custom payload fieldsThis example uses a development HS256 token signed with:
secret = dev_secretThe route /api/me requires:
Authorization: Bearer <token>What this example builds
The app exposes:
GET /
GET /api/public
GET /api/meBehavior:
GET /
public help page
GET /api/public
public JSON route
GET /api/me
protected route, requires a valid JWTThe JWT middleware validates the token and stores claims in request state.
The handler reads:
vix::middleware::auth::JwtClaimsHeader
For Vix v2.6.2 and newer, use:
#include <vix/middleware.hpp>For older v2.6.0 or v2.6.1, App presets may need an explicit include:
#include <vix/middleware/app/presets.hpp>This example uses the modern public entry point.
Project structure
Create:
auth_jwt_demo/
└── auth_jwt.cppCreate the file:
mkdir auth_jwt_demo
cd auth_jwt_demo
touch auth_jwt.cppSource
Open:
auth_jwt.cppAdd:
#include <iostream>
#include <string>
#include <vix.hpp>
#include <vix/middleware.hpp>
using namespace vix;
static const std::string kToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0."
"3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo";
static void install_middleware(App &app)
{
app.use("/api", middleware::app::request_id_dev());
app.use("/api", middleware::app::timing_dev());
app.use("/api", middleware::app::security_headers_dev());
app.use("/api/me", middleware::app::jwt_dev(
"dev_secret"
));
}
static void register_routes(App &app)
{
app.get("/", [](Request &, Response &res)
{
res.send(
"JWT auth example\n"
"\n"
"Public:\n"
" curl -i http://127.0.0.1:8080/api/public\n"
"\n"
"Protected:\n"
" curl -i http://127.0.0.1:8080/api/me\n"
" curl -i -H \"Authorization: Bearer <TOKEN>\" http://127.0.0.1:8080/api/me\n"
);
});
app.get("/api/public", [](Request &, Response &res)
{
res.json({
"ok", true,
"message", "public route"
});
});
app.get("/api/me", [](Request &req, Response &res)
{
auto &claims =
req.state<middleware::auth::JwtClaims>();
res.json({
"ok", true,
"subject", claims.subject,
"roles", claims.roles
});
});
}
static void print_help()
{
std::cout
<< "Vix JWT auth example running:\n"
<< " http://127.0.0.1:8080/\n"
<< " http://127.0.0.1:8080/api/public\n"
<< " http://127.0.0.1:8080/api/me\n\n"
<< "Development token:\n"
<< " " << kToken << "\n\n"
<< "Try:\n"
<< " curl -i http://127.0.0.1:8080/api/me\n"
<< " curl -i -H \"Authorization: Bearer " << kToken
<< "\" http://127.0.0.1:8080/api/me\n";
}
int main()
{
App app;
install_middleware(app);
register_routes(app);
print_help();
app.run(8080);
return 0;
}Run it
Run:
vix run auth_jwt.cppThe server listens on:
http://127.0.0.1:8080Test the public route
curl -i http://127.0.0.1:8080/api/publicExpected body:
{
"ok": true,
"message": "public route"
}This route does not require a JWT.
Test the protected route without token
curl -i http://127.0.0.1:8080/api/meExpected status:
401 UnauthorizedThe middleware rejects the request before the handler runs.
The request is missing:
Authorization: Bearer <token>Test with a valid token
Store the token:
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0.3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo"Call the protected route:
curl -i \
-H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:8080/api/meExpected body shape:
{
"ok": true,
"subject": "user123",
"roles": ["admin"]
}The token is valid because it was signed with:
dev_secretand the middleware was installed with the same secret:
app.use("/api/me", middleware::app::jwt_dev(
"dev_secret"
));Test with an invalid token
curl -i \
-H "Authorization: Bearer invalid.token.value" \
http://127.0.0.1:8080/api/meExpected status:
401 UnauthorizedThe middleware rejects invalid tokens before the handler runs.
How it works
The JWT middleware is installed only on the protected route:
app.use("/api/me", middleware::app::jwt_dev(
"dev_secret"
));The middleware validates the token.
If the token is valid, it stores the decoded claims in request state:
vix::middleware::auth::JwtClaimsThe handler reads them:
auto &claims =
req.state<middleware::auth::JwtClaims>();Then it can return values such as:
claims.subject
claims.rolesAuthorization header
The client must send:
Authorization: Bearer <token>Example:
curl -i \
-H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:8080/api/meCommon mistakes:
missing Authorization header
missing Bearer prefix
extra quotes around the token
token signed with a different secret
expired token when expiration verification is enabledWhat is inside the token
The development token payload contains:
{
"sub": "user123",
"roles": ["admin"]
}The sub claim becomes:
claims.subjectThe roles claim becomes:
claims.rolesThe raw payload is also available through the claims object if you need custom fields.
Protect a route group
You can protect a whole group.
Example:
app.use("/api/private", middleware::app::jwt_dev(
"dev_secret"
));
app.get("/api/private/me", [](Request &req, Response &res)
{
auto &claims =
req.state<middleware::auth::JwtClaims>();
res.json({
"ok", true,
"subject", claims.subject
});
});Now every route under:
/api/privaterequires a valid JWT.
Protect one exact route
If only one route should be protected, install the middleware on that route prefix:
app.use("/api/me", middleware::app::jwt_dev(
"dev_secret"
));This keeps public routes public.
Example:
/api/public
no token required
/api/me
token requiredCustom JWT options
For more control, use JwtOptions directly and adapt the context middleware to App middleware.
middleware::auth::JwtOptions options;
options.secret = "dev_secret";
options.verify_exp = false;
app.use("/api/me", middleware::app::adapt_ctx(
middleware::auth::jwt(options)
));Use this when you want to configure JWT behavior explicitly.
The preset:
middleware::app::jwt_dev("dev_secret")is shorter and useful for examples.
Development preset
This example uses:
middleware::app::jwt_dev("dev_secret")The development preset is designed to make local examples simple.
For production, configure JWT intentionally.
Do not hardcode secrets in source code.
Use environment variables or secure configuration.
Production secret
Bad:
middleware::app::jwt_dev("dev_secret");Good shape:
const std::string jwt_secret =
cfg.getString("jwt.secret", "");
if (jwt_secret.empty())
{
throw std::runtime_error("jwt.secret is required");
}
middleware::auth::JwtOptions options;
options.secret = jwt_secret;
options.verify_exp = true;
app.use("/api/private", middleware::app::adapt_ctx(
middleware::auth::jwt(options)
));In production, secrets should come from:
environment variables
secret manager
deployment configurationnot from source code.
Expiration
JWTs commonly use the exp claim.
For local examples, expiration verification may be disabled.
For production, enable expiration verification:
middleware::auth::JwtOptions options;
options.secret = jwt_secret;
options.verify_exp = true;Then expired tokens should be rejected.
Use short token lifetimes for sensitive APIs.
JWT vs session
JWT and sessions solve related but different problems.
session
server stores state
browser stores session id cookie
JWT
token contains signed claims
server verifies signature
no server session store required for basic validationUse sessions when the server should own state.
Use JWT when clients or services pass signed claims.
For browser apps, sessions are often simpler.
For APIs, mobile apps, and service-to-service calls, JWT can be useful.
JWT vs API key
API keys identify applications or clients.
JWTs usually identify a user or principal with claims.
API key
simple client credential
JWT
signed token with claimsUse API keys for simple service access.
Use JWT when you need structured identity claims.
Middleware order
The example installs:
app.use("/api", middleware::app::request_id_dev());
app.use("/api", middleware::app::timing_dev());
app.use("/api", middleware::app::security_headers_dev());
app.use("/api/me", middleware::app::jwt_dev(
"dev_secret"
));The order means:
request id
identify request
timing
measure request
security headers
harden response
jwt
validate token and store claims
handler
reads JwtClaimsIf you use RBAC, install it after JWT because RBAC needs JWT claims.
JWT with RBAC
JWT authentication answers:
who is the caller?RBAC authorization answers:
what is the caller allowed to do?The order is:
jwt
rbac_context
require_role or require_perm
handlerExample shape:
app.use("/admin", middleware::app::jwt_dev("dev_secret"));
app.use("/admin", middleware::app::adapt_ctx(
middleware::auth::rbac_context()
));
app.use("/admin", middleware::app::adapt_ctx(
middleware::auth::require_role("admin")
));For a complete example, see examples/auth-rbac.md.
Error behavior
Typical JWT failures:
missing token
401 Unauthorized
invalid token
401 Unauthorized
bad signature
401 Unauthorized
expired token
401 Unauthorized when exp verification is enabledAuthorization failures such as missing role or permission belong to RBAC and usually return:
403 ForbiddenComplete test flow
Run:
vix run auth_jwt.cppPublic route:
curl -i http://127.0.0.1:8080/api/publicProtected route without token:
curl -i http://127.0.0.1:8080/api/meSet token:
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0.3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo"Protected route with token:
curl -i \
-H "Authorization: Bearer $TOKEN" \
http://127.0.0.1:8080/api/meInvalid token:
curl -i \
-H "Authorization: Bearer invalid.token.value" \
http://127.0.0.1:8080/api/meSummary
Use JWT middleware when a route needs a signed bearer token.
Install it:
app.use("/api/me", middleware::app::jwt_dev(
"dev_secret"
));Read claims:
auto &claims =
req.state<middleware::auth::JwtClaims>();Return identity data:
res.json({
"ok", true,
"subject", claims.subject,
"roles", claims.roles
});The mental model is:
client sends Bearer token
JWT middleware verifies token
middleware stores JwtClaims
handler reads claims
RBAC can enforce roles and permissions later