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 |
|---|---|---|
|
object |
LavaLust super object (via |
|
string |
Database table name for storing refresh tokens |
|
integer |
Lifetime of access (payload) tokens in seconds |
|
integer |
Lifetime of refresh tokens in seconds |
|
string|array |
Allowed origin(s) for CORS. Accepts |
|
string |
Secret key used for signing JWT tokens (min 32 chars) |
|
string |
Key used for HMAC-hashing refresh tokens before DB storage (min 32 chars) |
|
string |
|
|
string |
|
|
boolean |
Enable or disable rate limiting globally |
|
integer |
Maximum number of requests allowed per window |
|
integer |
Rate limit window size in seconds |
Methods
CORS & Request Helpers
Method |
Description |
|---|---|
|
Sets CORS headers. Called automatically in the constructor. Supports |
|
Parses and sanitizes JSON or form-encoded request body. Returns an array. |
|
Returns sanitized URL query parameters ( |
|
Enforces a specific HTTP method. Sends |
Responses
Method |
Description |
|---|---|
|
Sends a JSON response with the given HTTP status code and exits. |
|
Sends a JSON error response with |
Rate Limiting
Method |
Description |
|---|---|
|
Enforces a rate limit using the cache library. Falls back to config defaults when parameters are |
JWT Authentication
Method |
Description |
|---|---|
|
Encodes a payload into a signed HS256 JWT. Automatically adds |
|
Decodes and verifies the signature of a JWT. Returns the payload array or |
|
Decodes the JWT and additionally validates |
|
Extracts a JWT bearer token from the |
|
Validates the current bearer token and returns its payload. Responds with |
Token System
Method |
Description |
|---|---|
|
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 |
|
Validates the refresh token, revokes it, and issues a new token pair (rotation). Responds directly with the new tokens. |
|
HMAC-hashes the token and deletes the matching row from the database. |
|
Deletes all expired refresh tokens. If |
Basic Authentication
Method |
Description |
|---|---|
|
Verifies HTTP Basic Auth credentials using constant-time comparison. |
|
Requires valid basic auth credentials. Sends |
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 |
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_tokenandrefresh_tokenfrom the login response into environment variables.
Note
jwt_secretandrefresh_token_keymust each be at least 32 random characters.jwt_issuerandjwt_audienceare validated on everyvalidate_jwt()call.CORS is automatically handled on every request.
OPTIONSpreflight requests are automatically answered with204and 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.