Minnow

Authentication & Permissions

Minnow provides session-based authentication, role-based access control (RBAC), CSRF protection, and application passwords for API access. The system is fully compatible with WordPress user data.


Quick Start

Helper Functions

The most common auth operations are available as simple functions:

use function Minnow\Core\Auth\{
    auth, user, userId, authenticated, guest,
    attempt, logout, can, hasRole, isAdmin,
    session, hashPassword, verifyPassword,
    createNonce, verifyNonce, nonceField
};

// Login
if (attempt('user@example.com', 'password123')) {
    // Logged in
}

// Check auth state
authenticated();          // true if logged in
guest();                  // true if not logged in
user();                   // User object or null
userId();                 // User ID or null

// Check permissions
can('edit_posts');         // true/false
hasRole('administrator'); // true/false
isAdmin();                // true/false

// Logout
logout();

Authentication

Authenticator

The Authenticator class manages login, logout, and session state:

use Minnow\Core\Auth\Authenticator;

$auth = Authenticator::getInstance();

// Login with credentials (email or username)
$auth->attempt('user@example.com', 'password');

// Login a user directly (skip password check)
$auth->login($user);

// Act as user without session (for CLI/testing)
$auth->actAs($user);

// Check state
$auth->check();       // Is authenticated?
$auth->guest();       // Is guest?
$auth->user();        // Get User object
$auth->id();          // Get user ID

// Check permissions
$auth->can('edit_posts');
$auth->hasRole('editor');
$auth->isAdmin();

// Logout
$auth->logout();

Login Flow

  1. attempt() looks up the user by email or username
  2. Verifies the password against the stored hash
  3. On success, regenerates the session ID (prevents session fixation)
  4. Stores the user ID and a random auth token in the session
  5. If the password uses a legacy WordPress hash, automatically rehashes to bcrypt

Password Formats

Minnow supports three password hash formats for WordPress compatibility:

Format Pattern Description
WordPress phpass $P$... or $H$... Legacy format, auto-upgraded on login
WordPress bcrypt $wp$2y$... WordPress 6.8+ format with HMAC-SHA384
Modern bcrypt $2y$... Default for new passwords
use function Minnow\Core\Auth\{hashPassword, verifyPassword};

$hash = hashPassword('my-password');           // Modern bcrypt
$valid = verifyPassword('my-password', $hash); // Works with all formats

Sessions

Sessions use secure defaults with PHP's native session handling:

use function Minnow\Core\Auth\session;

$session = session();

$session->set('key', 'value');
$session->get('key');           // 'value'
$session->has('key');           // true
$session->remove('key');

// Flash data (persists for one request)
$session->flash('message', 'Saved!');
$session->getFlash('message');  // 'Saved!' (gone after this request)

Session Configuration

Configured during bootstrap in admin/boot.php:

Option Default Description
name minnow_session Cookie name
lifetime 120 (minutes) Session lifetime
secure false HTTPS-only cookies
httponly true No JavaScript access
samesite Lax CSRF protection

Roles & Capabilities

Minnow uses WordPress-compatible RBAC. Users are assigned roles, and roles grant capabilities.

Built-in Roles

Role Capabilities Description
Administrator All 46 capabilities Full access
Editor 27 capabilities Manage all content
Author 7 capabilities Publish own posts
Contributor 3 capabilities Write drafts
Subscriber 1 capability (read) Read-only

Checking Capabilities

// Via helper functions
can('edit_posts');
can('manage_options');
hasRole('administrator');
isAdmin();

// Via User object
$user = user();
$user->can('edit_posts');
$user->hasRole('editor');
$user->getAllCapabilities();
$user->getRoles();

Managing User Roles

$user = \Minnow\Core\Entity\User::find(1);

$user->assignRole('editor');
$user->removeRole('subscriber');
$user->syncRoles(['editor', 'author']);  // Replace all roles
$user->getRoles();                       // ['editor', 'author']

Managing Roles

use Minnow\Core\Auth\{Role, RoleManager};

$manager = RoleManager::getInstance();

// List all roles
$roles = $manager->all();  // ['administrator' => Role, 'editor' => Role, ...]

// Create a custom role
$role = new Role('moderator', 'Moderator', [
    'read' => true,
    'edit_posts' => true,
    'moderate_comments' => true,
]);
$manager->create($role);

// Update a role
$role = $manager->get('moderator');
$updated = $role->withCapability('upload_files');
$manager->update($updated);

// Delete a role (cannot delete 'administrator')
$manager->delete('moderator');

// User counts
$manager->getUserCount('editor');    // Number of editors
$manager->getAllUserCounts();        // ['administrator' => 2, 'editor' => 5, ...]

Direct Capability Overrides

Grant or revoke individual capabilities for a specific user, independent of their roles:

$user->setCapability('upload_files', true);   // Grant
$user->setCapability('delete_posts', false);  // Revoke
$user->removeCapability('upload_files');       // Remove override
$user->getDirectCapabilities();               // Only overrides

Capability Groups

Capabilities are organized into groups for the admin UI:

Group Capabilities
Users manage_roles, list_users, create_users, edit_users, delete_users, promote_users
Posts edit_posts, edit_others_posts, publish_posts, delete_posts, delete_others_posts, delete_published_posts, edit_published_posts, read_private_posts
Pages edit_pages, edit_others_pages, publish_pages, delete_pages, delete_others_pages, delete_published_pages, edit_published_pages, read_private_pages
Media upload_files, edit_files, delete_files
Comments moderate_comments, edit_comment
Taxonomies manage_categories, manage_tags, edit_categories, delete_categories, assign_categories, assign_tags
Appearance switch_themes, edit_themes, edit_theme_options
Plugins activate_plugins, edit_plugins, install_plugins, delete_plugins, update_plugins
System manage_options, import, export, unfiltered_html, update_core, view_site_health_checks
General read

Plugins can register additional capabilities:

use Minnow\Core\Auth\CapabilityRegistry;

CapabilityRegistry::getInstance()
    ->register('manage_bookmarks', 'Manage Bookmarks', 'Bookmarks', 'Full bookmark management');

CSRF Protection (Nonces)

Nonces are time-limited tokens that prevent cross-site request forgery. Each nonce is valid for 24 hours and is tied to the current user.

In HTML Forms

use function Minnow\Core\Auth\{createNonce, verifyNonce, nonceField};

// Generate a hidden input field
echo nonceField('save_settings');
// <input type="hidden" name="_nonce" value="a1b2c3d4e5" />

// Verify on submission
if (verifyNonce($_POST['_nonce'], 'save_settings')) {
    // Valid request
}

In API Requests

// Create token
$token = createNonce('api_action');

// Verify token
$valid = verifyNonce($token, 'api_action');

Nonces use HMAC-SHA256 with a 12-hour tick system. A nonce created in the current tick remains valid through the next tick, giving an effective 12-24 hour window.


Application Passwords

Application passwords provide secure API authentication without exposing the user's main password. Each application password is tracked independently.

use Minnow\Core\Auth\ApplicationPasswords;

// Create a new application password
$result = ApplicationPasswords::create($userId, [
    'name' => 'My Mobile App',
    'app_id' => 'mobile-app-v2',
]);
// $result['password'] = 'abcd efgh ijkl mnop qrst uvwx' (plaintext, shown once)
// $result['item'] = stored password object

// List all passwords for a user
$passwords = ApplicationPasswords::getForUser($userId);

// Authenticate with application password
$user = ApplicationPasswords::authenticate('username', 'abcdefghijklmnopqrstuvwx');

// Delete a password
ApplicationPasswords::delete($userId, $uuid);

// Delete all passwords
ApplicationPasswords::deleteAll($userId);

Application passwords are stored as bcrypt hashes in user meta. Usage (last used time, IP address) is tracked automatically and throttled to one update per day.


Security Features

Feature Implementation
Password hashing bcrypt (default), with WordPress phpass/bcrypt migration
Session fixation Session ID regenerated on login
Session cookies HttpOnly, SameSite=Lax, configurable Secure flag
CSRF protection HMAC-SHA256 nonces with 24-hour expiry
Password comparison Constant-time via hash_equals()
Session destruction Full cleanup on logout (session data + cookie)
API auth Separate application passwords with usage tracking
Auto-upgrade Legacy password hashes upgraded to bcrypt on login