Session-Based Authentication
This example demonstrates a complete session-based authentication implementation using Stilmark Base's Session, Auth, AuthMiddleware, and CsrfMiddleware classes.
Table of Contents
Application Structure
/your-app
├── public/
│ └── index.php # Entry point
├── src/
│ ├── bootstrap.php # Application bootstrap
│ └── Middleware/
│ └── AppAuthMiddleware.php # Custom auth middleware
├── .env # Environment configuration
└── composer.jsonBootstrap & Configuration
.env Configuration
.env Configuration# Server
SERVER_NAME=example.com
APP_ENV=production
# Session
SESSION_AUTH_NAME=auth
SESSION_COOKIE_SECURE=true
SESSION_COOKIE_SAMESITE=Strict
SESSION_IDLE_TIMEOUT=1800 # 30 minutes
SESSION_ABSOLUTE_TIMEOUT=28800 # 8 hours
# Google OAuth
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=/auth/google/callback
# CSRF
CSRF_ALLOWED_ORIGINS=https://example.com,https://www.example.comsrc/bootstrap.php
src/bootstrap.php<?php
require __DIR__ . '/../vendor/autoload.php';
use Stilmark\Base\Env;
use Stilmark\Base\Session;
// Load environment variables
Env::load(__DIR__ . '/../.env');
// Configure session security
Session::configure([
'cookie_secure' => Env::get('SESSION_COOKIE_SECURE', false),
'cookie_httponly' => true,
'cookie_samesite' => Env::get('SESSION_COOKIE_SAMESITE', 'Lax'),
'use_strict_mode' => true,
'name' => Env::get('SESSION_AUTH_NAME', 'auth') . '_SESSION',
'gc_maxlifetime' => (int) Env::get('SESSION_ABSOLUTE_TIMEOUT', 28800),
]);
// Start session
session_start();Login Flow
Login Route Handler
<?php
use Stilmark\Base\Auth;
use Stilmark\Base\Request;
use Stilmark\Base\Render;
use Stilmark\Base\Session;
// Route: GET /login
function loginPage(Request $request)
{
// Generate CSRF token for the login form
$csrfToken = $request->generateCsrfToken();
// Get flash messages
$error = Session::getFlash('error');
$success = Session::getFlash('success');
// Render login page (example with inline HTML)
?>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<?php if ($error): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="success"><?= htmlspecialchars($success) ?></div>
<?php endif; ?>
<h2>Login with Google</h2>
<form method="POST" action="/auth/google">
<input type="hidden" name="_token" value="<?= $csrfToken ?>">
<button type="submit">Sign in with Google</button>
</form>
</body>
</html>
<?php
}
// Route: POST /auth/google (initiate OAuth)
function initiateGoogleAuth(Request $request)
{
// Validate CSRF token
if (!$request->validateCsrfToken()) {
Session::flash('error', 'Invalid CSRF token');
header('Location: /login');
exit;
}
$auth = new Auth('google');
$auth->callout(); // Redirects to Google
}
// Route: GET /auth/google/callback
function handleGoogleCallback(Request $request)
{
$auth = new Auth('google');
try {
$result = $auth->callback($request);
if ($result['status'] === 'success') {
// Session is already set up by Auth::callback()
// with regenerated session ID and timestamps
Session::flash('success', 'Login successful!');
header('Location: /dashboard');
exit;
} else {
Session::flash('error', $result['message'] ?? 'Login failed');
header('Location: /login');
exit;
}
} catch (Exception $e) {
Session::flash('error', 'Authentication error: ' . $e->getMessage());
header('Location: /login');
exit;
}
}Protected Routes
Custom Auth Middleware with Database Validation
<?php
// src/Middleware/AppAuthMiddleware.php
namespace App\Middleware;
use Stilmark\Base\AuthMiddleware;
use Stilmark\Base\Env;
class AppAuthMiddleware extends AuthMiddleware
{
private $db;
public function __construct($db = null)
{
parent::__construct(
authSessionKey: Env::get('SESSION_AUTH_NAME', 'auth'),
idleTimeout: (int) Env::get('SESSION_IDLE_TIMEOUT', 1800),
absoluteTimeout: (int) Env::get('SESSION_ABSOLUTE_TIMEOUT', 28800)
);
$this->db = $db;
}
/**
* Custom validation: Check if user exists and is active
*/
protected function validateSession(array $sessionData): bool
{
// First, call parent validation
if (!parent::validateSession($sessionData)) {
return false;
}
// If no database connection, skip database validation
if (!$this->db) {
return true;
}
// Get user email from session
$userEmail = $sessionData['user']['email'] ?? null;
if (!$userEmail) {
return false;
}
// Check if user exists and is active in database
$stmt = $this->db->prepare("SELECT id, status FROM users WHERE email = ?");
$stmt->execute([$userEmail]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || $user['status'] !== 'active') {
$this->clearAuthSession();
return false;
}
return true;
}
}Protected Route Example
<?php
use Stilmark\Base\Request;
use Stilmark\Base\Render;
use Stilmark\Base\Session;
use App\Middleware\AppAuthMiddleware;
// Route: GET /dashboard
function dashboard(Request $request, $db)
{
// Create auth middleware with database connection
$authMiddleware = new AppAuthMiddleware($db);
// Validate authentication
if (!$authMiddleware->handle()) {
Session::flash('error', 'Please login to continue');
header('Location: /login');
exit;
}
// Get user data from session
$authData = Session::get('auth');
$user = $authData['user'] ?? null;
// Generate CSRF token for forms on this page
$csrfToken = $request->generateCsrfToken();
?>
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
</head>
<body>
<h1>Welcome, <?= htmlspecialchars($user['name'] ?? 'User') ?>!</h1>
<p>Email: <?= htmlspecialchars($user['email'] ?? '') ?></p>
<h2>Session Info</h2>
<ul>
<li>Provider: <?= htmlspecialchars($authData['provider'] ?? 'N/A') ?></li>
<li>Login Time: <?= date('Y-m-d H:i:s', $authData['auth_time'] ?? 0) ?></li>
<li>Last Activity: <?= date('Y-m-d H:i:s', Session::get('last_activity', 0)) ?></li>
</ul>
<form method="POST" action="/logout">
<input type="hidden" name="_token" value="<?= $csrfToken ?>">
<button type="submit">Logout</button>
</form>
</body>
</html>
<?php
}CSRF Protection
Using CsrfMiddleware with Router
<?php
use Stilmark\Base\Router;
use Stilmark\Base\CsrfMiddleware;
use Stilmark\Base\Env;
$router = new Router();
// Create CSRF middleware
$allowedOrigins = explode(',', Env::get('CSRF_ALLOWED_ORIGINS', ''));
$csrfMiddleware = new CsrfMiddleware($allowedOrigins);
// Apply CSRF protection to all POST/PUT/PATCH/DELETE routes
$router->post('/auth/google', 'initiateGoogleAuth', [$csrfMiddleware]);
$router->post('/logout', 'handleLogout', [$csrfMiddleware]);
$router->post('/api/users', 'createUser', [$csrfMiddleware]);
$router->delete('/api/users/:id', 'deleteUser', [$csrfMiddleware]);Manual CSRF Validation
<?php
use Stilmark\Base\Request;
use Stilmark\Base\Render;
function createUser(Request $request)
{
// Manual CSRF validation (if not using middleware)
if ($request->isUnsafeMethod() && !$request->validateCsrfToken()) {
Render::json(['error' => 'CSRF validation failed'], 403);
exit;
}
// Process user creation
$data = $request->json();
// ... create user logic
Render::json(['success' => true, 'user_id' => 123]);
}JavaScript API Calls with CSRF
<!DOCTYPE html>
<html>
<head>
<title>API Example</title>
</head>
<body>
<button id="createUserBtn">Create User</button>
<script>
// Get CSRF token from meta tag or data attribute
const csrfToken = '<?= $request->generateCsrfToken() ?>';
document.getElementById('createUserBtn').addEventListener('click', async () => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
});
const data = await response.json();
console.log('User created:', data);
} catch (error) {
console.error('Error:', error);
}
});
</script>
</body>
</html>Logout
Logout Handler
<?php
use Stilmark\Base\Auth;
use Stilmark\Base\Request;
use Stilmark\Base\Session;
// Route: POST /logout
function handleLogout(Request $request)
{
// Validate CSRF token
if (!$request->validateCsrfToken()) {
Session::flash('error', 'Invalid CSRF token');
header('Location: /dashboard');
exit;
}
$auth = new Auth();
$auth->logout(); // Destroys session completely
// Redirect to login page
header('Location: /login?logged_out=1');
exit;
}Complete Code
public/index.php - Full Application
public/index.php - Full Application<?php
require __DIR__ . '/../src/bootstrap.php';
use Stilmark\Base\Router;
use Stilmark\Base\Request;
use Stilmark\Base\Render;
use Stilmark\Base\Auth;
use Stilmark\Base\Session;
use Stilmark\Base\CsrfMiddleware;
use Stilmark\Base\Env;
use App\Middleware\AppAuthMiddleware;
// Initialize router
$router = new Router();
// Initialize database (example with PDO)
$db = new PDO(
'mysql:host=localhost;dbname=myapp',
Env::get('DB_USER'),
Env::get('DB_PASS')
);
// Create middleware instances
$allowedOrigins = array_filter(explode(',', Env::get('CSRF_ALLOWED_ORIGINS', '')));
$csrfMiddleware = new CsrfMiddleware($allowedOrigins);
$authMiddleware = new AppAuthMiddleware($db);
// ============================================
// Public Routes
// ============================================
$router->get('/', function() {
header('Location: /login');
exit;
});
$router->get('/login', function(Request $request) {
$csrfToken = $request->generateCsrfToken();
$error = Session::getFlash('error');
$success = Session::getFlash('success');
include __DIR__ . '/../views/login.php';
});
// ============================================
// OAuth Routes (with CSRF protection)
// ============================================
$router->post('/auth/google', function(Request $request) {
$auth = new Auth('google');
$auth->callout();
}, [$csrfMiddleware]);
$router->get('/auth/google/callback', function(Request $request) {
$auth = new Auth('google');
try {
$result = $auth->callback($request);
if ($result['status'] === 'success') {
Session::flash('success', 'Welcome back!');
header('Location: /dashboard');
} else {
Session::flash('error', $result['message'] ?? 'Login failed');
header('Location: /login');
}
} catch (Exception $e) {
Session::flash('error', 'Authentication error');
header('Location: /login');
}
exit;
});
// ============================================
// Protected Routes (with Auth + CSRF)
// ============================================
$router->get('/dashboard', function(Request $request) use ($authMiddleware) {
if (!$authMiddleware->handle()) {
Session::flash('error', 'Please login to continue');
header('Location: /login');
exit;
}
$authData = Session::get('auth');
$user = $authData['user'] ?? null;
$csrfToken = $request->generateCsrfToken();
include __DIR__ . '/../views/dashboard.php';
});
$router->post('/logout', function(Request $request) use ($authMiddleware) {
if (!$authMiddleware->handle()) {
header('Location: /login');
exit;
}
$auth = new Auth();
$auth->logout();
header('Location: /login?logged_out=1');
exit;
}, [$csrfMiddleware]);
// ============================================
// API Routes (with Auth + CSRF)
// ============================================
$router->get('/api/profile', function(Request $request) use ($authMiddleware) {
if (!$authMiddleware->handle()) {
Render::json(['error' => 'Unauthorized'], 401);
exit;
}
$authData = Session::get('auth');
Render::json([
'user' => $authData['user'] ?? null,
'provider' => $authData['provider'] ?? null
]);
}, [$authMiddleware]);
$router->post('/api/users', function(Request $request) use ($authMiddleware, $db) {
if (!$authMiddleware->handle()) {
Render::json(['error' => 'Unauthorized'], 401);
exit;
}
$data = $request->json();
// Validate input
if (!isset($data['name']) || !isset($data['email'])) {
Render::json(['error' => 'Missing required fields'], 400);
exit;
}
// Create user in database
$stmt = $db->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
$stmt->execute([$data['name'], $data['email']]);
Render::json([
'success' => true,
'user_id' => $db->lastInsertId()
]);
}, [$authMiddleware, $csrfMiddleware]);
// ============================================
// Dispatch Router
// ============================================
$router->dispatch();Security Checklist
✅ Session Security
HttpOnly cookies enabled
Secure flag enabled (HTTPS only)
SameSite attribute set
Session ID regenerated after login
Complete session destruction on logout
✅ CSRF Protection
CSRF tokens on all forms
CSRF validation on all unsafe methods
Time-bucketed tokens with rotation
Origin/Referer validation
✅ Timeout Management
Idle timeout (30 minutes default)
Absolute timeout (8 hours default)
Automatic activity tracking
Session cleanup on timeout
✅ Authentication
OAuth2 state validation
Custom session validation
Database user status checks
Protected route enforcement
Testing the Application
1. Test Login Flow
# Visit login page
curl -c cookies.txt http://localhost/login
# Initiate OAuth (will redirect to Google)
curl -b cookies.txt -X POST http://localhost/auth/google \
-d "_token=YOUR_CSRF_TOKEN"2. Test Protected Routes
# Access dashboard without auth (should redirect)
curl -L http://localhost/dashboard
# Access dashboard with session
curl -b cookies.txt http://localhost/dashboard3. Test CSRF Protection
# POST without CSRF token (should fail)
curl -b cookies.txt -X POST http://localhost/api/users \
-H "Content-Type: application/json" \
-d '{"name":"John","email":"john@example.com"}'
# POST with CSRF token (should succeed)
curl -b cookies.txt -X POST http://localhost/api/users \
-H "Content-Type: application/json" \
-H "X-CSRF-TOKEN: YOUR_TOKEN" \
-d '{"name":"John","email":"john@example.com"}'4. Test Timeout
# Login and wait for idle timeout (30 minutes)
# Then try to access protected route (should redirect to login)See Also
Session - Session management utilities
Auth - OAuth2 authentication
AuthMiddleware - Authentication middleware
CsrfMiddleware - CSRF protection
Request - Request handling and CSRF tokens
Last updated