A modern PHP 8.5+ micro-framework with attribute-based routing, JWT authentication, request signature verification, and CLI support.
- Attribute-based routing - Define routes directly on action classes using
#[Route] - JWT Authentication - Stateless authentication with token types (USER, CHARACTER, ADMIN, APPLICATION)
- Request Signatures - HMAC-SHA256 request signing with device binding for security
- Middleware Pipeline - PSR-15 compliant middleware system
- Validation - Declarative request validation with
#[ValidateRequest]attributes - CLI Support - Symfony Console integration with auto-discovered commands
- Rate Limiting - Built-in rate limiting middleware
- PHP 8.5+
- PostgreSQL
- Redis
- Composer
Create an App.php file that extends the base Application class:
<?php
declare(strict_types=1);
namespace YourApp;
use PCF\Addendum\Application\Application;
use PCF\Addendum\Attribute\Actions;
use PCF\Addendum\Attribute\Commands;
use PCF\Addendum\Attribute\Name;
use PCF\Addendum\Attribute\Version;
#[Name('MyApplication')]
#[Version('1.0.0')]
#[Actions(__DIR__ . '/Action')]
#[Commands(__DIR__ . '/Command')]
final class App extends Application
{
}HTTP Entry Point (pub/index.php):
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use YourApp\App;
App::http();CLI Entry Point (bin/app):
#!/usr/bin/env php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use YourApp\App;
App::console();Actions are single-purpose request handlers:
<?php
declare(strict_types=1);
namespace YourApp\Action;
use PCF\Addendum\Action\ActionInterface;
use PCF\Addendum\Attribute\Route;
use PCF\Addendum\Attribute\Middleware;
use PCF\Addendum\Attribute\ValidateRequest;
use PCF\Addendum\Http\Request;
use PCF\Addendum\Http\Middleware\Auth;
use PCF\Addendum\Validation\Rules\Required;
use PCF\Addendum\Validation\Rules\Email;
#[Route(path: '/users', method: 'POST')]
#[ValidateRequest('email', new Required())]
#[ValidateRequest('email', new Email())]
#[ValidateRequest('password', new Required())]
class CreateUserAction implements ActionInterface
{
public function __invoke(Request $request): CreateUserResponse
{
$email = $request->get('email');
$password = $request->get('password');
// Your logic here...
return new CreateUserResponse($user);
}
}Create a .env file in your project root:
# Database
POSTGRES_HOST=localhost
POSTGRES_DB=myapp
POSTGRES_USER=myapp
POSTGRES_PASSWORD=secret
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# JWT
JWT_SECRET=your-secret-key-min-32-characters
# Application
APP_ENV=development
DEBUG=trueAll API requests must include signature headers for security. This protects against:
- Request tampering
- Replay attacks
- Token theft
- Man-in-the-middle attacks
Every request must include these headers:
| Header | Description |
|---|---|
X-Request-Timestamp |
Unix timestamp (max 5 minutes old) |
X-Request-Fingerprint |
Unique device/client identifier |
X-Request-Signature |
HMAC-SHA256 signature |
Authorization |
Bearer token (authenticated endpoints only) |
For unauthenticated endpoints, the signature uses the fingerprint as the signing key:
data = timestamp + fingerprint + method + path + body
signature = HMAC-SHA256(fingerprint, data)
For authenticated endpoints, the signature uses a composite key:
data = timestamp + fingerprint + method + path + body
signingKey = JWT_SECRET + jti + fingerprintHash
signature = HMAC-SHA256(signingKey, data)
Where:
jtiis the JWT token ID (from the token payload)fingerprintHashis stored in the token and must matchSHA1(fingerprint)
JWT tokens are bound to specific devices:
- During login/register, the server creates
fingerprintHash = SHA1(fingerprint) - This hash is stored in the JWT token
- On each request, the server verifies the fingerprint matches the token
- Stolen tokens cannot be used from different devices
import crypto from 'crypto';
// Generate and store device fingerprint (persistent)
function getDeviceFingerprint(): string {
let fingerprint = localStorage.getItem('device_fingerprint');
if (!fingerprint) {
const components = [
navigator.userAgent,
navigator.language,
screen.width + 'x' + screen.height,
screen.colorDepth,
new Date().getTimezoneOffset(),
];
fingerprint = btoa(components.join('|'));
localStorage.setItem('device_fingerprint', fingerprint);
}
return fingerprint;
}
// Generate request signature
function signRequest(params: {
method: string;
path: string;
body: object | null;
fingerprint: string;
token?: string; // JWT token (for authenticated requests)
jwtSecret?: string; // Only needed server-side or in secure env
}): { timestamp: number; signature: string } {
const timestamp = Math.floor(Date.now() / 1000);
const bodyString = params.body ? JSON.stringify(params.body) : '';
const data = `${timestamp}${params.fingerprint}${params.method}${params.path}${bodyString}`;
let signingKey: string;
if (params.token && params.jwtSecret) {
// Authenticated request
const payload = JSON.parse(atob(params.token.split('.')[1]));
const jti = payload.jti;
const fingerprintHash = payload.fingerprintHash || '';
signingKey = params.jwtSecret + jti + fingerprintHash;
} else {
// Public request
signingKey = params.fingerprint;
}
const signature = crypto
.createHmac('sha256', signingKey)
.update(data)
.digest('hex');
return { timestamp, signature };
}async function register(email: string, password: string) {
const method = 'POST';
const path = '/v1/users';
const body = { email, password };
const fingerprint = getDeviceFingerprint();
const { timestamp, signature } = signRequest({
method,
path,
body,
fingerprint,
});
const response = await fetch(`https://api.example.com${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Request-Timestamp': timestamp.toString(),
'X-Request-Fingerprint': fingerprint,
'X-Request-Signature': signature,
},
body: JSON.stringify(body),
});
return response.json();
}async function login(email: string, password: string) {
const method = 'POST';
const path = '/v1/sessions';
const body = { email, password };
const fingerprint = getDeviceFingerprint();
const { timestamp, signature } = signRequest({
method,
path,
body,
fingerprint,
});
const response = await fetch(`https://api.example.com${path}`, {
method,
headers: {
'Content-Type': 'application/json',
'X-Request-Timestamp': timestamp.toString(),
'X-Request-Fingerprint': fingerprint,
'X-Request-Signature': signature,
},
body: JSON.stringify(body),
});
// Response includes access_token and refresh_token
return response.json();
}async function getCharacters(accessToken: string) {
const method = 'GET';
const path = '/v1/characters';
const fingerprint = getDeviceFingerprint();
const { timestamp, signature } = signRequest({
method,
path,
body: null,
fingerprint,
token: accessToken,
jwtSecret: process.env.JWT_SECRET, // Server-side only!
});
const response = await fetch(`https://api.example.com${path}`, {
method,
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-Request-Timestamp': timestamp.toString(),
'X-Request-Fingerprint': fingerprint,
'X-Request-Signature': signature,
},
});
return response.json();
}import hashlib
import hmac
import json
import time
import requests
def get_device_fingerprint() -> str:
"""Generate or retrieve device fingerprint."""
# In a real app, persist this value
return "unique-device-identifier"
def sign_request(
method: str,
path: str,
body: dict | None,
fingerprint: str,
token: str | None = None,
jwt_secret: str | None = None,
) -> tuple[int, str]:
"""Generate request signature."""
timestamp = int(time.time())
body_string = json.dumps(body, separators=(',', ':')) if body else ''
data = f"{timestamp}{fingerprint}{method}{path}{body_string}"
if token and jwt_secret:
# Authenticated request
import base64
payload = json.loads(base64.b64decode(token.split('.')[1] + '=='))
jti = payload.get('jti', '')
fingerprint_hash = payload.get('fingerprintHash', '')
signing_key = jwt_secret + jti + fingerprint_hash
else:
# Public request
signing_key = fingerprint
signature = hmac.new(
signing_key.encode(),
data.encode(),
hashlib.sha256
).hexdigest()
return timestamp, signature
# Example: Login
def login(email: str, password: str):
method = 'POST'
path = '/v1/sessions'
body = {'email': email, 'password': password}
fingerprint = get_device_fingerprint()
timestamp, signature = sign_request(method, path, body, fingerprint)
response = requests.post(
f'https://api.example.com{path}',
json=body,
headers={
'X-Request-Timestamp': str(timestamp),
'X-Request-Fingerprint': fingerprint,
'X-Request-Signature': signature,
}
)
return response.json()# Variables
FINGERPRINT="my-device-fingerprint"
TIMESTAMP=$(date +%s)
METHOD="POST"
PATH="/v1/sessions"
BODY='{"email":"user@example.com","password":"secret123"}'
# Calculate signature for public endpoint
DATA="${TIMESTAMP}${FINGERPRINT}${METHOD}${PATH}${BODY}"
SIGNATURE=$(echo -n "$DATA" | openssl dgst -sha256 -hmac "$FINGERPRINT" | cut -d' ' -f2)
# Make request
curl -X POST "https://api.example.com${PATH}" \
-H "Content-Type: application/json" \
-H "X-Request-Timestamp: ${TIMESTAMP}" \
-H "X-Request-Fingerprint: ${FINGERPRINT}" \
-H "X-Request-Signature: ${SIGNATURE}" \
-d "${BODY}"The framework includes these built-in endpoints:
| Method | Path | Description |
|---|---|---|
POST |
/v1/users |
Register new user |
POST |
/v1/sessions |
Login (get tokens) |
POST |
/v1/sessions/refresh |
Refresh access token |
DELETE |
/v1/sessions |
Logout |
| Method | Path | Description |
|---|---|---|
GET |
/v1/users/me |
Get current user profile |
GET |
/v1/users/:uuid |
Get user by UUID |
PATCH |
/v1/users/me |
Update profile |
DELETE |
/v1/users/me |
Delete account |
| Method | Path | Description |
|---|---|---|
DELETE |
/v1/admin/tokens |
Revoke tokens (admin only) |
# Database migrations
php bin/app migrate
# List all routes
php bin/app app:routes
php bin/app app:routes --detailed
php bin/app app:routes --method=POST
# User management
php bin/app auth:logout <user-uuid>
php bin/app auth:logout --all
# Admin management
php bin/app app:grant-admin <email>
php bin/app app:revoke-admin <email>
# Application tokens
php bin/app app:generate-token <app-name> <owner-name> <owner-email>
php bin/app app:revoke-tokens --application=<name>
# Cron jobs
php bin/app cron:run
# Cache management
php bin/app cache:clear-proxyThe framework supports multiple token types for different contexts:
| Type | Purpose | Capabilities |
|---|---|---|
USER |
After login | Manage characters, view profile |
CHARACTER |
After selecting character | In-game actions |
ADMIN |
Admin users | Full access, bypass ownership |
APPLICATION |
External services | Long-lived, elevated privileges |
- User logs in → receives
USERtoken - User selects character → receives
CHARACTERtoken - User performs in-game actions with
CHARACTERtoken - User can switch back to
USERcontext anytime
- Always use HTTPS - Never send signatures over unencrypted connections
- Persistent fingerprint - Store device fingerprint consistently across sessions
- Clock synchronization - Ensure client system time is accurate (NTP)
- Exact body match - Use the exact same JSON string for signature and request
- Secure JWT_SECRET - Keep secret secure, never expose in client-side code
- Fingerprint consistency - Use the same fingerprint for all requests from a device
MIT License