Skip to content

sokkian/SimpleAuth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SimpleAuth

A lightweight, secure PHP library for passwordless authentication using magic links (one-time login tokens).

Project Status

⚠️ Learning/educational project - not actively maintained. Feel free to fork and adapt it for your needs.

Features

  • Passwordless authentication: Secure login via email magic links
  • Replay protection: One-time use tokens with nonce validation
  • Clock skew tolerance: Configurable grace period for time synchronization
  • Immutable result objects: Type-safe error handling without exceptions
  • Internationalization: Built-in support for multiple languages
  • Zero dependencies: Pure PHP with no external libraries required
  • PSR-4 compatible: Easy integration via autoloading

Requirements

  • PHP 7.4 or higher
  • PDO extension with MySQL support
  • MySQL 5.7+ or MariaDB 10.2+

Installation

Via Composer (Recommended)

composer require sokkian/simpleauth

Manual Installation

  1. Download or clone this repository
  2. Copy the src/ directory to your project:
your-project/
├── src/
│   ├── Token.php
│   ├── Verifier.php
│   ├── Result.php
│   └── locales/
│       ├── en_US.php
│       ├── es_ES.php
│       └── it_IT.php
└── autoload.php
  1. Create an autoloader file in your project root:

autoload.php:

<?php
/**
 * SimpleAuth Autoloader
 * 
 * PSR-4 autoloader for manual installation
 */

spl_autoload_register(function ($class) {
    $prefix = 'SimpleAuth\\';
    $base_dir = __DIR__ . '/src/';
    
    $len = strlen($prefix);
    if (strncmp($prefix, $class, $len) !== 0) {
        return;
    }
    
    $relative_class = substr($class, $len);
    $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
    
    if (file_exists($file)) {
        require $file;
    }
});

Database Setup

Run the SQL schema to create required tables:

mysql -u your_user -p your_database < schema.sql

Or manually create tables:

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE access_tokens (
    id INT PRIMARY KEY AUTO_INCREMENT,
    token VARCHAR(64) UNIQUE NOT NULL,
    user_id INT NOT NULL,
    jti VARCHAR(32) NOT NULL,
    expires DATETIME NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    INDEX idx_token (token),
    INDEX idx_expires (expires)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE nonces (
    jti VARCHAR(32) PRIMARY KEY,
    expires DATETIME NOT NULL,
    consumed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_expires (expires)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Quick Start

1. Generate a Magic Link

<?php
// Autoload (works for both Composer and manual installation)
if (file_exists('vendor/autoload.php')) {
    require_once 'vendor/autoload.php';
} else {
    require_once 'autoload.php';
}

use SimpleAuth\Token;

// Database connection
$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');

// Generate token for user ID 1
$tokenGenerator = new Token($db);
$token = $tokenGenerator->generate(1, 900); // 900 seconds = 15 minutes

// Build magic link
$magicLink = 'https://example.com/auth/verify.php?t=' . $token;

// Send via email
mail('user@example.com', 'Login Link', "Click to login: $magicLink");

2. Verify the Token

<?php
// Autoload (works for both Composer and manual installation)
if (file_exists('vendor/autoload.php')) {
    require_once 'vendor/autoload.php';
} else {
    require_once 'autoload.php';
}

use SimpleAuth\Verifier;

session_start();

$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$verifier = new Verifier($db, 120); // 120 seconds clock skew tolerance

$token = $_GET['t'] ?? '';
$result = $verifier->verify($token);

if ($result->isOk()) {
    $_SESSION['user_id'] = $result->getUserId();
    header('Location: /dashboard.php');
    exit;
} else {
    echo 'Error: ' . $result->getReason();
}

Complete Testing Example

Here's a minimal working example to test the installation:

test-login.php (Request magic link)

<?php
// Autoload (works for both Composer and manual installation)
if (file_exists('vendor/autoload.php')) {
    require_once 'vendor/autoload.php';
} else {
    require_once 'autoload.php';
}

use SimpleAuth\Token;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
    
    if (!$email) {
        die('Invalid email address');
    }
    
    try {
        $db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
        $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        
        // Find user
        $stmt = $db->prepare("SELECT id, name FROM users WHERE email = ?");
        $stmt->execute([$email]);
        $user = $stmt->fetch();
        
        if (!$user) {
            // Don't reveal if user exists
            $message = 'If an account exists, you will receive a login link.';
        } else {
            // Generate token
            $tokenGenerator = new Token($db);
            $token = $tokenGenerator->generate($user['id'], 900);
            
            // Build magic link
            $magicLink = 'http://localhost/test-verify.php?t=' . $token;
            
            // For testing: display link instead of sending email
            $message = 'Magic link (for testing): <a href="' . $magicLink . '">' . $magicLink . '</a>';
        }
    } catch (Exception $e) {
        die('Error: ' . $e->getMessage());
    }
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Test SimpleAuth - Login</title>
</head>
<body>
    <h1>SimpleAuth Test - Request Login</h1>
    
    <?php if (isset($message)): ?>
        <p><?= $message ?></p>
        <p><a href="test-login.php">Try another email</a></p>
    <?php else: ?>
        <form method="POST">
            <label>Email:</label><br>
            <input type="email" name="email" required><br><br>
            <button type="submit">Send Magic Link</button>
        </form>
    <?php endif; ?>
</body>
</html>

test-verify.php (Verify token)

<?php
// Autoload (works for both Composer and manual installation)
if (file_exists('vendor/autoload.php')) {
    require_once 'vendor/autoload.php';
} else {
    require_once 'autoload.php';
}

use SimpleAuth\Verifier;

session_start();

$token = $_GET['t'] ?? '';

if (empty($token)) {
    die('No token provided');
}

try {
    $db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
    $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    $verifier = new Verifier($db, 120);
    $result = $verifier->verify($token);
    
    if ($result->isOk()) {
        $_SESSION['user_id'] = $result->getUserId();
        $success = true;
    } else {
        $error = $result->getReason();
    }
} catch (Exception $e) {
    die('Error: ' . $e->getMessage());
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Test SimpleAuth - Verify</title>
</head>
<body>
    <h1>SimpleAuth Test - Verification</h1>
    
    <?php if (isset($success)): ?>
        <p><strong>Success!</strong> You are now logged in.</p>
        <p>User ID: <?= $_SESSION['user_id'] ?></p>
        <p><a href="test-logout.php">Logout</a></p>
    <?php else: ?>
        <p><strong>Authentication Failed</strong></p>
        <p>Error code: <?= htmlspecialchars($error) ?></p>
        <p><a href="test-login.php">Try again</a></p>
    <?php endif; ?>
</body>
</html>

test-logout.php (Clear session)

<?php
session_start();
session_destroy();
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Logged Out</title>
</head>
<body>
    <h1>Logged Out</h1>
    <p>You have been logged out successfully.</p>
    <p><a href="test-login.php">Login again</a></p>
</body>
</html>

Testing steps:

  1. Insert a test user: INSERT INTO users (email, name) VALUES ('test@example.com', 'Test User');
  2. Open test-login.php in your browser
  3. Enter test@example.com
  4. Click the magic link displayed
  5. Verify you're logged in

API Reference

Token Class

__construct(PDO $db)

Create a new token generator.

Parameters:

  • $db - PDO database connection

generate(int $user_id, int $ttlSeconds = 900): string

Generate a new magic link token.

Parameters:

  • $user_id - User ID from users table
  • $ttlSeconds - Time to live in seconds (default: 900 = 15 minutes)

Returns: string - The generated token

Example:

$token = $tokenGenerator->generate(123, 600); // 10 minutes

cleanup(int $retentionWeeks = 4): array

Delete expired tokens and nonces.

Parameters:

  • $retentionWeeks - Keep records for this many weeks after expiration

Returns: array with keys:

  • tokens_deleted - Number of tokens deleted
  • nonces_deleted - Number of nonces deleted

Example:

$stats = $tokenGenerator->cleanup(4);
echo "Deleted {$stats['tokens_deleted']} tokens";

Verifier Class

__construct(PDO $db, int $clockSkewSeconds = 120)

Create a new token verifier.

Parameters:

  • $db - PDO database connection
  • $clockSkewSeconds - Clock skew tolerance in seconds (default: 120)

verify(string $token): Result

Verify a magic link token.

Parameters:

  • $token - The token to verify

Returns: Result object

Example:

$result = $verifier->verify($token);
if ($result->isOk()) {
    $userId = $result->getUserId();
}

verifyFromUrl(string $url, string $paramName = 't'): Result

Extract and verify token from URL.

Parameters:

  • $url - Complete URL with token parameter
  • $paramName - Query parameter name (default: 't')

Returns: Result object

Result Class

Constants (Error Codes)

Constant Value Description
TOKEN_NOT_FOUND token_not_found Token doesn't exist in database
TOKEN_EXPIRED token_expired Token has expired
TOKEN_ALREADY_USED token_already_used Replay attack detected
MISSING_TOKEN missing_token No token provided in URL

Methods

isOk(): bool
Returns true if verification succeeded.

isFailed(): bool
Returns true if verification failed.

getReason(): ?string
Returns error code (null if success).

getData(): ?array
Returns success data array (null if failed).

getUserId(): ?int
Returns authenticated user ID (null if failed).

Internationalization

SimpleAuth includes translations for error messages in multiple languages.

Supported locales:

  • en_US - English (United States)
  • es_ES - Spanish (Spain)
  • it_IT - Italian (Italy)

Customizing messages:

Messages are stored in src/locales/{locale}.php. To override messages in your application:

  1. Create directory: src/App/locales/simpleauth/
  2. Create locale file: src/App/locales/simpleauth/es_ES.php
  3. Override specific messages:
<?php
return [
    'token_expired' => 'Tu enlace ha caducado. Solicita uno nuevo.',
    // Only override what you need
];

See src/locales/README.md for available message IDs.

Security Best Practices

  1. Always use HTTPS for magic link URLs
  2. Short TTL: Keep token lifetime short (5-15 minutes recommended)
  3. Rate limiting: Limit magic link requests per email/IP
  4. Email validation: Verify email ownership before generating tokens
  5. Cleanup regularly: Run cleanup() daily via cron job
  6. Monitor nonces: Alert on unusual replay attack attempts
  7. Secure sessions: Use secure session configuration after authentication

Maintenance

Cleanup Cron Job

Add to your crontab to run daily cleanup:

0 2 * * * /usr/bin/php /path/to/cleanup.php

cleanup.php:

<?php
// Autoload
if (file_exists('vendor/autoload.php')) {
    require_once 'vendor/autoload.php';
} else {
    require_once 'autoload.php';
}

use SimpleAuth\Token;

$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$tokenGenerator = new Token($db);
$stats = $tokenGenerator->cleanup(4);

echo "Cleanup completed: {$stats['tokens_deleted']} tokens, {$stats['nonces_deleted']} nonces deleted\n";

Troubleshooting

Problem: Token always shows as "not found"
Solution: Check that the token is being passed correctly in the URL parameter

Problem: Token shows as "expired" immediately
Solution: Check server time synchronization. Increase clockSkewSeconds if needed.

Problem: "Token already used" on first attempt
Solution: Check for duplicate requests. Ensure the token isn't being consumed multiple times.

Problem: Database errors
Solution: Verify all tables are created and foreign keys are properly set up.

Problem: Autoloader not working (manual installation)
Solution: Verify autoload.php is in the project root and the src/ path is correct.

License

MIT License - see LICENSE file for details.

Forking and Using This Project

This project is a learning exercise and not actively maintained. You are encouraged to:

  • Fork this repository and adapt it for your own projects
  • Modify the code to fit your specific requirements
  • Use it as a reference for understanding passwordless authentication
  • Build upon it and create your own improved versions

If you create something interesting based on this work, feel free to share it (but not required).

Support

For questions, review the documentation above or fork the project to experiment. Limited support available at support@sokkian.net.

Changelog

1.0.0 (2025-01-15)

  • Initial release
  • Magic link token generation
  • Token verification with replay protection
  • Multi-language support (en_US, es_ES, it_IT)
  • Clock skew tolerance
  • PSR-4 autoloading

About

A lightweight, secure PHP library for passwordless authentication using magic links.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages