RBAC
This example shows how to protect Vix routes with role-based and permission-based authorization.
RBAC means:
Role-Based Access ControlUse RBAC when a route should be accessible only to users with specific roles or permissions.
Examples:
admin
seller
moderator
products:write
orders:read
users:deleteJWT authentication answers:
who is the caller?RBAC authorization answers:
what is the caller allowed to do?What this example builds
The app exposes:
GET /
GET /api/public
GET /api/me
GET /api/admin
GET /api/products/writeBehavior:
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:writeThe middleware order is:
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 passedHeader
For Vix v2.6.2 and newer, use:
#include <vix/middleware.hpp>For older v2.6.0 or v2.6.1, App integration headers may need explicit includes:
#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:
auth_rbac_demo/
└── auth_rbac.cppCreate the file:
mkdir auth_rbac_demo
cd auth_rbac_demo
touch auth_rbac.cppSource
Open:
auth_rbac.cppAdd:
#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:
vix run auth_rbac.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 authentication.
Set tokens
Admin token:
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiLCJvcmRlcnM6cmVhZCJdfQ.w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q"User token:
USER_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyNDU2Iiwicm9sZXMiOlsidXNlciJdLCJwZXJtcyI6WyJvcmRlcnM6cmVhZCJdfQ.1lcu1TtxMHllkoYc5mlneK7vKLLQDe0PxUtcfPG4XVM"The admin token contains:
{
"sub": "user123",
"roles": ["admin"],
"perms": ["products:write", "orders:read"]
}The user token contains:
{
"sub": "user456",
"roles": ["user"],
"perms": ["orders:read"]
}Test missing token
curl -i http://127.0.0.1:8080/api/meExpected status:
401 UnauthorizedThe request has no valid JWT, so RBAC cannot build an authorization context.
Test authenticated user
curl -i \
-H "Authorization: Bearer $USER_TOKEN" \
http://127.0.0.1:8080/api/meExpected body shape:
{
"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
curl -i \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://127.0.0.1:8080/api/adminExpected body shape:
{
"ok": true,
"message": "admin route accepted",
"subject": "user123",
"has_admin": true
}The admin token has:
role = adminso the request is accepted.
Test admin route with normal user token
curl -i \
-H "Authorization: Bearer $USER_TOKEN" \
http://127.0.0.1:8080/api/adminExpected status:
403 ForbiddenThe user is authenticated, but not authorized.
That is the difference between:
401 Unauthorized
authentication failed or missing
403 Forbidden
authentication passed, authorization failedTest permission route with admin token
curl -i \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://127.0.0.1:8080/api/products/writeExpected body shape:
{
"ok": true,
"message": "products:write permission accepted",
"subject": "user123",
"can_write_products": true
}The admin token has:
perm = products:writeso the request is accepted.
Test permission route with normal user token
curl -i \
-H "Authorization: Bearer $USER_TOKEN" \
http://127.0.0.1:8080/api/products/writeExpected status:
403 ForbiddenThe user token has:
orders:readbut not:
products:writeso the request is rejected.
How JWT and RBAC work together
JWT validates the token:
app.use("/api/admin", jwt_middleware());RBAC builds the authorization context from JWT claims:
app.use("/api/admin", rbac_context_middleware());Then the route requires a role:
app.use("/api/admin", require_admin_role());The full chain is:
request
-> JWT middleware
-> RBAC context middleware
-> require_role("admin")
-> handlerIf JWT fails, the handler does not run.
If RBAC fails, the handler does not run.
Build RBAC context
RBAC context is built with:
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:
middleware::auth::JwtClaimsand creates:
middleware::auth::AuthzThe handler can then read:
auto &authz =
req.state<middleware::auth::Authz>();Authz
Authz contains:
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:
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:
middleware::auth::require_role("admin")with App adaptation:
app.use("/api/admin", middleware::app::adapt_ctx(
middleware::auth::require_role("admin")
));This means the route requires:
role = adminRequire one permission
Use:
middleware::auth::require_perm("products:write")with App adaptation:
app.use("/api/products/write", middleware::app::adapt_ctx(
middleware::auth::require_perm("products:write")
));This means the route requires:
permission = products:writeRequire any role
Use:
middleware::auth::require_any_role({
"admin",
"moderator"
})This means the caller must have at least one of those roles.
Example:
app.use("/api/moderation", middleware::app::adapt_ctx(
middleware::auth::require_any_role({
"admin",
"moderator"
})
));Require any permission
Use:
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:
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:
admin
seller
support
moderatorExample permissions:
products:write
products:delete
orders:read
orders:refund
users:banA practical design is:
roles
describe who the user is
permissions
describe what the user can doFor critical routes, permissions are often clearer than roles.
Claims accepted by RBAC
RBAC reads roles from the JWT payload.
Common keys:
roles
rolePermissions can come from:
perms
permissions
scopeA scope string can contain space-separated permissions.
Example payload:
{
"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:
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:
load user roles from database
load team permissions
load tenant-specific permissions
merge JWT claims with stored permissionsFor this example:
options.use_resolver = false;That keeps the example simple and fully token-based.
Production secret
The example uses:
dev_secretDo not hardcode production secrets.
Production 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 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:
environment variables
secret manager
deployment configurationMiddleware order
The order must be correct.
Good:
app.use("/api/admin", jwt_middleware());
app.use("/api/admin", rbac_context_middleware());
app.use("/api/admin", require_admin_role());Wrong:
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:
401 Unauthorized
no valid identity
403 Forbidden
identity exists, but access is not allowedExamples:
missing JWT
401
invalid JWT
401
valid JWT but missing admin role
403
valid JWT but missing products:write permission
403This makes API behavior easier to debug.
Protect route groups
You can protect a route group:
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:
/api/admin
/api/admin/users
/api/admin/settingsIf 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:
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:
role = seller
permission = products:publishKeep handlers simple
Prefer this:
app.use("/api/admin", require_admin_role());
app.get("/api/admin", [](Request &, Response &res)
{
res.json({
"ok", true
});
});over this:
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:
app.use("/admin", rbac_context_middleware());
app.use("/admin", jwt_middleware());Correct:
app.use("/admin", jwt_middleware());
app.use("/admin", rbac_context_middleware());Forgetting RBAC context
Wrong:
app.use("/admin", jwt_middleware());
app.use("/admin", require_admin_role());Correct:
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:
products_write_user
orders_read_user
users_delete_userBetter:
role
seller
permissions
products:write
products:publishTrusting 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:
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 routesFor enterprise apps, also consider tenant-aware permissions:
tenant:123:products:write
tenant:123:orders:read
tenant:456:products:writeKeep the permission format consistent.
Complete test flow
Run:
vix run auth_rbac.cppSet tokens:
ADMIN_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZXMiOlsiYWRtaW4iXSwicGVybXMiOlsicHJvZHVjdHM6d3JpdGUiLCJvcmRlcnM6cmVhZCJdfQ.w1y3nA2F1kq0oJ0x8wWc5wQx8zF4h2d6V7mYp0jYk3Q"
USER_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyNDU2Iiwicm9sZXMiOlsidXNlciJdLCJwZXJtcyI6WyJvcmRlcnM6cmVhZCJdfQ.1lcu1TtxMHllkoYc5mlneK7vKLLQDe0PxUtcfPG4XVM"Public:
curl -i http://127.0.0.1:8080/api/publicMissing token:
curl -i http://127.0.0.1:8080/api/meAuthenticated user:
curl -i \
-H "Authorization: Bearer $USER_TOKEN" \
http://127.0.0.1:8080/api/meAdmin accepted:
curl -i \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://127.0.0.1:8080/api/adminAdmin rejected:
curl -i \
-H "Authorization: Bearer $USER_TOKEN" \
http://127.0.0.1:8080/api/adminPermission accepted:
curl -i \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://127.0.0.1:8080/api/products/writePermission rejected:
curl -i \
-H "Authorization: Bearer $USER_TOKEN" \
http://127.0.0.1:8080/api/products/writeSummary
RBAC protects routes after JWT authentication.
Install JWT first:
app.use("/api/admin", jwt_middleware());Build authorization context:
app.use("/api/admin", rbac_context_middleware());Require a role:
app.use("/api/admin", middleware::app::adapt_ctx(
middleware::auth::require_role("admin")
));Require a permission:
app.use("/api/products/write", middleware::app::adapt_ctx(
middleware::auth::require_perm("products:write")
));The mental model is:
JWT
proves identity
RBAC context
extracts roles and permissions
require_role / require_perm
enforces access
handler
runs only when access is allowed