| use_cases |
|
|---|
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-serverintegration- 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
- Auth System (
Pramnos\Auth\Auth) - Main authentication controller - User Management (
Pramnos\User\User) - User data and operations - JWT Support (
Pramnos\Auth\JWT) - JSON Web Token implementation - Permissions (
Pramnos\Auth\Permissions) - Access control system - Session Management (
Pramnos\Http\Session) - Session handling - Token Management (
Pramnos\User\Token) - User tokens and API access
// 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'];
}// 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 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();
}// 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 current user
$auth = \Pramnos\Auth\Auth::getInstance();
$auth->logout();
// This triggers the 'Logout' addon events and clears the sessionIf 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).
// 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();// 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 = 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();// 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');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();
}// 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.
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 anonymousBoth 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.
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);$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);// 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 requestThere 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.
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.
$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 allThat 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.
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 = \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 = \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();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;
}
}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'
];
}
}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');
}
}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)];
}
}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).
| 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.
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.
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);
}| 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 |
| Constant | Value |
|---|---|
Loginlockout::DEFAULT_WINDOW_SECONDS |
900 |
Loginlockout::DEFAULT_STEPS |
[3=>60, 5=>300, 7=>900, 10=>3600] |
Two classes: TOTPHelper (pure static, no DB) and TwoFactorAuthService (stateful, DB-backed).
Compatible with Google Authenticator, Authy, and any RFC 6238 TOTP app.
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 formatSecurity note:
getQRCodeUrl()sends the TOTP secret to an external API and is deprecated. Always usegetQRCodeDataUri()instead.
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 rowsReplay 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.
| 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) |
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);
}
}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 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'] ?? ''));
}
}// 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']
]
]
];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 theauthfeature's migrations — there is nothing to create by hand. An older revision of this guide printed aCREATE TABLE permissionsstatement 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.
- Always use prepared statements - The framework's
prepareQuery()method prevents SQL injection - Validate JWT tokens properly - Set appropriate expiry times and validate all claims
- Use strong passwords - Implement password complexity requirements
- Enable CSRF protection - Use session tokens for form submissions
- Implement rate limiting - Prevent brute force attacks on login endpoints
- Use HTTPS - Always transmit authentication data over secure connections
- Log security events - Monitor failed login attempts and permission violations
- Cache permissions - Use the caching system for frequently checked permissions
- Optimize token queries - Index token tables properly
- Clean up expired tokens - Regularly remove old authentication tokens
- Use efficient session storage - Consider Redis for session storage in production
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.
- Framework Guide - Core framework concepts and MVC patterns
- Database API Guide - Database operations for user data management
- Cache System Guide - Caching user sessions and permissions
- Console Commands Guide - CLI tools for user management
- Logging System Guide - Logging authentication events and security monitoring
- Email System Guide - Password reset and notification emails
- Internationalization Guide - Multi-language authentication flows
For additional information on implementing authentication in your controllers and APIs, see the Framework Guide section on authentication patterns.