Vix.cpp v2.6.0 is here Read the blog
Skip to content

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:

txt
subject
roles
permissions
expiration
custom payload fields

This example uses a development HS256 token signed with:

txt
secret = dev_secret

The route /api/me requires:

txt
Authorization: Bearer <token>

What this example builds

The app exposes:

txt
GET /
GET /api/public
GET /api/me

Behavior:

txt
GET /
  public help page

GET /api/public
  public JSON route

GET /api/me
  protected route, requires a valid JWT

The JWT middleware validates the token and stores claims in request state.

The handler reads:

cpp
vix::middleware::auth::JwtClaims

For Vix v2.6.2 and newer, use:

cpp
#include <vix/middleware.hpp>

For older v2.6.0 or v2.6.1, App presets may need an explicit include:

cpp
#include <vix/middleware/app/presets.hpp>

This example uses the modern public entry point.

Project structure

Create:

txt
auth_jwt_demo/
└── auth_jwt.cpp

Create the file:

bash
mkdir auth_jwt_demo
cd auth_jwt_demo
touch auth_jwt.cpp

Source

Open:

txt
auth_jwt.cpp

Add:

cpp
#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:

bash
vix run auth_jwt.cpp

The server listens on:

txt
http://127.0.0.1:8080

Test the public route

bash
curl -i http://127.0.0.1:8080/api/public

Expected body:

json
{
  "ok": true,
  "message": "public route"
}

This route does not require a JWT.

Test the protected route without token

bash
curl -i http://127.0.0.1:8080/api/me

Expected status:

txt
401 Unauthorized

The middleware rejects the request before the handler runs.

The request is missing:

txt
Authorization: Bearer <token>

Test with a valid token

Store the token:

bash
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0.3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo"

Call the protected route:

bash
curl -i \
  -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:8080/api/me

Expected body shape:

json
{
  "ok": true,
  "subject": "user123",
  "roles": ["admin"]
}

The token is valid because it was signed with:

txt
dev_secret

and the middleware was installed with the same secret:

cpp
app.use("/api/me", middleware::app::jwt_dev(
  "dev_secret"
));

Test with an invalid token

bash
curl -i \
  -H "Authorization: Bearer invalid.token.value" \
  http://127.0.0.1:8080/api/me

Expected status:

txt
401 Unauthorized

The middleware rejects invalid tokens before the handler runs.

How it works

The JWT middleware is installed only on the protected route:

cpp
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:

cpp
vix::middleware::auth::JwtClaims

The handler reads them:

cpp
auto &claims =
  req.state<middleware::auth::JwtClaims>();

Then it can return values such as:

cpp
claims.subject
claims.roles

Authorization header

The client must send:

txt
Authorization: Bearer <token>

Example:

bash
curl -i \
  -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:8080/api/me

Common mistakes:

txt
missing Authorization header
missing Bearer prefix
extra quotes around the token
token signed with a different secret
expired token when expiration verification is enabled

What is inside the token

The development token payload contains:

json
{
  "sub": "user123",
  "roles": ["admin"]
}

The sub claim becomes:

cpp
claims.subject

The roles claim becomes:

cpp
claims.roles

The 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:

cpp
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:

txt
/api/private

requires a valid JWT.

Protect one exact route

If only one route should be protected, install the middleware on that route prefix:

cpp
app.use("/api/me", middleware::app::jwt_dev(
  "dev_secret"
));

This keeps public routes public.

Example:

txt
/api/public
  no token required

/api/me
  token required

Custom JWT options

For more control, use JwtOptions directly and adapt the context middleware to App middleware.

cpp
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:

cpp
middleware::app::jwt_dev("dev_secret")

is shorter and useful for examples.

Development preset

This example uses:

cpp
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:

cpp
middleware::app::jwt_dev("dev_secret");

Good shape:

cpp
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:

txt
environment variables
secret manager
deployment configuration

not from source code.

Expiration

JWTs commonly use the exp claim.

For local examples, expiration verification may be disabled.

For production, enable expiration verification:

cpp
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.

txt
session
  server stores state
  browser stores session id cookie

JWT
  token contains signed claims
  server verifies signature
  no server session store required for basic validation

Use 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.

txt
API key
  simple client credential

JWT
  signed token with claims

Use API keys for simple service access.

Use JWT when you need structured identity claims.

Middleware order

The example installs:

cpp
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:

txt
request id
  identify request

timing
  measure request

security headers
  harden response

jwt
  validate token and store claims

handler
  reads JwtClaims

If you use RBAC, install it after JWT because RBAC needs JWT claims.

JWT with RBAC

JWT authentication answers:

txt
who is the caller?

RBAC authorization answers:

txt
what is the caller allowed to do?

The order is:

txt
jwt
rbac_context
require_role or require_perm
handler

Example shape:

cpp
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:

txt
missing token
  401 Unauthorized

invalid token
  401 Unauthorized

bad signature
  401 Unauthorized

expired token
  401 Unauthorized when exp verification is enabled

Authorization failures such as missing role or permission belong to RBAC and usually return:

txt
403 Forbidden

Complete test flow

Run:

bash
vix run auth_jwt.cpp

Public route:

bash
curl -i http://127.0.0.1:8080/api/public

Protected route without token:

bash
curl -i http://127.0.0.1:8080/api/me

Set token:

bash
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXX0.3HK5b1sXMbxkjC3Tllwtcuzxm-1OI0D184Fuav0-XQo"

Protected route with token:

bash
curl -i \
  -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:8080/api/me

Invalid token:

bash
curl -i \
  -H "Authorization: Bearer invalid.token.value" \
  http://127.0.0.1:8080/api/me

Summary

Use JWT middleware when a route needs a signed bearer token.

Install it:

cpp
app.use("/api/me", middleware::app::jwt_dev(
  "dev_secret"
));

Read claims:

cpp
auto &claims =
  req.state<middleware::auth::JwtClaims>();

Return identity data:

cpp
res.json({
  "ok", true,
  "subject", claims.subject,
  "roles", claims.roles
});

The mental model is:

txt
client sends Bearer token
JWT middleware verifies token
middleware stores JwtClaims
handler reads claims
RBAC can enforce roles and permissions later

Released under the MIT License.