Skip to content

Latest commit

 

History

History
1006 lines (773 loc) · 33 KB

File metadata and controls

1006 lines (773 loc) · 33 KB
use_cases
Signing a user in or out, or checking who is signed in
Creating or managing user accounts
Issuing, validating or revoking JWT / bearer tokens
Requiring authentication in a controller
Working with sessions or authentication addons

Pramnos Authentication & User Management Guide

Overview

The Pramnos Framework provides a comprehensive authentication system that supports multiple authentication methods, user management, permissions, JWT tokens, session management, and OAuth2 capabilities. The system is modular and extensible through addons.

v1.2 New Features:

  • Database Authentication Driver — Native auth without addons
  • Two-Factor Authentication (2FA/TOTP) — Time-based one-time passwords
  • Login Lockout — Brute-force protection with exponential backoff
  • Session Tracking — Bot detection and session monitoring
  • OAuth2 Server — Full league/oauth2-server integration
  • Security Hardening — CSRF, session cookie, view escaping
  • Auto-Login Lifecycle — Built-in login/logout handling

See related guides: Pramnos_Security_Guide.md, Pramnos_Authorization_Guide.md

Architecture

Core Components

  1. Auth System (Pramnos\Auth\Auth) - Main authentication controller
  2. User Management (Pramnos\User\User) - User data and operations
  3. JWT Support (Pramnos\Auth\JWT) - JSON Web Token implementation
  4. Permissions (Pramnos\Auth\Permissions) - Access control system
  5. Session Management (Pramnos\Http\Session) - Session handling
  6. Token Management (Pramnos\User\Token) - User tokens and API access

Authentication Flow

// Basic authentication flow
$auth = \Pramnos\Auth\Auth::getInstance();
$success = $auth->auth($username, $password, $rememberMe);

if ($success) {
    // User is authenticated, session is set
    $user = \Pramnos\User\User::getCurrentUser();
} else {
    // Authentication failed
    $response = $auth->lastResponse;
    echo $response['message'];
}

User Authentication

Basic Login

// Simple login
$auth = \Pramnos\Auth\Auth::getInstance();
$result = $auth->auth('user@example.com', 'password');

if ($result) {
    echo "Login successful";
    // User session is automatically created
} else {
    echo "Login failed: " . $auth->lastResponse['message'];
}

Login with Remember Me

// Login with persistent session
$auth = \Pramnos\Auth\Auth::getInstance();
$result = $auth->auth('user@example.com', 'password', true); // Third parameter enables "remember me"

if ($result) {
    // User will stay logged in across browser sessions
    $user = \Pramnos\User\User::getCurrentUser();
}

Encrypted Password Authentication

// Login with pre-encrypted password (for API scenarios)
$auth = \Pramnos\Auth\Auth::getInstance();
$hashedPassword = password_hash('plaintext_password', PASSWORD_DEFAULT);
$result = $auth->auth('user@example.com', $hashedPassword, false, true); // Fourth parameter indicates encrypted password

Logout

// Logout current user
$auth = \Pramnos\Auth\Auth::getInstance();
$auth->logout();
// This triggers the 'Logout' addon events and clears the session

Credential Verification (Without Session)

If you need to verify a user's credentials (username/password) without actually logging them in (e.g. for API tokens, OAuth2 Password Grants, or verifying a password before sensitive actions), use the central static method User::validateUserCredentials():

$credentials = \Pramnos\User\User::validateUserCredentials($username, $password);

if ($credentials !== false) {
    // Credentials match!
    $userId = $credentials['userid'];
    $username = $credentials['username'];
    $email = $credentials['email'];
} else {
    // Invalid credentials
}

This method delegates to Auth::getInstance()->verifyCredentials($username, $password) which resolves the active authentication logic in the proper priority order (respecting any registered addons like UserDatabase or custom AuthDriverInterface drivers).

User Management

Creating Users

// Create a new user
$user = new \Pramnos\User\User();
$user->username = 'johndoe';
$user->email = 'john@example.com';
$user->password = password_hash('password', PASSWORD_DEFAULT);
$user->firstname = 'John';
$user->lastname = 'Doe';
$user->status = 1; // Active
$user->save();

Loading Users

// Load user by ID
$user = new \Pramnos\User\User(123);

// Load user by username/email
$user = new \Pramnos\User\User();
$user->loadByUsername('johndoe');

// Get current logged-in user
$currentUser = \Pramnos\User\User::getCurrentUser();
if ($currentUser) {
    echo "Welcome, " . $currentUser->firstname;
}

User Data Management

$user = new \Pramnos\User\User(123);

// Get user data as array
$userData = $user->getData();

// Update user information
$user->email = 'newemail@example.com';
$user->save();

// Delete user
$user->delete();

JWT Token Authentication

Generating JWT Tokens

// Create JWT token for user
$payload = [
    'userId' => $user->userid,
    'username' => $user->username,
    'exp' => time() + 3600, // Expires in 1 hour
    'iat' => time(), // Issued at
];

$secret = 'your-secret-key';
$token = \Pramnos\Auth\JWT::encode($payload, $secret, 'HS256');

Validating JWT Tokens

try {
    $secret = 'your-secret-key';
    \Pramnos\Auth\JWT::$leeway = 60; // Allow 60 seconds clock skew
    
    $decoded = \Pramnos\Auth\JWT::decode($token, $secret, ['HS256']);
    
    // Token is valid, load user
    $user = new \Pramnos\User\User($decoded->userId);
    
} catch (\Exception $e) {
    // Token validation failed
    echo "Invalid token: " . $e->getMessage();
}

API Authentication with JWT

// In API controllers, JWT is validated by the auth middleware before the
// controller runs. Ask who the request is — never read $_SESSION.
class ApiController extends \Pramnos\Application\Controller
{
    public function secureEndpoint()
    {
        $user = \Pramnos\User\User::getCurrentUser();
        if (!is_object($user) || (int) $user->userid < 2) {
            return ['status' => 401, 'message' => 'Authentication required'];
        }

        return ['status' => 200, 'data' => 'Protected data for user ' . $user->userid];
    }
}

!!! warning "Do not read $_SESSION in an API controller" An application that serves a website and an API from one origin shares a session cookie between them. $_SESSION['user'] therefore answers with whoever is signed in to the website, which is not the caller — a request that presented no credential at all would be authenticated, and logout could not work, because revoking the token would leave the cookie answering.

`User::getCurrentUser()` asks the request instead: an API request seals its
own identity (see below), and a sealed answer — including "nobody" — stops
the session being consulted at all.

Where a request's identity comes from

The framework keeps two ideas apart, and the separation is what makes a hybrid application safe:

website API
credential session cookie token on the call
established by login, stored in the session auth middleware, per request
read with User::getCurrentUser() User::getCurrentUser()
may write the session yes no

Pramnos\Http\RequestIdentity is how a middleware says "this call is user X, or nobody, and nothing else may answer":

RequestIdentity::seal($user, 'accessToken');   // this request is $user
RequestIdentity::seal(null);                   // this request is anonymous

Both ApiAuthMiddleware and UnifiedAuthMiddleware do this for you. Sealing is request-scoped and writes nothing that outlives the call, so an API request can never change who the browser's next page belongs to — and an anonymous API call can never sign the browser out, which is what used to happen when the API "cleared" the ambient session to protect itself.

An API that genuinely needs both — an authserver whose own web UI calls its own endpoints — should use UnifiedAuthMiddleware, which accepts a Bearer token or a session cookie plus an X-CSRF-Token header. The CSRF token is not optional there: a cookie is sent by the browser automatically, so without it any site could make authenticated calls on the user's behalf.

Token Management

User Tokens

The framework supports various token types for different purposes:

$user = new \Pramnos\User\User(123);

// Add authentication token
$token = $user->addToken('auth', bin2hex(random_bytes(32)), 'API access token');

// Add Apple Push Notification token
$apnsToken = $user->addToken('apns', $deviceToken, 'iPhone device');

// Add OAuth2 access token
$accessToken = $user->addToken('access_token', $oauthToken, 'OAuth2 access', $refreshTokenId);

Token Operations

$user = new \Pramnos\User\User(123);

// Get user's active auth token
$authToken = $user->getToken();

// Get all user tokens
$allTokens = $user->getAllTokens();

// Load user by token
$user->loadByToken($tokenString, 'auth');

// Clean up old tokens (older than 30 days)
\Pramnos\User\User::cleanupAllAuthTokens(30);

Working with Token Objects

// Create token object
$token = new \Pramnos\User\Token();
$token->userid = $user->userid;
$token->tokentype = 'auth';
$token->token = bin2hex(random_bytes(32));
$token->notes = 'Mobile app access';
$token->expires = time() + (30 * 24 * 60 * 60); // 30 days
$token->save();

// Load existing token
$existingToken = new \Pramnos\User\Token($tokenId);
$details = $existingToken->getDetails();

// Track token usage
$existingToken->addAction(); // Logs the current request

Permissions System

There is one permission store: authserver.permissions. It is created by the auth feature's migrations, so every installation that has users has it — running an OAuth server is not a prerequisite. Two APIs read it, and which one you want depends on how much of the model you need:

Pramnos\Auth\Permissions Pramnos\Auth\PermissionResolver
Shape one question, one answer the user's whole effective grant list
Reads grants for a user or role grants, roles, priorities, expiry, audience, conditions
Writes yes — allow() / deny() no
Use it for a check in a controller anything conditional, scoped or audited

Permissions is the simple face; it cannot express what it cannot ask about. Grants carrying ABAC conditions are skipped by it rather than treated as unconditional — the resolver hands conditions to the application to evaluate against its own request context, and this API has no way to receive one.

Setting Permissions

allow(), deny() and removePermission() are instance methods; get the shared instance from the factory.

$permissions = \Pramnos\Auth\Permissions::getInstance();

// Grant a user the right to create articles
$permissions->allow(
    $userId,           // Subject — a user id
    'articles',        // Resource      → object_type
    'create',          // Privilege     → action
    '',                // Element: '' means all articles → object_id NULL
    'module',          // Resource type (not stored; see below)
    'user'             // Subject type: user | group
);

// Grant a group — stored as a role, which is what the new model calls it
$permissions->allow($editorsRoleId, 'articles', 'edit', '', 'module', 'group');

// Deny — stored above allow so it wins a tie
$permissions->deny($userId, 'admin', 'access');

// Remove — absence is not the same answer as deny
$permissions->removePermission($userId, 'admin', 'access');

Two mappings worth knowing: the admin privilege is stored as the * action (everything on this object), and resourceType has no column in the new model — object_type already carries that distinction.

A subject type other than user or group cannot be represented. Rather than store it under the wrong subject_type, the call is refused and logged.

Checking Permissions

$permissions = \Pramnos\Auth\Permissions::getInstance();

// Default: an unknown permission is "no"
if ($permissions->isAllowed($userId, 'articles', 'create')) {
    $this->showCreateForm();
} else {
    throw new \Exception('Insufficient permissions', 403);
}

Pass false as the last argument when you need to tell "denied" apart from "nobody ever said":

$verdict = $permissions->isAllowed(
    $userId, 'articles', 'create', '', 'module', 'user', false
);

// true = allowed, false = explicitly denied, null = no rule at all

That distinction matters. Collapsing null into false is how a screen ends up refusing a user for whom no rule was ever written — which is exactly what the class did before it knew which store it was reading.

Roles

The new model expresses groups as roles, and PermissionResolver folds a user's active roles into their effective permissions automatically. Assign one by inserting into authserver.user_roles; role definitions live in authserver.roles.

For the full picture — priorities, expiry, audience scoping and ABAC conditions — use the resolver directly:

$resolver = new \Pramnos\Auth\PermissionResolver($database);
$effective = $resolver->resolve($userId, null)['permissions'];

Each entry carries object_type, object_id, action, grant and conditions, with deny-over-allow already applied.

Session Management

Basic Session Operations

$session = \Pramnos\Http\Session::getInstance();

// Check if user is logged in
if ($session->isLogged()) {
    $userId = $_SESSION['uid'];
    $username = $_SESSION['username'];
}

// Create snapshot for post-login redirect
$session->snapshot($_SERVER['REQUEST_URI']);

// Get and clear snapshot
$returnUrl = $session->getSnapshot();
if ($returnUrl) {
    $this->redirect($returnUrl);
}

Session Security

$session = \Pramnos\Http\Session::getInstance();

// Get session token for CSRF protection
$token = $session->getToken();

// Validate CSRF token
if ($session->checkToken('post', 'csrf_')) {
    // Token is valid, process request
} else {
    // Invalid token, possible CSRF attack
    throw new \Exception('Invalid security token', 403);
}

// Reset session (for logout)
$session->reset();

Authentication Addons

User Database Addon

The framework includes a user database addon for standard username/password authentication:

// The UserDatabase addon is automatically triggered during authentication
// It checks credentials against the users table

// Custom validation can be added by extending the addon
class CustomUserAuth extends \Pramnos\Addon\Auth\UserDatabase
{
    public function onAuth($username, $password, $remember, $encryptedPassword, $validate)
    {
        // Add custom validation logic
        $result = parent::onAuth($username, $password, $remember, $encryptedPassword, $validate);
        
        if ($result['status']) {
            // Additional checks (e.g., account verification, 2FA)
            if (!$this->isTwoFactorVerified($result['uid'])) {
                return [
                    'status' => false,
                    'message' => 'Two-factor authentication required',
                    'statusCode' => 401
                ];
            }
        }
        
        return $result;
    }
}

Creating Custom Authentication Addons

class LdapAuth extends \Pramnos\Addon\Addon
{
    public function onAuth($username, $password, $remember, $encryptedPassword, $validate)
    {
        // LDAP authentication logic
        $ldapConnection = ldap_connect($this->config['ldap_server']);
        
        if (ldap_bind($ldapConnection, $username, $password)) {
            // Authentication successful
            $userInfo = $this->getLdapUserInfo($ldapConnection, $username);
            
            return [
                'status' => true,
                'username' => $username,
                'uid' => $userInfo['uid'],
                'email' => $userInfo['email'],
                'auth' => password_hash($password, PASSWORD_DEFAULT)
            ];
        }
        
        return [
            'status' => false,
            'message' => 'LDAP authentication failed'
        ];
    }
}

Controller Authentication

Protecting Controller Actions

class ArticleController extends \Pramnos\Application\Controller
{
    public function __construct(?\Pramnos\Application\Application $application = null)
    {
        // Define public actions (no authentication required)
        $this->addAction(['display', 'view']);
        
        // Define authenticated actions (login required)
        $this->addAuthAction(['create', 'edit', 'delete', 'save']);
        
        parent::__construct($application);
    }
    
    public function display()
    {
        // Public action - anyone can access
        return $this->getView('article')->display();
    }
    
    public function create()
    {
        // Authenticated action - user must be logged in
        // Framework automatically checks authentication
        return $this->getView('article')->display('create');
    }
}

API Authentication

class ApiController extends \Pramnos\Application\Controller
{
    public function __construct(?\Pramnos\Application\Application $application = null)
    {
        // API endpoints typically require authentication
        $this->addAuthAction(['list', 'create', 'update', 'delete']);
        parent::__construct($application);
    }
    
    public function list()
    {
        // Who this request is — established by whatever authenticated it, and
        // not by a session cookie the caller never meant as a credential.
        $user = \Pramnos\User\User::getCurrentUser();
        if (!is_object($user) || (int) $user->userid < 2) {
            return ['status' => 401, 'error' => 'Authentication required'];
        }

        // Return data specific to the authenticated user
        return ['status' => 200, 'data' => $this->getUserData($user->userid)];
    }
}

Advanced Features

Login Lockout (Brute-Force Protection)

Pramnos\Auth\Loginlockout — progressive brute-force lockout for login endpoints.

Tracks failed login attempts per scope+identifier pair. Three scopes are supported: 'user' (by user ID), 'identifier' (by normalized email/username), and 'ip' (by remote address).

Default thresholds

Failures Lockout duration
3 60 s (1 minute)
5 300 s (5 minutes)
7 900 s (15 minutes)
10+ 3600 s (1 hour)

A sliding window of 900 seconds applies: if the gap between the previous failure and the current attempt exceeds the window, the counter resets to 1. This prevents indefinite accumulation from past brute-force campaigns.

Lifting a lockout while developing

The lockout is doing its job when it locks somebody out — and that is no help when you have mistyped a fixture password three times and cannot test the login flow you are working on:

php pramnos auth:unlock admin                # this identifier, every scope
php pramnos auth:unlock 2 --scope=user       # by user id
php pramnos auth:unlock --list               # who is locked, and for how long
php pramnos auth:unlock --all                # everything (development only)

It clears the failure counter and nothing else — a wrong password is still a wrong password afterwards. --all refuses to run outside development: "clear every lockout on this server" is precisely what somebody working through a password list would want.

Usage

use Pramnos\Auth\Loginlockout;

$lockout = new Loginlockout();

// Check BEFORE processing the login attempt
$status = $lockout->getLockoutStatus('identifier', strtolower($email));
// Returns: ['locked' => bool, 'remaining' => int]

if ($status['locked']) {
    return ['status' => 429, 'retry_after' => $status['remaining']];
}

// Attempt authentication ...

if ($authFailed) {
    // Record failure for all applicable scopes
    $lockout->recordFailedAttempt('identifier', strtolower($email));
    $lockout->recordFailedAttempt('user', (string) $userId);
    $lockout->recordFailedAttempt('ip', $ipAddress);
} else {
    // Clear state on success
    $lockout->clearSuccessfulLoginState('identifier', strtolower($email));
    $lockout->clearSuccessfulLoginState('user', (string) $userId);
    $lockout->clearSuccessfulLoginState('ip', $ipAddress);
}

API

Method Description
recordFailedAttempt(string $scope, string $identifier): void Increment failure counter; apply threshold; creates row if absent
getLockoutStatus(string $scope, string $identifier): array Returns ['locked' => bool, 'remaining' => int]; 0 remaining when not locked
clearSuccessfulLoginState(string $scope, string $identifier): void Deletes the tracking row — fully resets counter and lockout

Constants

Constant Value
Loginlockout::DEFAULT_WINDOW_SECONDS 900
Loginlockout::DEFAULT_STEPS [3=>60, 5=>300, 7=>900, 10=>3600]

Two-Factor Authentication (TOTP)

Two classes: TOTPHelper (pure static, no DB) and TwoFactorAuthService (stateful, DB-backed).

Compatible with Google Authenticator, Authy, and any RFC 6238 TOTP app.

TOTPHelper — setup and verification

use Pramnos\Auth\TOTPHelper;

// Generate a new shared secret (once per user, during setup)
$secret = TOTPHelper::generateSecret(); // e.g., 'JBSWY3DPEHPK3PXP'

// Verify a user-submitted code (±1 window drift tolerance)
$valid = TOTPHelper::verifyCode($secret, $userCode); // bool

// QR code as an inline data URI — no external API calls, CSP-safe
$dataUri = TOTPHelper::getQRCodeDataUri($secret, 'user@example.com', 'MyApp');
// Returns 'data:image/svg+xml;base64,…' (requires chillerlan/php-qrcode ^5.0) or null

// Build the provisioning URI
$uri = TOTPHelper::buildOtpAuthUri($secret, 'user@example.com', 'MyApp');
// 'otpauth://totp/MyApp:user%40example.com?secret=…&issuer=MyApp'

// Backup codes
$codes   = TOTPHelper::generateBackupCodes(10);        // ['ABCD2345', ...]
$hash    = TOTPHelper::hashBackupCode($codes[0]);       // bcrypt hash for storage
$isMatch = TOTPHelper::verifyBackupCode($code, $hash);  // bool, case-insensitive

// Utilities
$remaining = TOTPHelper::getRemainingTime();    // seconds until window expires [1, 30]
$isValid   = TOTPHelper::isValidSecret($secret); // validate base32 format

Security note: getQRCodeUrl() sends the TOTP secret to an external API and is deprecated. Always use getQRCodeDataUri() instead.

TwoFactorAuthService — full setup flow

use Pramnos\Auth\TwoFactorAuthService;

$svc = new TwoFactorAuthService(); // uses Factory::getDatabase()

// Step 1 — generate secret and show QR code to user
$info = $svc->startSetup($userId, $userEmail);
// ['secret', 'qr_code_data_uri', 'qr_code_url', 'manual_entry_key', 'backup_codes']
// Show backup_codes ONCE — user must record them

// Step 2 — user scans QR, enters first code to confirm
$success = $svc->completeSetup($userId, $submittedCode); // bool

// Verify on login
$valid = $svc->verifyCode($userId, $code); // accepts TOTP or backup code

// State queries
$enabled   = $svc->isEnabled($userId);   // bool
$remaining = $svc->getRemainingBackupCodes($userId); // int
$status    = $svc->getStatus($userId);
// ['enabled' => bool, 'setup' => bool, 'backup_codes_remaining' => int]

// Management
$svc->disable($userId);                   // clears secret, sets enabled=0
$newCodes = $svc->regenerateBackupCodes($userId); // string[]|false
$svc->cleanupExpiredSessions();            // removes used/expired setup rows

Replay protection: verifyCode() compares the current 30-second window against last_used. If the same window was already used, the code is rejected even if cryptographically valid.

Backup codes are one-time: the matching hash is removed from storage after successful verification.

Database tables

Table Description
user_twofactor One row per user — enabled flag, secret, backup code hashes, last_used
twofactor_setup Temporary setup sessions (15-min TTL)
twofactor_attempts Append-only attempt log (TimescaleDB hypertable where available)

Password Security

class SecureUser extends \Pramnos\User\User
{
    public function setPassword($plainPassword)
    {
        // Validate password strength
        if (!$this->isPasswordStrong($plainPassword)) {
            throw new \Exception('Password does not meet security requirements');
        }
        
        // Hash with current best practices
        $this->password = password_hash($plainPassword, PASSWORD_ARGON2ID, [
            'memory_cost' => 65536,
            'time_cost' => 4,
            'threads' => 3
        ]);
    }
    
    public function verifyPassword($plainPassword)
    {
        return password_verify($plainPassword, $this->password);
    }
    
    private function isPasswordStrong($password)
    {
        // Implement password strength requirements
        return strlen($password) >= 8 
            && preg_match('/[A-Z]/', $password)
            && preg_match('/[a-z]/', $password) 
            && preg_match('/[0-9]/', $password)
            && preg_match('/[^A-Za-z0-9]/', $password);
    }
}

OAuth2 Scopes and Policy

Pramnos\Auth\Scopes — static scope registry for OAuth2 consent screens and validation.

use Pramnos\Auth\Scopes;

// All scopes grouped by category (consent screen)
$grouped = Scopes::getScopes();
// ['Personal User Data' => ['profile' => [...], 'email' => [...]], ...]

// Flat scope → description map
$descriptions = Scopes::getScopeDescriptions();

// Scopes implicitly granted to all clients
$defaults = Scopes::getDefaultScopes(); // ['profile', 'email', 'user']

// Validate a scope string
[$hasInvalid, $invalidList] = Scopes::hasInvalidScopes('profile email unknown_scope');

// Resolve inherited scopes transitively
$resolved = Scopes::resolveInheritedScopes('system:notifications_write');
// ['system:notifications_read', 'system:notifications_write']

Standard scopes: profile, email, phone, address, user, openid, offline_access, system:admin, system:audit_read, system:health, system:notifications_read/write.

Pramnos\Auth\OAuthPolicyHelper — server-wide OAuth2 policy defaults.

use Pramnos\Auth\OAuthPolicyHelper;

$methods = OAuthPolicyHelper::getDefaultAllowedAuthMethods();
// ['client_secret_basic', 'client_secret_post', 'private_key_jwt']

$grants = OAuthPolicyHelper::getDefaultAllowedGrantTypes();
// ['authorization_code', 'client_credentials', 'device_code', 'refresh_token', 'exchange_token']
// 'password' grant excluded (deprecated per RFC 9126 / OAuth 2.1)

OAuth2 Integration

// OAuth2 server capabilities are built into the API system
class OAuth2Controller extends \Pramnos\Application\Controller
{
    public function authorize()
    {
        // Handle OAuth2 authorization requests
        $clientId = $_GET['client_id'];
        $redirectUri = $_GET['redirect_uri'];
        $scope = $_GET['scope'] ?? 'read';
        
        // Validate client application
        $app = new \Pramnos\Application\Api\Apikey($clientId);
        if ($app->appid == 0 || $app->callback !== $redirectUri) {
            throw new \Exception('Invalid client application');
        }
        
        // If user is not logged in, redirect to login
        if (!\Pramnos\Http\Session::staticIsLogged()) {
            $this->redirect('/login?return_to=' . urlencode($_SERVER['REQUEST_URI']));
            return;
        }
        
        // Generate authorization code
        $user = \Pramnos\User\User::getCurrentUser();
        $authCode = bin2hex(random_bytes(32));
        
        $user->addToken('auth_code', $authCode, 'OAuth2 authorization code', null);
        
        // Redirect back to client with code
        $this->redirect($redirectUri . '?code=' . $authCode . '&state=' . ($_GET['state'] ?? ''));
    }
}

Configuration

Authentication Settings

// In your application configuration
return [
    'authentication' => [
        'jwt_secret' => env('JWT_SECRET', 'your-secret-key'),
        'jwt_expiry' => env('JWT_EXPIRY', 3600), // 1 hour
        'session_timeout' => env('SESSION_TIMEOUT', 1800), // 30 minutes
        'remember_me_duration' => env('REMEMBER_ME_DURATION', 2592000), // 30 days
        'password_hash_algo' => PASSWORD_ARGON2ID,
        'require_email_verification' => env('REQUIRE_EMAIL_VERIFICATION', true),
        'enable_mfa' => env('ENABLE_MFA', false),
    ],
    
    'permissions' => [
        'cache_permissions' => env('CACHE_PERMISSIONS', true),
        'default_user_permissions' => [
            'profile' => ['read', 'update'],
            'content' => ['read']
        ]
    ]
];

Database Setup

The authentication system requires several database tables. Use the framework's migration system to set them up:

-- Users table
CREATE TABLE `users` (
    `userid` int NOT NULL AUTO_INCREMENT,
    `username` varchar(255) NOT NULL,
    `email` varchar(255) NOT NULL,
    `password` varchar(255) NOT NULL,
    `firstname` varchar(255),
    `lastname` varchar(255),
    `status` tinyint DEFAULT 1,
    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (`userid`),
    UNIQUE KEY `username` (`username`),
    UNIQUE KEY `email` (`email`)
);

-- User tokens table
CREATE TABLE `usertokens` (
    `tokenid` int NOT NULL AUTO_INCREMENT,
    `userid` int NOT NULL,
    `tokentype` varchar(50) NOT NULL,
    `token` varchar(255) NOT NULL,
    `created` int NOT NULL,
    `lastused` int DEFAULT 0,
    `expires` int DEFAULT NULL,
    `status` tinyint DEFAULT 1,
    `notes` text,
    PRIMARY KEY (`tokenid`),
    KEY `userid` (`userid`),
    KEY `token` (`token`),
    FOREIGN KEY (`userid`) REFERENCES `users` (`userid`) ON DELETE CASCADE
);

Permissions are not in this list on purpose. They live in authserver.permissions, created by the auth feature's migrations — there is nothing to create by hand. An older revision of this guide printed a CREATE TABLE permissions statement for a table no migration has ever created; if you built one from it, see Legacy Permissions Migration for the SQL that moves its rows across.

Best Practices

Security Guidelines

  1. Always use prepared statements - The framework's prepareQuery() method prevents SQL injection
  2. Validate JWT tokens properly - Set appropriate expiry times and validate all claims
  3. Use strong passwords - Implement password complexity requirements
  4. Enable CSRF protection - Use session tokens for form submissions
  5. Implement rate limiting - Prevent brute force attacks on login endpoints
  6. Use HTTPS - Always transmit authentication data over secure connections
  7. Log security events - Monitor failed login attempts and permission violations

Performance Considerations

  1. Cache permissions - Use the caching system for frequently checked permissions
  2. Optimize token queries - Index token tables properly
  3. Clean up expired tokens - Regularly remove old authentication tokens
  4. Use efficient session storage - Consider Redis for session storage in production

Error Handling

try {
    $auth = \Pramnos\Auth\Auth::getInstance();
    $result = $auth->auth($username, $password);
    
    if (!$result) {
        $response = $auth->lastResponse;
        \Pramnos\Logs\Logger::logWarning('Failed login attempt', [
            'username' => $username,
            'ip' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT'],
            'reason' => $response['message']
        ]);
        
        return ['status' => 401, 'message' => 'Authentication failed'];
    }
    
} catch (\Exception $e) {
    \Pramnos\Logs\Logger::logError('Authentication error', [
        'error' => $e->getMessage(),
        'trace' => $e->getTraceAsString()
    ]);
    
    return ['status' => 500, 'message' => 'Internal authentication error'];
}

This authentication system provides a robust foundation for securing your Pramnos Framework applications with support for modern authentication patterns, comprehensive user management, and flexible permission systems.


Related Documentation


For additional information on implementing authentication in your controllers and APIs, see the Framework Guide section on authentication patterns.