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

RBAC ​

This example shows how to protect Vix routes with role-based and permission-based authorization.

RBAC means:

txt
Role-Based Access Control

Use RBAC when a route should be accessible only to users with specific roles or permissions.

Examples:

txt
admin
seller
moderator
products:write
orders:read
users:delete

JWT authentication answers:

txt
who is the caller?

RBAC authorization answers:

txt
what is the caller allowed to do?

What this example builds ​

The app exposes:

txt
GET /
GET /api/public
GET /api/me
GET /api/admin
GET /api/products/write

Behavior:

txt
GET /api/public
  public route

GET /api/me
  requires valid JWT

GET /api/admin
  requires valid JWT and role admin

GET /api/products/write
  requires valid JWT and permission products:write

The middleware order is:

txt
JWT
  validates the token and stores JwtClaims

RBAC context
  builds Authz from JwtClaims

require_role or require_perm
  enforces authorization

handler
  runs only if authorization passed

For Vix v2.6.2 and newer, use:

cpp
#include <vix/middleware.hpp>

For older v2.6.0 or v2.6.1, App integration headers may need explicit includes:

cpp
#include <vix/middleware/app/adapter.hpp>
#include <vix/middleware/auth/jwt.hpp>
#include <vix/middleware/auth/rbac.hpp>

This example uses the modern public entry point.

Project structure ​

Create:

txt
auth_rbac_demo/
└── auth_rbac.cpp

Create the file:

bash
mkdir auth_rbac_demo
cd auth_rbac_demo
touch auth_rbac.cpp

Source ​

Open:

txt
auth_rbac.cpp

Add:

cpp
#include <iostream>
#include <string>

#include <vix.hpp>
#include <vix/middleware.hpp>

using namespace vix;

static const std::string kAdminToken =
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
  "eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiLCJvcmRlcnM6cmVhZCJdfQ."
  "w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q";

static const std::string kUserToken =
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
  "eyJzdWIiOiJ1c2VyNDU2Iiwicm9sZXMiOlsidXNlciJdLCJwZXJtcyI6WyJvcmRlcnM6cmVhZCJdfQ."
  "1lcu1TtxMHllkoYc5mlneK7vKLLQDe0PxUtcfPG4XVM";

static vix::App::Middleware jwt_middleware()
{
  middleware::auth::JwtOptions options;

  options.secret = "dev_secret";
  options.verify_exp = false;

  return middleware::app::adapt_ctx(
    middleware::auth::jwt(options)
  );
}

static vix::App::Middleware rbac_context_middleware()
{
  middleware::auth::RbacOptions options;

  options.require_auth = true;
  options.use_resolver = false;

  return middleware::app::adapt_ctx(
    middleware::auth::rbac_context(options)
  );
}

static vix::App::Middleware require_admin_role()
{
  return middleware::app::adapt_ctx(
    middleware::auth::require_role("admin")
  );
}

static vix::App::Middleware require_products_write()
{
  return middleware::app::adapt_ctx(
    middleware::auth::require_perm("products:write")
  );
}

static void install_common_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());
}

static void install_auth_middleware(App &app)
{
  app.use("/api/me", jwt_middleware());
  app.use("/api/me", rbac_context_middleware());

  app.use("/api/admin", jwt_middleware());
  app.use("/api/admin", rbac_context_middleware());
  app.use("/api/admin", require_admin_role());

  app.use("/api/products/write", jwt_middleware());
  app.use("/api/products/write", rbac_context_middleware());
  app.use("/api/products/write", require_products_write());
}

static void register_routes(App &app)
{
  app.get("/", [](Request &, Response &res)
  {
    res.send(
      "RBAC 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"
      "\n"
      "Admin:\n"
      "  curl -i -H \"Authorization: Bearer <ADMIN_TOKEN>\" http://127.0.0.1:8080/api/admin\n"
      "\n"
      "Permission:\n"
      "  curl -i -H \"Authorization: Bearer <ADMIN_TOKEN>\" http://127.0.0.1:8080/api/products/write\n"
    );
  });

  app.get("/api/public", [](Request &, Response &res)
  {
    res.json({
      "ok", true,
      "message", "public route"
    });
  });

  app.get("/api/me", [](Request &req, Response &res)
  {
    auto &authz =
      req.state<middleware::auth::Authz>();

    res.json({
      "ok", true,
      "subject", authz.subject,
      "has_admin", authz.has_role("admin"),
      "can_write_products", authz.has_perm("products:write")
    });
  });

  app.get("/api/admin", [](Request &req, Response &res)
  {
    auto &authz =
      req.state<middleware::auth::Authz>();

    res.json({
      "ok", true,
      "message", "admin route accepted",
      "subject", authz.subject,
      "has_admin", authz.has_role("admin")
    });
  });

  app.get("/api/products/write", [](Request &req, Response &res)
  {
    auto &authz =
      req.state<middleware::auth::Authz>();

    res.json({
      "ok", true,
      "message", "products:write permission accepted",
      "subject", authz.subject,
      "can_write_products", authz.has_perm("products:write")
    });
  });
}

static void print_help()
{
  std::cout
    << "Vix RBAC 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"
    << "  http://127.0.0.1:8080/api/admin\n"
    << "  http://127.0.0.1:8080/api/products/write\n\n"
    << "Admin token:\n"
    << "  " << kAdminToken << "\n\n"
    << "User token:\n"
    << "  " << kUserToken << "\n\n";
}

int main()
{
  App app;

  install_common_middleware(app);
  install_auth_middleware(app);
  register_routes(app);

  print_help();

  app.run(8080);
  return 0;
}

Run it ​

Run:

bash
vix run auth_rbac.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 authentication.

Set tokens ​

Admin token:

bash
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiLCJvcmRlcnM6cmVhZCJdfQ.w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q"

User token:

bash
USER_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyNDU2Iiwicm9sZXMiOlsidXNlciJdLCJwZXJtcyI6WyJvcmRlcnM6cmVhZCJdfQ.1lcu1TtxMHllkoYc5mlneK7vKLLQDe0PxUtcfPG4XVM"

The admin token contains:

json
{
  "sub": "user123",
  "roles": ["admin"],
  "perms": ["products:write", "orders:read"]
}

The user token contains:

json
{
  "sub": "user456",
  "roles": ["user"],
  "perms": ["orders:read"]
}

Test missing token ​

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

Expected status:

txt
401 Unauthorized

The request has no valid JWT, so RBAC cannot build an authorization context.

Test authenticated user ​

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

Expected body shape:

json
{
  "ok": true,
  "subject": "user456",
  "has_admin": false,
  "can_write_products": false
}

This route only requires a valid JWT.

It does not require a specific role or permission.

Test admin route with admin token ​

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

Expected body shape:

json
{
  "ok": true,
  "message": "admin route accepted",
  "subject": "user123",
  "has_admin": true
}

The admin token has:

txt
role = admin

so the request is accepted.

Test admin route with normal user token ​

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

Expected status:

txt
403 Forbidden

The user is authenticated, but not authorized.

That is the difference between:

txt
401 Unauthorized
  authentication failed or missing

403 Forbidden
  authentication passed, authorization failed

Test permission route with admin token ​

bash
curl -i \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://127.0.0.1:8080/api/products/write

Expected body shape:

json
{
  "ok": true,
  "message": "products:write permission accepted",
  "subject": "user123",
  "can_write_products": true
}

The admin token has:

txt
perm = products:write

so the request is accepted.

Test permission route with normal user token ​

bash
curl -i \
  -H "Authorization: Bearer $USER_TOKEN" \
  http://127.0.0.1:8080/api/products/write

Expected status:

txt
403 Forbidden

The user token has:

txt
orders:read

but not:

txt
products:write

so the request is rejected.

How JWT and RBAC work together ​

JWT validates the token:

cpp
app.use("/api/admin", jwt_middleware());

RBAC builds the authorization context from JWT claims:

cpp
app.use("/api/admin", rbac_context_middleware());

Then the route requires a role:

cpp
app.use("/api/admin", require_admin_role());

The full chain is:

txt
request
  -> JWT middleware
  -> RBAC context middleware
  -> require_role("admin")
  -> handler

If JWT fails, the handler does not run.

If RBAC fails, the handler does not run.

Build RBAC context ​

RBAC context is built with:

cpp
middleware::auth::RbacOptions options;

options.require_auth = true;
options.use_resolver = false;

app.use("/api/me", middleware::app::adapt_ctx(
  middleware::auth::rbac_context(options)
));

The middleware reads:

cpp
middleware::auth::JwtClaims

and creates:

cpp
middleware::auth::Authz

The handler can then read:

cpp
auto &authz =
  req.state<middleware::auth::Authz>();

Authz ​

Authz contains:

cpp
struct Authz
{
  std::string subject;
  std::unordered_set<std::string> roles;
  std::unordered_set<std::string> perms;

  bool has_role(std::string_view r) const;
  bool has_perm(std::string_view p) const;
};

Typical usage:

cpp
auto &authz =
  req.state<middleware::auth::Authz>();

if (authz.has_role("admin"))
{
  // admin logic
}

if (authz.has_perm("products:write"))
{
  // write product logic
}

Most of the time, prefer middleware checks instead of manual checks inside handlers.

Require one role ​

Use:

cpp
middleware::auth::require_role("admin")

with App adaptation:

cpp
app.use("/api/admin", middleware::app::adapt_ctx(
  middleware::auth::require_role("admin")
));

This means the route requires:

txt
role = admin

Require one permission ​

Use:

cpp
middleware::auth::require_perm("products:write")

with App adaptation:

cpp
app.use("/api/products/write", middleware::app::adapt_ctx(
  middleware::auth::require_perm("products:write")
));

This means the route requires:

txt
permission = products:write

Require any role ​

Use:

cpp
middleware::auth::require_any_role({
  "admin",
  "moderator"
})

This means the caller must have at least one of those roles.

Example:

cpp
app.use("/api/moderation", middleware::app::adapt_ctx(
  middleware::auth::require_any_role({
    "admin",
    "moderator"
  })
));

Require any permission ​

Use:

cpp
middleware::auth::require_any_perm({
  "products:write",
  "products:manage"
})

This means the caller must have at least one of those permissions.

Require all permissions ​

Use:

cpp
middleware::auth::require_all_perms({
  "products:write",
  "products:publish"
})

This means the caller must have every listed permission.

Use this for sensitive operations that require several capabilities.

Roles vs permissions ​

Roles are broad.

Permissions are specific.

Example roles:

txt
admin
seller
support
moderator

Example permissions:

txt
products:write
products:delete
orders:read
orders:refund
users:ban

A practical design is:

txt
roles
  describe who the user is

permissions
  describe what the user can do

For critical routes, permissions are often clearer than roles.

Claims accepted by RBAC ​

RBAC reads roles from the JWT payload.

Common keys:

txt
roles
role

Permissions can come from:

txt
perms
permissions
scope

A scope string can contain space-separated permissions.

Example payload:

json
{
  "sub": "user123",
  "roles": ["admin"],
  "perms": ["products:write", "orders:read"],
  "scope": "products:write orders:read"
}

Permission resolver ​

RBAC can also enrich roles and permissions through a resolver.

The interface is:

cpp
struct PermissionResolver
{
  virtual ~PermissionResolver() = default;

  virtual void resolve(
    std::string_view subject,
    std::unordered_set<std::string> &roles_inout,
    std::unordered_set<std::string> &perms_inout) = 0;
};

Use a resolver when permissions should come from a database or external service.

Example use cases:

txt
load user roles from database
load team permissions
load tenant-specific permissions
merge JWT claims with stored permissions

For this example:

cpp
options.use_resolver = false;

That keeps the example simple and fully token-based.

Production secret ​

The example uses:

txt
dev_secret

Do not hardcode production secrets.

Production 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 jwt_options;

jwt_options.secret = jwt_secret;
jwt_options.verify_exp = true;

app.use("/api/private", middleware::app::adapt_ctx(
  middleware::auth::jwt(jwt_options)
));

Use secrets from:

txt
environment variables
secret manager
deployment configuration

Middleware order ​

The order must be correct.

Good:

cpp
app.use("/api/admin", jwt_middleware());
app.use("/api/admin", rbac_context_middleware());
app.use("/api/admin", require_admin_role());

Wrong:

cpp
app.use("/api/admin", require_admin_role());
app.use("/api/admin", jwt_middleware());
app.use("/api/admin", rbac_context_middleware());

RBAC requirements need the Authz state.

Authz needs JWT claims.

JWT must run first.

401 vs 403 ​

Use this model:

txt
401 Unauthorized
  no valid identity

403 Forbidden
  identity exists, but access is not allowed

Examples:

txt
missing JWT
  401

invalid JWT
  401

valid JWT but missing admin role
  403

valid JWT but missing products:write permission
  403

This makes API behavior easier to debug.

Protect route groups ​

You can protect a route group:

cpp
app.use("/api/admin", jwt_middleware());
app.use("/api/admin", rbac_context_middleware());
app.use("/api/admin", require_admin_role());

Then these routes are protected:

txt
/api/admin
/api/admin/users
/api/admin/settings

If you want a public route under the same prefix, use a different prefix or install middleware more narrowly.

Combine role and permission ​

For sensitive routes, you can require both:

cpp
app.use("/api/products/publish", jwt_middleware());
app.use("/api/products/publish", rbac_context_middleware());

app.use("/api/products/publish", middleware::app::adapt_ctx(
  middleware::auth::require_role("seller")
));

app.use("/api/products/publish", middleware::app::adapt_ctx(
  middleware::auth::require_perm("products:publish")
));

This means the caller must have:

txt
role = seller
permission = products:publish

Keep handlers simple ​

Prefer this:

cpp
app.use("/api/admin", require_admin_role());

app.get("/api/admin", [](Request &, Response &res)
{
  res.json({
    "ok", true
  });
});

over this:

cpp
app.get("/api/admin", [](Request &req, Response &res)
{
  auto &authz = req.state<middleware::auth::Authz>();

  if (!authz.has_role("admin"))
  {
    res.status(403).json({
      "ok", false
    });
    return;
  }

  res.json({
    "ok", true
  });
});

Authorization middleware keeps route handlers focused on business logic.

Common mistakes ​

Installing RBAC before JWT ​

Wrong:

cpp
app.use("/admin", rbac_context_middleware());
app.use("/admin", jwt_middleware());

Correct:

cpp
app.use("/admin", jwt_middleware());
app.use("/admin", rbac_context_middleware());

Forgetting RBAC context ​

Wrong:

cpp
app.use("/admin", jwt_middleware());
app.use("/admin", require_admin_role());

Correct:

cpp
app.use("/admin", jwt_middleware());
app.use("/admin", rbac_context_middleware());
app.use("/admin", require_admin_role());

require_role needs Authz.

Authz is created by rbac_context.

Confusing role and permission ​

Avoid making roles too specific.

Bad role design:

txt
products_write_user
orders_read_user
users_delete_user

Better:

txt
role
  seller

permissions
  products:write
  products:publish

Trusting client-side UI only ​

Hiding a button in the frontend is not authorization.

The backend must enforce roles and permissions.

Hardcoding production secrets ​

Use configuration for secrets.

Do not commit production secrets to the repository.

Production notes ​

For production RBAC, consider:

txt
short-lived JWTs
expiration verification
issuer validation
audience validation
secret rotation
role and permission design
database-backed permission resolver
audit logs for sensitive actions
tests for protected routes

For enterprise apps, also consider tenant-aware permissions:

txt
tenant:123:products:write
tenant:123:orders:read
tenant:456:products:write

Keep the permission format consistent.

Complete test flow ​

Run:

bash
vix run auth_rbac.cpp

Set tokens:

bash
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiLCJvcmRlcnM6cmVhZCJdfQ.w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q"

USER_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyNDU2Iiwicm9sZXMiOlsidXNlciJdLCJwZXJtcyI6WyJvcmRlcnM6cmVhZCJdfQ.1lcu1TtxMHllkoYc5mlneK7vKLLQDe0PxUtcfPG4XVM"

Public:

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

Missing token:

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

Authenticated user:

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

Admin accepted:

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

Admin rejected:

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

Permission accepted:

bash
curl -i \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://127.0.0.1:8080/api/products/write

Permission rejected:

bash
curl -i \
  -H "Authorization: Bearer $USER_TOKEN" \
  http://127.0.0.1:8080/api/products/write

Summary ​

RBAC protects routes after JWT authentication.

Install JWT first:

cpp
app.use("/api/admin", jwt_middleware());

Build authorization context:

cpp
app.use("/api/admin", rbac_context_middleware());

Require a role:

cpp
app.use("/api/admin", middleware::app::adapt_ctx(
  middleware::auth::require_role("admin")
));

Require a permission:

cpp
app.use("/api/products/write", middleware::app::adapt_ctx(
  middleware::auth::require_perm("products:write")
));

The mental model is:

txt
JWT
  proves identity

RBAC context
  extracts roles and permissions

require_role / require_perm
  enforces access

handler
  runs only when access is allowed

Released under the MIT License.