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

Authentication ​

This guide shows how to add authentication to a Vix REST API.

You will build:

  • POST /auth/register
  • POST /auth/login
  • GET /auth/me
  • protected routes
  • bearer token style authentication

This guide keeps the implementation simple and in-memory. For production, store users in SQLite or MySQL, hash passwords, and use real JWT/session middleware.

Goal ​

txt
client registers → client logs in → server returns token
→ client sends Authorization: Bearer <token> → server protects private routes

Setup ​

bash
vix new auth-api
cd auth-api
cpp
#include <vix.hpp>
#include <vix/validation.hpp>

Define the user model ​

cpp
struct User
{
  int id{};
  std::string email;
  std::string password;
  std::string role{"user"};
};

struct AppState
{
  std::vector<User> users;
  std::unordered_map<std::string, int> tokens;
};

Request body types ​

cpp
struct RegisterBody
{
  std::string email;
  std::string password;
  std::string confirm;

  static vix::validation::Schema<RegisterBody> schema()
  {
    using namespace vix::validation;
    return vix::validation::schema<RegisterBody>()
        .field("email", &RegisterBody::email, field<std::string>().required("email is required").email("invalid email format").length_max(120))

        .field("password", &RegisterBody::password, field<std::string>().required("password is required").length_min(8).length_max(64))

        .field("confirm", &RegisterBody::confirm, field<std::string>().required("confirm is required"))

        .check([](const RegisterBody &body, ValidationErrors &errors){
          if (!body.password.empty() && !body.confirm.empty() && body.password != body.confirm)
            errors.add("confirm", ValidationErrorCode::Custom, "passwords do not match");
        });
  }
};

struct LoginBody : vix::validation::BaseModel<LoginBody>
{
  std::string email;
  std::string password;

  static vix::validation::Schema<LoginBody> schema()
  {
    using namespace vix::validation;
    return vix::validation::schema<LoginBody>()
        .field("email", &LoginBody::email, field<std::string>().required("email is required").email("invalid email format"))

        .field("password", &LoginBody::password, field<std::string>().required("password is required").length_min(8));
  }
};

Helper functions ​

cpp
// Generate a simple token (use secure random tokens in production)
static std::string make_token(int user_id)
{
  return "dev-token-" + std::to_string(user_id);
}

// Read bearer token from request
static std::string bearer_token_from_request(Request &req)
{
  const std::string header = req.header("Authorization");
  const std::string prefix = "Bearer ";
  if (header.rfind(prefix, 0) != 0) return "";
  return header.substr(prefix.size());
}

// Find current user from token
static User *current_user(AppState &state, Request &req)
{
  const std::string token = bearer_token_from_request(req);
  if (token.empty()) return nullptr;
  auto it = state.tokens.find(token);
  if (it == state.tokens.end()) return nullptr;
  for (auto &user : state.users)
    if (user.id == it->second) return &user;
  return nullptr;
}

Auth routes ​

cpp
// POST /auth/register
app.post("/auth/register", [&state](Request &req, Response &res){
  // read body, validate, check duplicate, create user, return 201
});

// POST /auth/login
app.post("/auth/login", [&state](Request &req, Response &res){
  / validate, check credentials, create token, return token
});

// GET /auth/me
app.get("/auth/me", [&state](Request &req, Response &res){
  User *user = current_user(state, req);
  if (user == nullptr) {
    respond_error(res, 401, "unauthorized");
    return;
  }

  res.json(json::kv({
        {"ok", json::Json(true)},
        {"user",  json::kv({
                    {"id", json::Json(user->id)},
                    {"email", json::Json(user->email)},
                    {"role", json::Json(user->role)}
                  })
        }
      })
  );
});

Protecting routes ​

cpp
// Route-level protection
User *user = current_user(state, req);
if (user == nullptr) {
  respond_error(res, 401, "unauthorized");
  return;
}

// Role-based protection
if (user->role != "admin") {
  respond_error(res, 403, "admin role required");
  return;
}

Test the API ​

bash
# Health
curl -i http://127.0.0.1:8080/health

# Register
curl -i -X POST http://127.0.0.1:8080/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","password":"password123","confirm":"password123"}'

# Login
curl -i -X POST http://127.0.0.1:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"ada@example.com","password":"password123"}'

# Access protected route
curl -i http://127.0.0.1:8080/auth/me \
  -H "Authorization: Bearer dev-token-1"

# Access without token
curl -i http://127.0.0.1:8080/private

Authentication status codes ​

StatusMeaningExample
201Created.User registered successfully.
400Bad Request.Invalid input was provided.
401Unauthorized.Missing or invalid auth token.
403Forbidden.User is authenticated but denied.
409Conflict.Email is already registered.

Important production notes ​

For production: hash passwords, use secure random tokens, add token expiration, use HTTPS, use SQLite/MySQL persistence, rate limit login attempts.

Common mistakes ​

Forgetting Authorization: Bearer ​

bash
# Wrong
curl -i http://127.0.0.1:8080/auth/me -H "Authorization: dev-token-1"

# Correct
curl -i http://127.0.0.1:8080/auth/me -H "Authorization: Bearer dev-token-1"

Using 403 instead of 401 ​

Use 401 when not authenticated. Use 403 when authenticated but not allowed.

Forgetting to return after auth error ​

cpp
// Wrong
if (user == nullptr) {
  respond_error(res, 401, "unauthorized");
}

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

// Correct
if (user == nullptr) {
  respond_error(res, 401, "unauthorized");
  return;
}

What to use next ​

Released under the MIT License.