API Library

The Api library provides helper methods for building secure RESTful APIs in LavaLust. It supports:

  • CORS headers (with single origin or array of allowed origins)

  • Parsing and sanitizing JSON/form bodies

  • Standard API responses

  • JWT-based authentication (access/refresh token system)

  • Basic authentication

  • Rate limiting (with cache-backed sliding window)

  • Refresh token hashing and rotation

This library is automatically configured using values from app/config/api.php.

Folder Structure

app/
├── controllers/
│   └── ApiController.php
│
└── config/
    └── api.php

Configuration

Set the following values in app/config/api.php:

<?php
$config['api_helper_enabled']       = TRUE;

// Token expiration (seconds)
$config['payload_token_expiration'] = 900;       // 15 minutes
$config['refresh_token_expiration'] = 604800;    // 7 days

// Signing keys (minimum 32 characters each)
$config['jwt_secret']               = 'your-32-char-minimum-secret-key!!';
$config['refresh_token_key']        = 'your-32-char-minimum-refresh-key!';

// JWT claims
$config['jwt_issuer']               = 'https://yourdomain.com';
$config['jwt_audience']             = 'https://yourdomain.com';

// CORS: single origin or array of allowed origins
$config['allow_origin']             = '*';
// $config['allow_origin']          = ['https://app.example.com', 'https://admin.example.com'];

// Refresh token table
$config['refresh_token_table']      = 'refresh_tokens';

// Rate limiting
$config['rate_limit_enabled']       = TRUE;
$config['rate_limit_requests']      = 60;    // max requests per window
$config['rate_limit_seconds']       = 60;    // window size in seconds

Note

jwt_secret and refresh_token_key must each be at least 32 characters long. The library will call show_error() on startup if either key is missing or too short.

Properties

Property

Type

Description

$_lava

object

LavaLust super object (via lava_instance())

$refresh_token_table

string

Database table name for storing refresh tokens

$payload_token_expiration

integer

Lifetime of access (payload) tokens in seconds

$refresh_token_expiration

integer

Lifetime of refresh tokens in seconds

$allow_origin

string|array

Allowed origin(s) for CORS. Accepts '*', a single origin string, or an array of origins.

$jwt_secret

string

Secret key used for signing JWT tokens (min 32 chars)

$refresh_token_key

string

Key used for HMAC-hashing refresh tokens before DB storage (min 32 chars)

$jwt_issuer

string

iss claim added to every JWT

$jwt_audience

string

aud claim added to every JWT and validated on decode

$rate_limit_enabled

boolean

Enable or disable rate limiting globally

$rate_limit_requests

integer

Maximum number of requests allowed per window

$rate_limit_seconds

integer

Rate limit window size in seconds

Methods

CORS & Request Helpers

Method

Description

handle_cors()

Sets CORS headers. Called automatically in the constructor. Supports *, a single origin, or an array of allowed origins.

body(): array

Parses and sanitizes JSON or form-encoded request body. Returns an array.

get_query_params(): array

Returns sanitized URL query parameters ($_GET) as an associative array.

require_method(string $method)

Enforces a specific HTTP method. Sends 405 Method Not Allowed if not matched.

Responses

Method

Description

respond(mixed $data, int $code = 200)

Sends a JSON response with the given HTTP status code and exits.

respond_error(string $message, int $code = 400)

Sends a JSON error response with error and status fields.

Rate Limiting

Method

Description

rate_limit(?string $key, ?int $requests, ?int $seconds)

Enforces a rate limit using the cache library. Falls back to config defaults when parameters are null. Uses the client IP as key if $key is null. Sets X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset response headers. Responds with 429 Too Many Requests when the limit is exceeded.

JWT Authentication

Method

Description

encode_jwt(array $payload): string

Encodes a payload into a signed HS256 JWT. Automatically adds iat, exp, iss, aud, and a unique jti claim.

decode_jwt(string $token): array|null

Decodes and verifies the signature of a JWT. Returns the payload array or null on failure.

validate_jwt(string $token): array|null

Decodes the JWT and additionally validates exp, iat, iss, and aud claims. Returns the payload or null.

get_bearer_token(): ?string

Extracts a JWT bearer token from the Authorization header (supports Apache apache_request_headers() fallback).

require_jwt(): array

Validates the current bearer token and returns its payload. Responds with 401 Unauthorized on failure.

Token System

Method

Description

issue_tokens(array $user_data): array

Issues an access token and a refresh token. The refresh token is HMAC-hashed before being stored in the database. Cleans up expired tokens for the user before inserting. Returns access_token, refresh_token, expires_in, and token_type.

refresh_access_token(string $refresh_token)

Validates the refresh token, revokes it, and issues a new token pair (rotation). Responds directly with the new tokens.

revoke_refresh_token(string $refresh_token)

HMAC-hashes the token and deletes the matching row from the database.

cleanup_expired_refresh_tokens(?int $user_id)

Deletes all expired refresh tokens. If $user_id is provided, only that user’s tokens are cleaned up.

Basic Authentication

Method

Description

check_basic_auth(string $user, string $pass): bool

Verifies HTTP Basic Auth credentials using constant-time comparison.

require_basic_auth(string $user, string $pass)

Requires valid basic auth credentials. Sends 401 Unauthorized with a WWW-Authenticate header if credentials do not match.

Database Structure

Table: ``users``

Column

Type

Null

Default

Description

id

INT(11) PK AI

NO

AUTO_INCREMENT

Primary key

username

VARCHAR(50) UNIQUE

NO

Username of the user

email

VARCHAR(100) UNIQUE

NO

Email address of the user

password

VARCHAR(255)

NO

Hashed password

role

VARCHAR(20)

YES

‘user’

User role (default: user)

created_at

TIMESTAMP

YES

CURRENT_TIMESTAMP

Record creation time

Table: ``refresh_tokens``

Column

Type

Description

id

INT PK AI

Primary key

user_id

INT

Associated user ID

token

TEXT

HMAC-hashed refresh token (never stored in plain text)

expires_at

DATETIME

Expiration date of the refresh token

jti

VARCHAR(64)

Unique JWT ID from the refresh token payload

Controller Example

Routes

<?php
$router->post('login',          'ApiController::login');
$router->post('logout',         'ApiController::logout');
$router->post('create',         'ApiController::create');
$router->put('update/{id}',     'ApiController::update');
$router->delete('delete/{id}',  'ApiController::delete');
$router->get('list',            'ApiController::list');
$router->get('profile',         'ApiController::profile');
$router->post('refresh',        'ApiController::refresh');

Controller

<?php
class ApiController extends Controller
{
    public function login()
    {
        $this->api->require_method('POST');
        $input    = $this->api->body();
        $username = $input['username'] ?? '';
        $password = $input['password'] ?? '';

        $stmt = $this->db->raw('SELECT * FROM users WHERE username = ?', [$username]);
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($user && password_verify($password, $user['password'])) {
            $tokens = $this->api->issue_tokens([
                'id'   => $user['id'],
                'role' => $user['role'],
            ]);
            $this->api->respond($tokens);
        } else {
            $this->api->respond_error('Invalid credentials', 401);
        }
    }

    public function logout()
    {
        $this->api->require_method('POST');
        $input = $this->api->body();
        $this->api->revoke_refresh_token($input['refresh_token'] ?? '');
        $this->api->respond(['message' => 'Logged out']);
    }

    public function list()
    {
        // Apply rate limiting before processing
        $this->api->rate_limit();

        $users = $this->db->table('users')
                          ->select('id, username, email, role, created_at')
                          ->get_all();
        $this->api->respond($users);
    }

    public function create()
    {
        $this->api->require_method('POST');
        $input = $this->api->body();

        $this->db->raw(
            "INSERT INTO users (username, email, password, role, created_at)
             VALUES (?, ?, ?, ?, NOW())",
            [
                $input['username'],
                $input['email'],
                password_hash($input['password'], PASSWORD_BCRYPT),
                $input['role'] ?? 'user',
            ]
        );

        $this->api->respond(['message' => 'User created'], 201);
    }

    public function update($id)
    {
        $this->api->require_method('PUT');
        $input = $this->api->body();

        $this->db->raw(
            "UPDATE users SET username = ?, email = ?, role = ? WHERE id = ?",
            [$input['username'], $input['email'], $input['role'], $id]
        );

        $this->api->respond(['message' => 'User updated']);
    }

    public function delete($id)
    {
        $this->api->require_method('DELETE');
        $this->db->raw("DELETE FROM users WHERE id = ?", [$id]);
        $this->api->respond(['message' => 'User deleted']);
    }

    public function profile()
    {
        $auth = $this->api->require_jwt();

        $stmt = $this->db->raw(
            "SELECT id, username, email, role, created_at FROM users WHERE id = ?",
            [$auth['sub']]
        );
        $user = $stmt->fetch(PDO::FETCH_ASSOC);

        $this->api->respond($user ?: ['message' => 'User not found']);
    }

    public function refresh()
    {
        $this->api->require_method('POST');
        $input = $this->api->body();
        $this->api->refresh_access_token($input['refresh_token'] ?? '');
    }
}

Rate Limiting Usage

rate_limit() can be called with no arguments (uses config defaults and client IP as key), or with custom parameters per route:

<?php
// Use config defaults (IP-based key)
$this->api->rate_limit();

// Custom limit: 10 requests per 30 seconds, keyed by IP
$this->api->rate_limit(null, 10, 30);

// Custom key (e.g. per user ID to limit authenticated endpoints)
$auth = $this->api->require_jwt();
$this->api->rate_limit('user_' . $auth['sub'], 30, 60);

When the limit is exceeded the library automatically responds with 429 and exits:

HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1720000060

{
  "error": "Too many requests. Please try again later.",
  "limit": 60,
  "used": 61,
  "remaining": 0,
  "reset_at": "2025-01-01T00:01:00+00:00",
  "retry_after": 42
}

Using Postman with PHP API Controller

This guide shows how to test each endpoint of your PHP API using Postman, either in VS Code (via the Postman extension) or the standalone Postman app.

Note

Use your actual base URL (e.g. http://localhost/ApiController/...) when testing.

Login

Endpoint:

POST /ApiController/login

Request Body (JSON):

{
  "username": "admin",
  "password": "your_password"
}

Expected Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "expires_in": 900,
  "token_type": "Bearer"
}

Logout

Endpoint:

POST /ApiController/logout

Request Body (JSON):

{
  "refresh_token": "your_refresh_token"
}

Expected Response:

{
  "message": "Logged out"
}

List Users

Endpoint:

GET /ApiController/list

Headers:

Authorization: Bearer your_access_token

Expected Response:

[
  {
    "id": 1,
    "username": "admin",
    "email": "admin@example.com",
    "role": "admin",
    "created_at": "2025-08-01 12:00:00"
  }
]

Create User

Endpoint:

POST /ApiController/create

Headers:

Authorization: Bearer your_access_token
Content-Type: application/json

Request Body (JSON):

{
  "username": "newuser",
  "email": "newuser@example.com",
  "password": "secure123",
  "role": "user"
}

Expected Response:

{
  "message": "User created"
}

Update User

Endpoint:

PUT /ApiController/update/{id}

Headers:

Authorization: Bearer your_access_token
Content-Type: application/json

Request Body (JSON):

{
  "username": "updateduser",
  "email": "updated@example.com",
  "role": "editor"
}

Expected Response:

{
  "message": "User updated"
}

Delete User

Endpoint:

DELETE /ApiController/delete/{id}

Headers:

Authorization: Bearer your_access_token

Expected Response:

{
  "message": "User deleted"
}

Profile

Endpoint:

GET /ApiController/profile

Headers:

Authorization: Bearer your_access_token

Expected Response:

{
  "id": 1,
  "username": "admin",
  "email": "admin@example.com",
  "role": "admin",
  "created_at": "2025-08-01 12:00:00"
}

Refresh Token

Endpoint:

POST /ApiController/refresh

Request Body (JSON):

{
  "refresh_token": "your_refresh_token"
}

Expected Response:

{
  "message": "Tokens refreshed successfully",
  "tokens": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
    "expires_in": 900,
    "token_type": "Bearer"
  }
}

Tips

  • Use Postman collections to save all these endpoints.

  • Use environment variables for {{base_url}}, {{access_token}}, and {{refresh_token}} to streamline testing and token management.

  • Use Postman’s Tests tab to automatically extract and save access_token and refresh_token from the login response into environment variables.

Note

  • jwt_secret and refresh_token_key must each be at least 32 random characters.

  • jwt_issuer and jwt_audience are validated on every validate_jwt() call.

  • CORS is automatically handled on every request.

  • OPTIONS preflight requests are automatically answered with 204 and execution stops.

  • Refresh tokens are never stored in plain text — they are HMAC-hashed before insertion.

  • Token rotation is enforced: each refresh_access_token() call revokes the old token and issues a brand new pair.

Api library simplifies authentication, rate limiting, token management, and secure API development in LavaLust.