Authentication

Overview

The Auth class handles all authentication for your application. It supports two independent actor types — admin and user — each with their own session, login flow, and remember-me cookie. Both can be logged in simultaneously without conflict.

  • Class: JiFramework\Core\Auth\Auth
  • Access via: $app->auth

Features:

  • Email + password login with password_verify() (bcrypt-safe)
  • Login by ID for internal or OAuth flows
  • Remember me tokens stored in database, valid for 30 days
  • Token cookies are HttpOnly and Secure
  • Automatic token check on every new App() — users are restored from cookie transparently
  • Configurable table names — no hardcoded schema requirements

Database Tables

Auth requires three database tables. Create them with the following schema:

-- Admin accounts
CREATE TABLE admin (
    id       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email    VARCHAR(180) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL
);

-- User accounts
CREATE TABLE users (
    id       INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email    VARCHAR(180) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL
);

-- Remember-me tokens (shared by both admins and users)
CREATE TABLE tokens (
    id              INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id         INT UNSIGNED NOT NULL,
    token           VARCHAR(64)  NOT NULL UNIQUE,
    expire_datetime DATETIME     NOT NULL,
    type            ENUM('admin', 'user') NOT NULL
);

You can add any extra columns you need (e.g. name, status, created_at) — Auth only reads id, email, and password internally.

Always store passwords as bcrypt hashes:

$hash = password_hash('plain_password', PASSWORD_BCRYPT);

Configuration

Table names and session keys are fully configurable. Add any of these to config/jiconfig.php:

return [
    // Table names (defaults shown)
    'auth_admin_table' => 'admin',
    'auth_user_table'  => 'users',
    'auth_token_table' => 'tokens',

    // Session keys (defaults shown)
    'session_admin_key' => 'admin',
    'session_user_key'  => 'user',
];

These map to Config::$authAdminTable, Config::$authUserTable, Config::$authTokenTable, Config::$adminSessionKey, and Config::$userSessionKey. Changing them does not require touching framework code.

adminLogin()

adminLogin(string $email, string $password, bool $remember = false): bool

Verifies credentials against the admin table. On success, stores the admin ID in the session. Returns true on success, false on failure.

  • $email(string) Admin email address
  • $password(string) Plain-text password to verify against the stored hash
  • $remember(bool, optional) Set to true to issue a 30-day remember-me cookie. Default: false
// Basic login
if ($app->auth->adminLogin($_POST['email'], $_POST['password'])) {
    $app->redirect('/admin/dashboard');
} else {
    $error = 'Invalid email or password.';
}

// Login with remember me
$remember = isset($_POST['remember']) && $_POST['remember'] === '1';
if ($app->auth->adminLogin($_POST['email'], $_POST['password'], $remember)) {
    $app->redirect('/admin/dashboard');
}

userLogin()

userLogin(string $email, string $password, bool $remember = false): bool

Identical to adminLogin() but operates on the user table and user session key.

  • $email(string) User email address
  • $password(string) Plain-text password
  • $remember(bool, optional) Issue a 30-day remember-me cookie. Default: false
if ($app->auth->userLogin($_POST['email'], $_POST['password'])) {
    $app->redirect('/dashboard');
} else {
    $error = 'Invalid credentials.';
}

adminLoginById() and userLoginById()

adminLoginById(int $id): bool
userLoginById(int $id): bool

Logs in an admin or user by their database ID, skipping password verification. Useful for OAuth callbacks, impersonation, or automatic login after registration.

  • $id(int) The primary key of the admin or user record
// Log in a user immediately after registration
$userId = $app->db->table('users')->insertGetId([
    'email'    => $_POST['email'],
    'password' => password_hash($_POST['password'], PASSWORD_BCRYPT),
]);
$app->auth->userLoginById($userId);
$app->redirect('/dashboard');

// OAuth callback: find or create user, then log them in
$user = $app->db->table('users')->where('email', $oauthEmail)->first();
if (!$user) {
    $id = $app->db->table('users')->insertGetId(['email' => $oauthEmail, 'password' => '']);
} else {
    $id = $user['id'];
}
$app->auth->userLoginById($id);
$app->redirect('/dashboard');

adminLogout() and userLogout()

adminLogout(): void
userLogout(): void

Destroys the session entry, deletes the remember-me token from the database, and expires the remember-me cookie. Call these on logout endpoints.

// Admin logout
$app->auth->adminLogout();
$app->redirect('/admin/login');

// User logout
$app->auth->userLogout();
$app->redirect('/login');

// Combined logout (log out both at once if needed)
$app->auth->adminLogout();
$app->auth->userLogout();
$app->redirect('/');

isAdminLoggedIn() and isUserLoggedIn()

isAdminLoggedIn(): bool
isUserLoggedIn(): bool

Returns true if the respective actor has an active session. Use these as guards at the top of protected pages.

// Guard an admin page
if (!$app->auth->isAdminLoggedIn()) {
    $app->redirect('/admin/login');
}

// Guard a user page
if (!$app->auth->isUserLoggedIn()) {
    $app->redirect('/login');
}

// Check both independently
if ($app->auth->isAdminLoggedIn() && $app->auth->isUserLoggedIn()) {
    // Edge case: someone is logged in as both admin and user simultaneously
}

Because remember-me tokens are checked automatically when $app->auth is first accessed, a user who closed the browser but has a valid cookie will appear as logged in immediately — no extra code needed.

getAdminId() and getUserId()

getAdminId(): int|null
getUserId(): int|null

Returns the primary key of the currently authenticated admin or user. Returns null if not logged in.

$adminId = $app->auth->getAdminId(); // e.g. 1 or null
$userId  = $app->auth->getUserId();  // e.g. 42 or null

// Typical usage: fetch logged-in user data
$userId = $app->auth->getUserId();
if ($userId) {
    $posts = $app->db->table('posts')
        ->where('user_id', $userId)
        ->orderBy('created_at', 'DESC')
        ->get();
}

getAdmin() and getUser()

getAdmin(): array|null
getUser(): array|null

Fetches the full database row of the currently authenticated admin or user. Returns the row as an associative array, or null if not logged in.

// Get logged-in user details
$user = $app->auth->getUser();
if ($user) {
    echo 'Welcome, ' . htmlspecialchars($user['name']);
}

// Get logged-in admin details
$admin = $app->auth->getAdmin();
if ($admin) {
    echo 'Logged in as admin: ' . htmlspecialchars($admin['email']);
}

// Safe access with null check
$name = $app->auth->getUser()['name'] ?? 'Guest';

This performs a database query on every call. If you need the user data multiple times in one request, assign it to a variable.

clearExpiredTokens()

clearExpiredTokens(): void

Deletes all expired remember-me tokens from the tokens table. This is a maintenance method — the framework does not call it automatically. Run it periodically to keep the tokens table lean.

// Call from a scheduled script or cron job
$app->auth->clearExpiredTokens();

// Or call it on a low-frequency page to do light maintenance
// (e.g. once every 100 requests using a random check)
if (rand(1, 100) === 1) {
    $app->auth->clearExpiredTokens();
}

Complete Example - Full Login Page

A complete login page handling both GET (show form) and POST (process login) in a single file:

<?php
require __DIR__ . '/vendor/autoload.php';
$app = new App();

// Already logged in - send to dashboard
if ($app->auth->isUserLoggedIn()) {
    $app->redirect('/dashboard');
}

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Validate input first
    $valid = $app->validator->validate($_POST, [
        'email'    => 'required|email',
        'password' => 'required',
    ]);

    if (!$valid) {
        $error = 'Please enter a valid email and password.';
    } else {
        $remember = !empty($_POST['remember']);

        if ($app->auth->userLogin($_POST['email'], $_POST['password'], $remember)) {
            $app->logger->info('User logged in', ['user_id' => $app->auth->getUserId()]);
            $app->redirect('/dashboard');
        } else {
            $error = 'Incorrect email or password.';
        }
    }
}
?>
<!DOCTYPE html>
<html>
<body>
  <form method="POST">
    <?php if ($error): ?>
      <p style="color:red"><?= htmlspecialchars($error) ?></p>
    <?php endif ?>
    <input type="email"    name="email"    placeholder="Email" required>
    <input type="password" name="password" placeholder="Password" required>
    <label><input type="checkbox" name="remember" value="1"> Remember me</label>
    <button type="submit">Login</button>
  </form>
</body>
</html>

Complete Example - Protected Dashboard

A protected admin panel page that guards access, shows the logged-in user, and handles logout:

<?php
require __DIR__ . '/vendor/autoload.php';
$app = new App();

// Guard: redirect if not logged in
if (!$app->auth->isAdminLoggedIn()) {
    $app->redirect('/admin/login');
}

// Handle logout
if (isset($_GET['logout'])) {
    $app->auth->adminLogout();
    $app->redirect('/admin/login');
}

// Fetch current admin and their stats
$admin      = $app->auth->getAdmin();
$totalUsers = $app->db->table('users')->count();
$newToday   = $app->db->table('users')
    ->where('created_at', '>=', date('Y-m-d') . ' 00:00:00')
    ->count();
?>
<!DOCTYPE html>
<html>
<body>
  <h1>Admin Dashboard</h1>
  <p>Welcome, <?= htmlspecialchars($admin['email']) ?></p>
  <p>Total users: <?= $totalUsers ?> | New today: <?= $newToday ?></p>
  <a href="?logout=1">Logout</a>
</body>
</html>