Complete guide to authentication methods in PHP-CRUD-API-Generator.
- Overview
- Authentication Methods
- Configuration
- API Key Authentication
- Basic Authentication
- JWT Authentication
- Role-Based Access Control (RBAC)
- Security Best Practices
- Troubleshooting
The API supports 4 authentication methods:
| Method | Best For | Performance | Security | Complexity |
|---|---|---|---|---|
| API Key | Server-to-server, webhooks | ⚡ Fast | 🔒 Medium | ⭐ Simple |
| Basic Auth | Development, internal tools | ⚡ Fast | 🔒 Medium | ⭐ Simple |
| JWT | Web/mobile apps, high traffic | ⚡⚡⚡ Very Fast | 🔒🔒 High | ⭐⭐ Medium |
| OAuth | Third-party integrations | ⚡ Fast | 🔒🔒🔒 Very High | ⭐⭐⭐ Complex |
Use these exact values in config/api.php:
'auth_method' => 'apikey', // ✅ Correct (not 'api_key')
'auth_method' => 'basic', // ✅ Correct
'auth_method' => 'jwt', // ✅ Correct
'auth_method' => 'oauth', // ✅ Correct (placeholder)❌ Common mistakes:
'api_key'(with underscore) - Won't work!'API_KEY'(uppercase) - Won't work!'bearer'- Use'jwt'instead
Edit: config/api.php
<?php
return [
// Enable/disable authentication globally
'auth_enabled' => true,
// Choose ONE authentication method
'auth_method' => 'jwt', // Options: 'apikey', 'basic', 'jwt', 'oauth'
// ... method-specific configs below
];✅ Good for:
- Server-to-server communication
- Webhooks and callbacks
- Internal microservices
- Automated scripts/cron jobs
- Testing and development
❌ Avoid for:
- Public-facing web apps (keys can be exposed in browser)
- Mobile apps (keys in source code)
- Multi-user systems (one key = all same permissions)
'auth_enabled' => true,
'auth_method' => 'apikey',
// List of valid API keys
'api_keys' => [
'changeme123',
'production-key-xyz789',
'webhook-secret-abc456',
],
// Default role for ALL API key users
'api_key_role' => 'admin', // Options: 'admin', 'editor', 'readonly', customPostman:
GET http://localhost:8000?action=tables
Headers:
X-API-Key: changeme123
Steps:
1. Create new request (GET)
2. URL: http://localhost:8000?action=tables
3. Go to "Headers" tab
4. Add header:
- Key: X-API-Key
- Value: changeme123
5. Click "Send"
HTTPie:
http GET http://localhost:8000 action==tables X-API-Key:changeme123
# Or with explicit header syntax:
http http://localhost:8000 action==tables "X-API-Key: changeme123"cURL:
curl -H "X-API-Key: changeme123" \
http://localhost:8000?action=tablesJavaScript (Fetch):
fetch('http://localhost:8000?action=tables', {
headers: {
'X-API-Key': 'changeme123'
}
})
.then(res => res.json())
.then(data => console.log(data));PHP:
$ch = curl_init('http://localhost:8000?action=tables');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-API-Key: changeme123'
]);
$response = curl_exec($ch);Python (Requests):
import requests
response = requests.get(
'http://localhost:8000?action=tables',
headers={'X-API-Key': 'changeme123'}
)
print(response.json())URL:
http://localhost:8000?action=tables&api_key=changeme123
Postman:
GET http://localhost:8000?action=tables&api_key=changeme123
Steps:
1. Create new request (GET)
2. URL: http://localhost:8000
3. Go to "Params" tab
4. Add parameters:
- action: tables
- api_key: changeme123
5. Click "Send"
HTTPie:
http GET http://localhost:8000 action==tables api_key==changeme123cURL:
curl "http://localhost:8000?action=tables&api_key=changeme123"JavaScript:
fetch('http://localhost:8000?action=tables&api_key=changeme123')
.then(res => res.json());🔒 Best Practices:
- Rotate keys regularly (every 90 days)
- Use long, random keys (32+ characters)
- Generate keys securely:
bin2hex(random_bytes(32)) // 64-char hex string
- One key per service (easier to revoke)
- Use HTTPS only (keys sent in plaintext)
✅ Good for:
- Development and testing
- Internal admin tools
- Legacy system integration
- Small teams (< 10 users)
❌ Avoid for:
- High-traffic APIs (queries database on every request)
- Scalable systems (use JWT instead)
- Public APIs (username/password less secure than tokens)
'auth_enabled' => true,
'auth_method' => 'basic',
// Option 1: Config file users (simple, not recommended for production)
'basic_users' => [
'admin' => 'secret', // Username => Password
'john' => 'password123',
'alice' => 'alicepass',
],
// Option 2: Database users (recommended for production)
'use_database_auth' => true, // Enable database lookup
// Map config users to roles
'user_roles' => [
'admin' => 'admin',
'john' => 'readonly',
'alice' => 'editor',
],Postman:
GET http://localhost:8000?action=tables
Authorization:
Type: Basic Auth
Username: admin
Password: secret
Steps:
1. Create new request (GET)
2. URL: http://localhost:8000?action=tables
3. Go to "Authorization" tab
4. Type: Select "Basic Auth" from dropdown
5. Username: admin
6. Password: secret
7. Click "Send"
Postman automatically encodes credentials as Base64 and adds header:
Authorization: Basic YWRtaW46c2VjcmV0
HTTPie:
# Method 1: Simple syntax (HTTPie handles Basic Auth automatically)
http -a admin:secret GET http://localhost:8000 action==tables
# Method 2: Explicit header (manual Base64 encoding)
http GET http://localhost:8000 action==tables "Authorization: Basic YWRtaW46c2VjcmV0"cURL:
curl -u admin:secret \
http://localhost:8000?action=tablesJavaScript (Fetch):
const credentials = btoa('admin:secret'); // Base64 encode
fetch('http://localhost:8000?action=tables', {
headers: {
'Authorization': 'Basic ' + credentials
}
})
.then(res => res.json());PHP:
$ch = curl_init('http://localhost:8000?action=tables');
curl_setopt($ch, CURLOPT_USERPWD, 'admin:secret');
$response = curl_exec($ch);Python:
import requests
from requests.auth import HTTPBasicAuth
response = requests.get(
'http://localhost:8000?action=tables',
auth=HTTPBasicAuth('admin', 'secret')
)Simply visit the URL in a browser:
http://localhost:8000?action=tables
Browser will prompt for username and password automatically.
Create users via CLI:
php scripts/create_user.php john john@example.com SecurePass123! readonlyHow it works:
- User credentials stored in
api_userstable (password hashed with Argon2ID) - Basic Auth first checks database, then falls back to config file
- Role comes from database
api_users.rolecolumn
Authentication Flow:
Request with Basic Auth
↓
Check database (if use_database_auth = true)
↓ (if not found)
Check config file basic_users
↓ (if not found)
Return 401 Unauthorized
With Basic Auth + database users:
- 1000 users × 10 requests/minute = 10,000 database queries/minute
Solution: Use JWT instead (99.8% fewer queries)
✅ Best for:
- High-traffic APIs
- Web and mobile apps
- Scalable microservices
- Multi-user systems
- Public-facing APIs
✅ Advantages:
- Performance: No database query per request (stateless)
- Scalability: Works with load balancers (no shared sessions)
- Security: Signed tokens, expiration, role claims
- User experience: Login once, use for hours
'auth_enabled' => true,
'auth_method' => 'jwt',
// JWT signing secret (CHANGE THIS IN PRODUCTION!)
'jwt_secret' => 'YourSuperSecretKeyChangeMe',
// Token expiration time (seconds)
'jwt_expiration' => 3600, // 1 hour
// Optional: JWT issuer and audience claims
'jwt_issuer' => 'api.yourdomain.com',
'jwt_audience' => 'yourdomain.com',
// Enable database authentication for login
'use_database_auth' => true,jwt_secret in production to a long random string (64+ characters)
// Generate secure secret:
bin2hex(random_bytes(32))The API accepts login credentials in 3 different formats:
| Format | Content-Type | Is JSON? | Use Case |
|---|---|---|---|
| JSON | application/json |
✅ Yes | Modern APIs, JavaScript apps |
| Form Data | application/x-www-form-urlencoded |
❌ No | Traditional HTML forms |
| Multipart | multipart/form-data |
❌ No | File uploads |
Important: Only Option 1 (JSON) uses actual JSON format!
cURL:
curl -X POST "http://localhost:8000?action=login" \
-H "Content-Type: application/json" \
-d '{"username":"john","password":"SecurePass123!"}'Postman:
POST http://localhost:8000?action=login
Headers:
Content-Type: application/json
Body → raw → JSON:
{
"username": "john",
"password": "SecurePass123!"
}
HTTPie:
http POST http://localhost:8000 action==login username=john password=SecurePass123!Format: application/x-www-form-urlencoded (traditional HTML form format)
cURL:
curl -X POST \
-d "username=john&password=SecurePass123!" \
http://localhost:8000?action=loginPostman:
POST http://localhost:8000?action=login
Body → x-www-form-urlencoded:
username: john
password: SecurePass123!
(This is NOT JSON - it's the same format as URL query parameters)
Format: multipart/form-data (used when uploading files)
cURL:
curl -X POST \
-F "username=john" \
-F "password=SecurePass123!" \
http://localhost:8000?action=loginPostman:
POST http://localhost:8000?action=login
Body → form-data:
username: john
password: SecurePass123!
(This is NOT JSON - it's a multipart format for file uploads)
Response (Success):
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MzQ4...",
"expires_at": 1699568400,
"user": "john",
"role": "readonly"
}Response (Failure):
{
"error": "Invalid credentials"
}Request:
GET /api.php?action=tables
Authorization: Bearer eyJ0eXAiOiJKV1Qi...cURL:
TOKEN="eyJ0eXAiOiJKV1Qi..."
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8000?action=tablesJavaScript (Fetch):
// After login, save token
const loginResponse = await fetch('/api.php?action=login', {
method: 'POST',
body: new URLSearchParams({
username: 'john',
password: 'SecurePass123!'
})
});
const { token } = await loginResponse.json();
// Use token for subsequent requests
const dataResponse = await fetch('/api.php?action=tables', {
headers: {
'Authorization': 'Bearer ' + token
}
});
const data = await dataResponse.json();React Example:
import { useState, useEffect } from 'react';
function App() {
const [token, setToken] = useState(localStorage.getItem('jwt_token'));
const [tables, setTables] = useState([]);
const login = async (username, password) => {
const response = await fetch('/api.php?action=login', {
method: 'POST',
body: new URLSearchParams({ username, password })
});
const data = await response.json();
if (data.success) {
setToken(data.token);
localStorage.setItem('jwt_token', data.token);
}
};
const fetchTables = async () => {
const response = await fetch('/api.php?action=tables', {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await response.json();
setTables(data.tables);
};
return (
<div>
{!token ? (
<LoginForm onLogin={login} />
) : (
<Dashboard tables={tables} onLoad={fetchTables} />
)}
</div>
);
}JWT tokens contain 3 parts (separated by .):
eyJ0eXAiOiJKV1QiLCJhbGc... ← Header (algorithm)
.
eyJpYXQiOjE3MzQ4MzIwMDA... ← Payload (user, role, expiration)
.
9Xw7rZ8kL5mN3pQ6tY1uV... ← Signature (prevents tampering)
Payload (decoded):
{
"iat": 1734832000, // Issued at (timestamp)
"exp": 1734835600, // Expires at (timestamp)
"iss": "api.yourdomain.com", // Issuer
"aud": "yourdomain.com", // Audience
"sub": "john", // Subject (username)
"role": "readonly" // Custom: user role
}Before JWT (Basic Auth with 1000 users):
10 requests/min × 1000 users = 10,000 auth queries/minute
10,000 queries/min × 60 min = 600,000 queries/hour
After JWT:
1 login/hour × 1000 users = 1,000 auth queries/hour (99.8% reduction!)
Why so fast?
- Token validated in-memory (no database)
- Signature verification takes microseconds
- Role embedded in token (no lookup needed)
✅ Signed Tokens:
- Signature prevents tampering
- If token modified, validation fails
✅ Expiration:
- Tokens auto-expire (default: 1 hour)
- Reduces impact of stolen tokens
✅ Role Claims:
- Role embedded in token
- RBAC enforced without database query
✅ Stateless:
- No server-side session storage
- Scales horizontally (load balancers)
Option 1: localStorage (Simple)
// After login
localStorage.setItem('jwt_token', token);
// For requests
const token = localStorage.getItem('jwt_token');
fetch('/api.php?action=tables', {
headers: { 'Authorization': 'Bearer ' + token }
});Option 2: httpOnly Cookie (More Secure)
Modify login endpoint to set cookie:
setcookie('jwt_token', $token, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true, // HTTPS only
'httponly' => true, // JavaScript can't access
'samesite' => 'Strict' // CSRF protection
]);Browser automatically sends cookie with requests.
Option 3: Memory (Most Secure)
Store token in JavaScript variable (lost on page refresh):
let token = null;
// After login
token = loginResponse.token;
// User must re-login on page refreshFor sessions longer than token expiration:
- Login: Get access token (1 hour) + refresh token (30 days)
- Access Expired: Use refresh token to get new access token
- Refresh Expired: User must re-login
Implementation: (Future enhancement)
RBAC controls which tables and actions each role can access.
Defined in: config/api.php
'roles' => [
// Admin: Full access to everything
'admin' => [
'*' => ['list', 'read', 'create', 'update', 'delete']
],
// Read-only: Can view data but not modify
'readonly' => [
'*' => ['list', 'read'],
// Explicitly deny system tables
'api_users' => [], // Empty array = NO ACCESS
'api_key_usage' => [],
],
// Editor: Can modify data but not see system tables
'editor' => [
'*' => ['list', 'read', 'create', 'update', 'delete'],
'api_users' => [], // Deny access
'api_key_usage' => [],
],
// Custom: Users manager (specific tables only)
'users_manager' => [
'users' => ['list', 'read', 'create', 'update'],
'orders' => ['list', 'read'],
// All other tables: no access
],
],| Action | Description | Example |
|---|---|---|
list |
View list of records | GET /api.php?table=users&action=list |
read |
View single record | GET /api.php?table=users&action=read&id=1 |
create |
Insert new record | POST /api.php?table=users&action=create |
update |
Modify existing record | PUT /api.php?table=users&action=update&id=1 |
delete |
Remove record | DELETE /api.php?table=users&action=delete&id=1 |
Empty array blocks all access:
'readonly' => [
'*' => ['list', 'read'], // Can read all tables...
'api_users' => [], // ...EXCEPT this one (denied)
]Specific table permissions override wildcards:
'users_manager' => [
'users' => ['list', 'read', 'create', 'update'],
// All other tables: no access (no wildcard = deny by default)
]All API key users get the same role:
'api_key_role' => 'admin', // All API keys = admin roleConfig file users:
'basic_users' => [
'admin' => 'secret',
],
'user_roles' => [
'admin' => 'admin', // Username => Role
],Database users:
-- Role stored in database
SELECT username, role FROM api_users WHERE username = 'john';
-- john, readonlyRole embedded in token during login:
// Login endpoint creates token with role claim
$token = createJwt([
'sub' => 'john',
'role' => 'readonly' // ← Role from database
]);Extracted during request validation:
$decoded = JWT::decode($token, ...);
$role = $decoded->role; // No database query!Test 1: Admin can access system tables
curl -H "X-API-Key: changeme123" \
http://localhost:8000?table=api_users&action=list
# Expected: 200 OK with user listTest 2: Readonly blocked from system tables
curl -u john:password123 \
http://localhost:8000?table=api_users&action=list
# Expected: 403 ForbiddenTest 3: Readonly can view regular tables
curl -u john:password123 \
http://localhost:8000?table=products&action=list
# Expected: 200 OK with product listTest 4: Editor blocked from creating users
curl -X POST -u alice:alicepass \
-d "username=hacker&role=admin" \
http://localhost:8000?table=api_users&action=create
# Expected: 403 Forbidden❌ HTTP (Insecure):
http://api.example.com
- Credentials sent in plaintext
- Tokens can be intercepted
- Man-in-the-middle attacks
✅ HTTPS (Secure):
https://api.example.com
JWT Secret:
// ❌ Weak
'jwt_secret' => 'secret123',
// ✅ Strong (64+ characters, random)
'jwt_secret' => 'a7f92c8e4b6d1f3a9e8c7b5d2f1a6e9b8c7d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1',Generate:
php -r "echo bin2hex(random_bytes(32));"Rotate keys every 90 days:
'api_keys' => [
'current-key-xyz789', // Active
'previous-key-abc456', // Grace period (7 days)
// 'old-key-def123', // Removed after grace period
],Prevent brute force attacks:
'rate_limit' => [
'enabled' => true,
'max_requests' => 100, // 100 requests
'window_seconds' => 60, // Per minute
],'monitoring' => [
'enabled' => true,
'thresholds' => [
'auth_failures' => 10, // Alert if > 10 failures in time window
],
],View dashboard: http://localhost/dashboard.html
Database users: Argon2ID hashing (automatic via create_user.php)
// In create_user.php
$passwordHash = password_hash($password, PASSWORD_ARGON2ID);Config file users: Use hashed passwords (future enhancement)
Short-lived tokens:
'jwt_expiration' => 3600, // 1 hour (recommended)Long-lived tokens (less secure):
'jwt_expiration' => 86400, // 24 hoursRestrict API access to specific domains:
// Add to public/index.php
header('Access-Control-Allow-Origin: https://yourdomain.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Authorization, X-API-Key, Content-Type');Causes:
- Wrong credentials
- Auth enabled but no credentials provided
- Token expired (JWT)
- Wrong auth method configured
Solutions:
# Check auth method in config
'auth_method' => 'jwt', # Must match your usage
# Test with API key
curl -H "X-API-Key: changeme123" http://localhost:8000?action=tables
# Test with Basic Auth
curl -u admin:secret http://localhost:8000?action=tables
# Test JWT login
curl -X POST -d "username=john&password=pass" http://localhost:8000?action=loginCause: User authenticated but no role configured
Solutions:
For API Key:
'api_key_role' => 'admin', // Add this to configFor Basic Auth (config users):
'user_roles' => [
'john' => 'readonly', // Map username to role
],For Basic Auth (database users):
-- Check role in database
SELECT username, role FROM api_users WHERE username = 'john';
-- Update if NULL
UPDATE api_users SET role = 'readonly' WHERE username = 'john';For JWT:
- Role should be in token claims (check login endpoint)
Cause: RBAC blocking access to table
Check RBAC config:
'roles' => [
'readonly' => [
'*' => ['list', 'read'],
'api_users' => [], // ← Explicitly denied
],
],Solution: Grant permission or use admin role
❌ Wrong:
'auth_method' => 'api_key', // Underscore won't work!✅ Correct:
'auth_method' => 'apikey', // No underscoreCauses:
- Token expired
- Wrong secret key
- Token tampered with
Debug:
# Decode token (without verification)
echo "eyJ0eXAi..." | base64 -d
# Check expiration
php -r "
\$token = 'eyJ0eXAi...';
\$parts = explode('.', \$token);
\$payload = json_decode(base64_decode(\$parts[1]));
echo 'Expires: ' . date('Y-m-d H:i:s', \$payload->exp);
"Solution: Re-login to get fresh token
Check configuration:
'use_database_auth' => true, // Must be enabledCheck database:
-- Verify user exists
SELECT * FROM api_users WHERE username = 'john';
-- Check password hash
SELECT password_hash FROM api_users WHERE username = 'john';Test password:
php -r "
\$hash = '$2y$10$...'; // From database
\$password = 'SecurePass123!';
echo password_verify(\$password, \$hash) ? 'Match' : 'No match';
"Cause: Database query on every request
Solution: Switch to JWT
Before (Basic Auth):
- 1000 users × 10 req/min = 10,000 auth queries/minute
After (JWT):
- 1000 users × 1 login/hour = 1,000 auth queries/hour
- 99.8% reduction!
Change config:
'auth_method' => 'jwt', // Instead of 'basic'Request Type: GET
URL: http://localhost:8000?action=tables
Option A - Header (Recommended):
├── Headers tab
└── Add: X-API-Key = changeme123
Option B - Query Parameter:
├── Params tab
└── Add: api_key = changeme123
Request Type: GET
URL: http://localhost:8000?action=tables
Authorization tab:
├── Type: Basic Auth
├── Username: admin
└── Password: secret
Step 1 - Login:
Request Type: POST
URL: http://localhost:8000?action=login
Body → x-www-form-urlencoded:
├── username: john
└── password: SecurePass123!
Response: Copy the "token" value
Step 2 - Use Token:
Request Type: GET
URL: http://localhost:8000?action=tables
Headers tab:
└── Add: Authorization = Bearer eyJ0eXAiOiJKV1Qi...
# API Key (Header)
http GET http://localhost:8000 action==tables X-API-Key:changeme123
# API Key (Query Parameter)
http GET http://localhost:8000 action==tables api_key==changeme123
# Basic Auth
http -a admin:secret GET http://localhost:8000 action==tables
# JWT Login
http POST http://localhost:8000 action==login username=john password=SecurePass123!
# JWT Request (after login)
http GET http://localhost:8000 action==tables "Authorization: Bearer TOKEN_HERE"| Feature | API Key | Basic Auth | JWT |
|---|---|---|---|
| Config Value | 'apikey' |
'basic' |
'jwt' |
| Header Name | X-API-Key |
Authorization: Basic |
Authorization: Bearer |
| Query Param | ?api_key=XXX |
❌ | ❌ |
| Login Required | ❌ | ❌ | ✅ (POST ?action=login) |
| Role Assignment | api_key_role config |
user_roles or DB |
Token claim |
| DB Query per Request | ❌ | ✅ (with DB users) | ❌ |
| Best For | Webhooks | Development | Production |
| Performance | ⚡ Fast | ⚡ Fast | ⚡⚡⚡ Very Fast |
| Security | 🔒 Medium | 🔒 Medium | 🔒🔒 High |
| User Tracking | ❌ (shared key) | ✅ | ✅ |
| Postman Setup | Headers or Params | Authorization tab | Headers (after login) |
| HTTPie Syntax | X-API-Key:value |
-a user:pass |
"Authorization: Bearer ..." |
- Choose auth method based on your use case
- Update
config/api.phpwith correct method name - Configure roles in RBAC section
- Test authentication with examples above
- Monitor dashboard for security events
- Read security best practices before production
Related Documentation:
Version: 1.0.0
Last Updated: October 22, 2025