WebFiori provides a layered security system combining authentication state management, role-based access control (RBAC), attribute-based access control (ABAC), and declarative annotations for endpoint protection.
| Component | Package | Role |
|---|---|---|
SecurityContext | webfiori/http | Holds current authenticated user for the request |
SecurityPrincipal | webfiori/http | Interface for user objects |
#[RequiresAuth] | webfiori/http | Annotation: endpoint requires authentication |
#[PreAuthorize] | webfiori/http | Annotation: expression-based access control |
Access | webfiori/framework | RBAC role/permission definitions |
AccessManager | webfiori/framework | RBAC + ABAC policy evaluation |
AuthorizeMiddleware | webfiori/framework | Middleware for permission checks |
Implement this interface on your user class:
<?phpnamespaceApp\Domain;
useWebFiori\Http\SecurityPrincipal;
class User implements SecurityPrincipal {
publicfunction__construct(
publicint$id,
publicstring$name,
publicstring$role
) {
}
publicfunctiongetAuthorities(): array {
returnmatch ($this->role) {
'admin' => ['users.manage', 'orders.manage', 'orders.view'],
'customer' => ['orders.create', 'orders.view'],
default => []
};
}
publicfunctiongetId(): int|string {
return$this->id;
}
publicfunctiongetName(): string {
return$this->name;
}
publicfunctiongetRoles(): array {
return [$this->role];
}
publicfunctionisActive(): bool {
returntrue;
}
}Set the current user during request processing (typically in middleware):
useWebFiori\Http\SecurityContext;
// Set authenticated user
SecurityContext::setCurrentUser($user);
// Check authentication
SecurityContext::isAuthenticated(); // true if user is set and active// Get current user$user = SecurityContext::getCurrentUser();
// Check roles and authorities
SecurityContext::hasRole('admin');
SecurityContext::hasAuthority('orders.manage');
// Clear (e.g., on logout)
SecurityContext::clear();Requires the user to be authenticated. Can be placed on a class (all methods) or individual methods:
useWebFiori\Http\Annotations\RequiresAuth;
useWebFiori\Http\Annotations\RestController;
#[RestController('orders', 'Order API')]
#[RequiresAuth] // All methods require authenticationclass OrderService extends WebService {
publicfunctionisAuthorized(): bool {
return SecurityContext::isAuthenticated();
}
// ...
}Evaluates a security expression before the method executes:
useWebFiori\Http\Annotations\PreAuthorize;
// Single role check
#[PreAuthorize("hasRole('admin')")]
publicfunctiondeleteUser(): array { ... }
// Single authority check
#[PreAuthorize("hasAuthority('orders.manage')")]
publicfunctionshipOrder(): array { ... }
// Multiple roles (any)
#[PreAuthorize("hasAnyRole('admin', 'moderator')")]
publicfunctionbanUser(): array { ... }
// Combined with AND
#[PreAuthorize("isAuthenticated() && hasAuthority('reports.view')")]
publicfunctiongetReport(): array { ... }
// Combined with OR
#[PreAuthorize("hasRole('admin') || hasAuthority('orders.manage')")]
publicfunctioncancelOrder(): array { ... }Supported expressions:
| Expression | Meaning |
|---|---|
hasRole('ROLE') | User has the specified role |
hasAnyRole('R1', 'R2') | User has any of the listed roles |
hasAuthority('PERM') | User has the specified authority/permission |
hasAnyAuthority('P1', 'P2') | User has any of the listed authorities |
isAuthenticated() | User is authenticated and active |
permitAll() | Always allows access |
Combine with && (AND) and || (OR).
Skips all authentication checks for a method:
#[AllowAnonymous]
publicfunctiongetPublicData(): array { ... }Define roles and their permissions during initialization:
useWebFiori\Framework\Access;
// Define roles with permissions
Access::role('customer', ['orders.create', 'orders.view', 'orders.cancel']);
Access::role('staff', ['orders.view', 'orders.update', 'orders.ship']);
Access::role('admin', ['orders.create', 'orders.view', 'orders.cancel',
'orders.update', 'orders.ship', 'orders.manage']);
// Assign role to user (for storage-backed scenarios)
Access::assignRoleToUser($userId, 'customer');Permission and group IDs can contain letters (A-Z, a-z), digits (0-9), underscores (_), dots (.), and dashes (-). Dots and dashes are useful as namespace separators (e.g., orders.create, billing-read).
Roles can inherit permissions from a parent role:
Access::role('manager', ['reports.view'])->inherits('staff');
// manager now has: reports.view + all staff permissions// Pass user object — reads roles from getRoles() if no internal mapping exists
Access::can($user, 'orders.cancel');
// Pass user ID — uses roles from assignRoleToUser() or storage
Access::can(42, 'orders.cancel');By default, roles are stored in memory. For persistence across requests, use a storage backend:
| Backend | Class | Use Case |
|---|---|---|
| In-memory | InMemoryAccessStorage | Testing, stateless APIs |
| File-based | FileAccessStorage | Simple deployments |
| Database | DatabaseAccessStorage | Production with DB |
useWebFiori\Framework\AccessManager;
useWebFiori\Framework\Storage\DatabaseAccessStorage;
$manager = newAccessManager(newDatabaseAccessStorage($connectionName));
$manager->loadFromStorage(); // load roles/permissions from DBTo persist changes:
$manager->saveToStorage();Policies add resource-level rules on top of RBAC. A policy is evaluated only after the RBAC check passes.
Create policy classes under App/Policies/:
<?phpnamespaceApp\Policies;
useApp\Domain\Order;
class OrderCancelPolicy {
publicfunctiongetPermission(): string {
return'orders.cancel';
}
publicfunctionevaluate($user, ?object$resource = null): bool {
if ($resource === null || !$resourceinstanceof Order) {
returnfalse;
}
// Only pending orders can be cancelledif ($resource->status !== 'pending') {
returnfalse;
}
// Admin can cancel any pending orderif (in_array('admin', $user->getRoles())) {
returntrue;
}
// Customers can only cancel their own ordersreturn$user->getId() === $resource->userId;
}
}useApp\Policies\OrderCancelPolicy;
useWebFiori\Framework\Access;
Access::registerPolicy(newOrderCancelPolicy());Pass the resource as the third argument to can():
$order = $orderRepo->findById($id);
if (!Access::can($user, 'orders.cancel', $order)) {
thrownewForbiddenException('You cannot cancel this order.');
}The evaluation flow:
- RBAC check: Does the user's role have the
orders.cancelpermission? If no → denied. - Policy check: Does
OrderCancelPolicy::evaluate($user, $order)return true? If no → denied. - Both pass → allowed.
A typical setup in App/Ini/Privileges.php:
<?phpnamespaceApp\Ini;
useApp\Policies\OrderCancelPolicy;
useApp\Policies\OrderViewPolicy;
useWebFiori\Framework\Access;
class Privileges {
publicstaticfunctioninitialize() {
// RBAC
Access::role('customer', ['orders.create', 'orders.view', 'orders.cancel']);
Access::role('admin', ['orders.create', 'orders.view', 'orders.cancel', 'orders.manage']);
// ABAC
Access::registerPolicy(newOrderViewPolicy());
Access::registerPolicy(newOrderCancelPolicy());
}
}A middleware that loads the user into SecurityContext:
<?phpnamespaceApp\Middleware;
useWebFiori\Framework\Access;
useWebFiori\Framework\Middleware\AbstractMiddleware;
useWebFiori\Http\SecurityContext;
class SecurityContextLoader extends AbstractMiddleware {
publicfunctionbefore(Request$request, Response$response) {
$userId = SessionsManager::get('user-id');
if ($userId === null) {
SecurityContext::clear();
return;
}
$user = $this->loadUser($userId);
if ($user !== null && $user->isActive()) {
SecurityContext::setCurrentUser($user);
Access::assignRoleToUser($user->getId(), $user->role);
}
}
}A service using both annotations and Access::can():
#[RestController('orders', 'Order API')]
#[RequiresAuth]
class OrderService extends WebService {
publicfunctionisAuthorized(): bool {
return SecurityContext::isAuthenticated();
}
#[DeleteMapping]
#[PreAuthorize("hasAuthority('orders.cancel')")]
publicfunctioncancelOrder(?int$id = null): array {
$order = $this->repo->findById($id);
$user = SecurityContext::getCurrentUser();
// ABAC: policy checks ownership + statusif (!Access::can($user, 'orders.cancel', $order)) {
thrownewForbiddenException('Cannot cancel this order.');
}
$order->status = 'cancelled';
$this->repo->save($order);
return [$order];
}
}