Password-based authentication is becoming outdated. Users forget passwords, reuse weak ones, and create security risks. A passwordless login system using magic links solves these issues by allowing users to log in via a secure link sent to their email. Passwordless Login System is a secure, modern passwordless authentication system built with PHP and MySQLi that uses magic links sent via email for user authentication.
In this tutorial, youβll learn how to build a secure passwordless authentication system in PHP with MySQLi, using email-based magic links.
You will create a passwordless login system that allows users to log in using magic links sent to their email. The system will include:
By the end of this tutorial, you will have a fully functional passwordless login system with:
Hereβs the project structure for the passwordless login system:
passwordless-login-system/ βββ config/ β βββ database.php # Database configuration & connection βββ includes/ β βββ functions.php # Core utility functions β βββ header.php # Header template β βββ footer.php # Footer template βββ public/ β βββ style.css # CSS styling βββ index.php # Home page βββ register.php # Registration page βββ login.php # Login page βββ verify-link.php # Magic link verification βββ dashboard.php # Protected dashboard βββ logout.php # Logout handler
To set up the database, create a MySQL database and run the following SQL to create the necessary tables.
SQL to Create Users Table (users):
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
SQL to Create Magic Tokens Table (magic_tokens):
CREATE TABLE magic_tokens (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
is_used BOOLEAN DEFAULT FALSE,
used_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_token (token),
INDEX idx_expires_at (expires_at)
);
SQL to create Login History Table (optional – for audit logging):
CREATE TABLE login_history (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
ip_address VARCHAR(45),
user_agent VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_login_time (login_time)
);
SQL to create Sessions Table (optional – for explicit session tracking):
CREATE TABLE sessions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
session_token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_expires_at (expires_at)
);
Create a file named database.php inside the config/ directory. This file establishes a connection to the MySQL database using MySQLi and defines constants for database credentials and application configuration.
<?php
// Database credentials
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'passwordless_login_db');
// Create connection using MySQLi
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// Set charset to utf8
$conn->set_charset("utf8mb4");
// Define app constants
define('APP_URL', 'Your_App_URL_Here');
define('SITE_NAME', 'Passwordless Login System');
define('FROM_EMAIL', 'sender@example.com');
define('TOKEN_EXPIRY', 30); // Token expiry in minutes
define('MAGIC_LINK_EXPIRY', 3600); // Magic link expiry in seconds (1 hour)
?>
Create a file named functions.php inside the includes/ directory. This file contains core utility functions for the passwordless login system, including:
generateToken($length = 32): Generates a secure random token of the specified length.createMagicToken($conn, $user_id): Creates a magic token for the specified user.verifyMagicToken($conn, $token): Verifies the provided magic token against the database.markTokenAsUsed($conn, $token_id): Marks the specified magic token as used.createOrGetUser($conn, $email, $name): Creates a new user or retrieves an existing user by email.getUserById($conn, $user_id): Retrieves a user record from the database by ID.getUserByEmail($conn, $email): Retrieves a user record from the database by email.sendMagicLinkEmail($email, $name, $magic_link): Sends a magic link email to the user with the provided details.logLoginActivity($conn, $user_id, $ip_address = null, $user_agent = null): Logs the user’s login activity.createSessionToken($conn, $user_id): Creates a session token for the specified user.verifySessionToken($conn, $token): Verifies the provided session token against the database.invalidateSession($conn, $token): Invalidates the specified session token.isAuthenticated(): Checks if the user is authenticated.getCurrentUserId(): Returns the ID of the currently authenticated user.<?php
/**
* Generate a secure random token
*/
function generateToken($length = 64) {
return bin2hex(random_bytes($length / 2));
}
/**
* Create a magic link token for user
* @param mysqli $conn - Database connection
* @param int $user_id - User ID
* @return string - Generated token
*/
function createMagicToken($conn, $user_id) {
$token = generateToken();
$expires_at = date('Y-m-d H:i:s', strtotime('+1 hour'));
$stmt = $conn->prepare("INSERT INTO magic_tokens (user_id, token, expires_at) VALUES (?, ?, ?)");
$stmt->bind_param("iss", $user_id, $token, $expires_at);
if ($stmt->execute()) {
$stmt->close();
return $token;
}
$stmt->close();
return false;
}
/**
* Verify and validate magic token
* @param mysqli $conn - Database connection
* @param string $token - Token to verify
* @return array|false - User data if valid, false otherwise
*/
function verifyMagicToken($conn, $token) {
// Check if token exists and is not expired or already used
$stmt = $conn->prepare("
SELECT mt.id as token_id, mt.user_id, u.id, u.email, u.name, mt.is_used
FROM magic_tokens mt
JOIN users u ON mt.user_id = u.id
WHERE mt.token = ?
AND mt.is_used = FALSE
AND mt.expires_at > NOW()
");
$stmt->bind_param("s", $token);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
$stmt->close();
return false;
}
$userData = $result->fetch_assoc();
$stmt->close();
return $userData;
}
/**
* Mark magic token as used
* @param mysqli $conn - Database connection
* @param int $token_id - Magic token ID
* @return bool - True if successful
*/
function markTokenAsUsed($conn, $token_id) {
$used_at = date('Y-m-d H:i:s');
$stmt = $conn->prepare("UPDATE magic_tokens SET is_used = TRUE, used_at = ? WHERE id = ?");
$stmt->bind_param("si", $used_at, $token_id);
$result = $stmt->execute();
$stmt->close();
return $result;
}
/**
* Create or get user
* @param mysqli $conn - Database connection
* @param string $email - User email
* @param string $name - User name
* @return int|false - User ID if successful, false otherwise
*/
function createOrGetUser($conn, $email, $name) {
// Check if user already exists
$stmt = $conn->prepare("SELECT id FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$user = $result->fetch_assoc();
$stmt->close();
return $user['id'];
}
$stmt->close();
// Create new user
$stmt = $conn->prepare("INSERT INTO users (email, name) VALUES (?, ?)");
$stmt->bind_param("ss", $email, $name);
if ($stmt->execute()) {
$user_id = $conn->insert_id;
$stmt->close();
return $user_id;
}
$stmt->close();
return false;
}
/**
* Get user by ID
* @param mysqli $conn - Database connection
* @param int $user_id - User ID
* @return array|false - User data if found, false otherwise
*/
function getUserById($conn, $user_id) {
$stmt = $conn->prepare("SELECT id, email, name, created_at FROM users WHERE id = ?");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
$stmt->close();
return false;
}
$user = $result->fetch_assoc();
$stmt->close();
return $user;
}
/**
* Get user by email
* @param mysqli $conn - Database connection
* @param string $email - User email
* @return array|false - User data if found, false otherwise
*/
function getUserByEmail($conn, $email) {
$stmt = $conn->prepare("SELECT id, email, name, created_at FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
$stmt->close();
return false;
}
$user = $result->fetch_assoc();
$stmt->close();
return $user;
}
/**
* Send magic link email
* @param string $email - Recipient email
* @param string $name - Recipient name
* @param string $magic_link - Complete magic link URL
* @return bool - True if email sent successfully
*/
function sendMagicLinkEmail($email, $name, $magic_link) {
$subject = "Your Magic Login Link - " . SITE_NAME;
$message = "
<html>
<head>
<title>Magic Login Link</title>
</head>
<body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>
<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>
<h2 style='color: #2c3e50;'>Welcome to " . SITE_NAME . "</h2>
<p>Hi <strong>" . htmlspecialchars($name) . "</strong>,</p>
<p>You have requested a magic login link. Click the button below to login to your account:</p>
<div style='text-align: center; margin: 30px 0;'>
<a href='" . htmlspecialchars($magic_link) . "'
style='display: inline-block; padding: 12px 30px; background-color: #3498db;
color: white; text-decoration: none; border-radius: 5px; font-weight: bold;'>
Login to Your Account
</a>
</div>
<p>Or copy and paste this link in your browser:</p>
<p style='word-break: break-all; background-color: #ecf0f1; padding: 10px; border-radius: 3px;'>
" . htmlspecialchars($magic_link) . "
</p>
<p style='color: #e74c3c; font-weight: bold;'>
β οΈ This link will expire in 1 hour.
</p>
<p>If you didn't request this link, you can safely ignore this email.</p>
<hr style='border: none; border-top: 1px solid #ecf0f1; margin: 20px 0;'>
<p style='color: #7f8c8d; font-size: 12px;'>
This is an automated email. Please don't reply to this message.
</p>
</div>
</body>
</html>
";
// Email headers
$headers = "MIME-Version: 1.0" . "\r\n";
$headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
//$headers .= "From: ".SITE_NAME." <noreply@" . $_SERVER['HTTP_HOST'] . ">\r\n";
$headers .= "From: ".SITE_NAME." <" . FROM_EMAIL . ">\r\n";
// Send email
return mail($email, $subject, $message, $headers);
}
/**
* Log login activity
* @param mysqli $conn - Database connection
* @param int $user_id - User ID
* @param string $ip_address - User IP address
* @param string $user_agent - User agent string
* @return bool - True if logged successfully
*/
function logLoginActivity($conn, $user_id, $ip_address = null, $user_agent = null) {
if ($ip_address === null) {
$ip_address = $_SERVER['REMOTE_ADDR'] ?? '';
}
if ($user_agent === null) {
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
}
$stmt = $conn->prepare("INSERT INTO login_history (user_id, ip_address, user_agent) VALUES (?, ?, ?)");
$stmt->bind_param("iss", $user_id, $ip_address, $user_agent);
$result = $stmt->execute();
$stmt->close();
return $result;
}
/**
* Create session token
* @param mysqli $conn - Database connection
* @param int $user_id - User ID
* @return string|false - Session token if successful, false otherwise
*/
function createSessionToken($conn, $user_id) {
$session_token = generateToken();
$expires_at = date('Y-m-d H:i:s', strtotime('+30 days'));
$stmt = $conn->prepare("INSERT INTO sessions (user_id, session_token, expires_at) VALUES (?, ?, ?)");
$stmt->bind_param("iss", $user_id, $session_token, $expires_at);
if ($stmt->execute()) {
$stmt->close();
return $session_token;
}
$stmt->close();
return false;
}
/**
* Verify session token
* @param mysqli $conn - Database connection
* @param string $token - Session token
* @return int|false - User ID if valid, false otherwise
*/
function verifySessionToken($conn, $token) {
$stmt = $conn->prepare("
SELECT user_id FROM sessions
WHERE session_token = ?
AND expires_at > NOW()
");
$stmt->bind_param("s", $token);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
$stmt->close();
return false;
}
$session = $result->fetch_assoc();
$stmt->close();
return $session['user_id'];
}
/**
* Invalidate session
* @param mysqli $conn - Database connection
* @param string $token - Session token
* @return bool - True if successful
*/
function invalidateSession($conn, $token) {
$stmt = $conn->prepare("DELETE FROM sessions WHERE session_token = ?");
$stmt->bind_param("s", $token);
$result = $stmt->execute();
$stmt->close();
return $result;
}
/**
* Check if user is authenticated
* @return bool - True if authenticated, false otherwise
*/
function isAuthenticated() {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
/**
* Get current logged-in user ID
* @return int|false - User ID if authenticated, false otherwise
*/
function getCurrentUserId() {
if (isAuthenticated()) {
return $_SESSION['user_id'];
}
return false;
}
/**
* Sanitize email
* @param string $email - Email to sanitize
* @return string - Sanitized email
*/
function sanitizeEmail($email) {
return filter_var(trim($email), FILTER_SANITIZE_EMAIL);
}
/**
* Validate email format
* @param string $email - Email to validate
* @return bool - True if valid, false otherwise
*/
function isValidEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
/**
* Sanitize name
* @param string $name - Name to sanitize
* @return string - Sanitized name
*/
function sanitizeName($name) {
return htmlspecialchars(trim($name), ENT_QUOTES, 'UTF-8');
}
?>
Create two files named header.php and footer.php inside the includes/ directory. These files contain the HTML structure for the header and footer of the application, including links to CSS stylesheets and any necessary JavaScript files.
The header.php file includes the opening HTML tags, meta tags, and links to the CSS stylesheet. It also checks for user authentication and retrieves the current user’s information if authenticated.
<?php
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Include configuration and functions
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/functions.php';
// Check authentication if needed
$is_authenticated = isAuthenticated();
$current_user = null;
if ($is_authenticated) {
$current_user = getUserById($conn, $_SESSION['user_id']);
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo isset($page_title) ? htmlspecialchars($page_title) . ' - ' . SITE_NAME : SITE_NAME; ?></title>
<link rel="stylesheet" href="<?php echo APP_URL; ?>/public/style.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<div class="nav-brand">
<a href="<?php echo APP_URL; ?>/index.php"><?php echo SITE_NAME; ?></a>
</div>
<div class="nav-menu">
<?php if ($is_authenticated): ?>
<span class="nav-item">Welcome, <?php echo htmlspecialchars($current_user['name']); ?></span>
<a href="<?php echo APP_URL; ?>/dashboard.php" class="nav-item">Dashboard</a>
<a href="<?php echo APP_URL; ?>/logout.php" class="nav-item btn-logout">Logout</a>
<?php else: ?>
<a href="<?php echo APP_URL; ?>/login.php" class="nav-item">Login</a>
<a href="<?php echo APP_URL; ?>/register.php" class="nav-item">Register</a>
<?php endif; ?>
</div>
</div>
</nav>
<div class="container">
The footer.php file includes the closing HTML tags.
</div>
<footer class="footer">
<div class="container">
<p>© <?php echo date('Y'); ?> <?php echo SITE_NAME; ?>. All rights reserved.</p>
</div>
</footer>
</body>
</html>
The index.php file serves as the home page of the application. It includes the header and footer templates and displays a welcome message. If the user is authenticated, it shows a link to the dashboard; otherwise, it provides links to the registration and login pages.
<?php
$page_title = 'Home';
require_once __DIR__ . '/includes/header.php';
?>
<div class="content-wrapper">
<div class="hero-section">
<h1>Welcome to <?php echo SITE_NAME; ?></h1>
<p>A secure, passwordless authentication system using magic links sent to your email</p>
<div class="cta-buttons">
<?php if (!isAuthenticated()): ?>
<a href="<?php echo APP_URL; ?>/login.php" class="btn btn-primary">Login</a>
<a href="<?php echo APP_URL; ?>/register.php" class="btn btn-secondary">Register</a>
<?php else: ?>
<a href="<?php echo APP_URL; ?>/dashboard.php" class="btn btn-primary">Go to Dashboard</a>
<?php endif; ?>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
The register.php file allows users to register by providing their name and email. It includes form validation and calls the createOrGetUser() function to create a new user or retrieve an existing user. Upon successful registration, it create a magic token and sends a magic link email to the user.
<?php
$page_title = 'Register';
require_once __DIR__ . '/includes/header.php';
// If already logged in, redirect to dashboard
if (isAuthenticated()) {
header('Location: ' . APP_URL . '/dashboard.php');
exit();
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = sanitizeEmail($_POST['email'] ?? '');
$name = sanitizeName($_POST['name'] ?? '');
// Validate inputs
if (empty($email)) {
$error = 'Email is required';
} elseif (!isValidEmail($email)) {
$error = 'Please enter a valid email address';
} elseif (empty($name)) {
$error = 'Name is required';
} elseif (strlen($name) > 100) {
$error = 'Name must not exceed 100 characters';
} else {
// Check if user already exists
if ($existing_user = getUserByEmail($conn, $email)) {
$error = 'An account with this email already exists. Please <a href="' . APP_URL . '/login.php">login instead</a>.';
} else {
// Create user
$user_id = createOrGetUser($conn, $email, $name);
if ($user_id) {
// Create magic token
$token = createMagicToken($conn, $user_id);
if ($token) {
// Create magic link
$magic_link = APP_URL . '/verify-link.php?token=' . $token;
// Send email
if (sendMagicLinkEmail($email, $name, $magic_link)) {
$success = 'Registration successful! Please check your email for a magic link to complete registration.';
} else {
$error = 'Registration successful, but we could not send the magic link email. Please try to <a href="' . APP_URL . '/login.php">login</a> and request a new link.';
}
} else {
$error = 'Error creating login token. Please try again.';
}
} else {
$error = 'An error occurred during registration. Please try again.';
}
}
}
}
?>
<div class="auth-container">
<div class="auth-box">
<h1>Create Account</h1>
<p class="auth-subtitle">Join us to get started</p>
<?php if (!empty($error)): ?>
<div class="alert alert-error">
<?php echo $error; ?>
</div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success">
<?php echo $success; ?>
</div>
<?php endif; ?>
<form method="POST" class="auth-form">
<div class="form-group">
<label for="name">Full Name</label>
<input type="text" id="name" name="name" required
placeholder="Enter your full name"
value="<?php echo htmlspecialchars($_POST['name'] ?? ''); ?>">
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required
placeholder="Enter your email address"
value="<?php echo htmlspecialchars($_POST['email'] ?? ''); ?>">
</div>
<button type="submit" class="btn btn-primary btn-block">Register</button>
</form>
<p class="auth-footer">
Already have an account? <a href="<?php echo APP_URL; ?>/login.php">Login here</a>
</p>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
The login.php file allows users to request a magic link for login. Users enter their email, and if the email exists in the database, a magic token is created and a magic link email is sent to the user.
<?php
$page_title = 'Login';
require_once __DIR__ . '/includes/header.php';
// If already logged in, redirect to dashboard
if (isAuthenticated()) {
header('Location: ' . APP_URL . '/dashboard.php');
exit();
}
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = sanitizeEmail($_POST['email'] ?? '');
// Validate input
if (empty($email)) {
$error = 'Email is required';
} elseif (!isValidEmail($email)) {
$error = 'Please enter a valid email address';
} else {
// Check if user exists
$user = getUserByEmail($conn, $email);
if (!$user) {
// Security: Don't reveal if email exists
$error = 'If an account exists with this email, a magic link has been sent to your inbox.';
} else {
// Create magic token
$token = createMagicToken($conn, $user['id']);
if ($token) {
// Create magic link
$magic_link = APP_URL . '/verify-link.php?token=' . $token;
// Send email
if (sendMagicLinkEmail($email, $user['name'], $magic_link)) {
$success = 'If an account exists with this email, a magic link has been sent to your inbox. Please check your email (and spam folder).';
} else {
$error = 'Error sending magic link email. Please try again later.';
}
} else {
$error = 'Error creating login token. Please try again.';
}
}
}
}
?>
<div class="auth-container">
<div class="auth-box">
<h1>Login</h1>
<p class="auth-subtitle">Enter your email to receive a magic login link</p>
<?php if (!empty($error)): ?>
<div class="alert alert-error">
<?php echo $error; ?>
</div>
<?php endif; ?>
<?php if (!empty($success)): ?>
<div class="alert alert-success">
<?php echo $success; ?>
</div>
<?php endif; ?>
<form method="POST" class="auth-form">
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required
placeholder="Enter your email address"
value="<?php echo htmlspecialchars($_POST['email'] ?? ''); ?>">
</div>
<button type="submit" class="btn btn-primary btn-block">Send Magic Link</button>
</form>
<p class="auth-footer">
Don't have an account? <a href="<?php echo APP_URL; ?>/register.php">Register here</a>
</p>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
The verify-link.php file handles the verification of magic links. It retrieves the token from the URL, verifies it using the verifyMagicToken() function, and if valid, marks the token as used, creates a session token, logs the login activity, and redirects the user to the dashboard.
<?php
$page_title = 'Verify Login';
require_once __DIR__ . '/includes/header.php';
// If already logged in, redirect to dashboard
if (isAuthenticated()) {
header('Location: ' . APP_URL . '/dashboard.php');
exit();
}
$error = '';
$success = '';
$token = trim($_GET['token'] ?? '');
if (empty($token)) {
$error = 'Invalid verification link. No token provided.';
} else {
// Verify the token
$user_data = verifyMagicToken($conn, $token);
if (!$user_data) {
$error = 'The magic link is invalid, expired, or has already been used. Please <a href="' . APP_URL . '/login.php">request a new one</a>.';
} else {
// Token is valid
$user_id = $user_data['user_id'];
$token_id = $user_data['token_id'];
// Mark token as used
if (markTokenAsUsed($conn, $token_id)) {
// Log login activity
logLoginActivity($conn, $user_id);
// Create session
$_SESSION['user_id'] = $user_id;
$_SESSION['user_email'] = $user_data['email'];
$_SESSION['user_name'] = $user_data['name'];
$success = 'Login successful! Redirecting to dashboard...';
// Redirect to dashboard
header('Refresh: 2; URL=' . APP_URL . '/dashboard.php');
} else {
$error = 'Error during login process. Please try again.';
}
}
}
?>
<div class="verify-container">
<div class="verify-box">
<h1>Email Verification</h1>
<?php if (!empty($error)): ?>
<div class="alert alert-error">
<?php echo $error; ?>
</div>
<div class="action-buttons">
<a href="<?php echo APP_URL; ?>/login.php" class="btn btn-primary">Back to Login</a>
</div>
<?php elseif (!empty($success)): ?>
<div class="alert alert-success">
<?php echo $success; ?>
</div>
<p style="text-align: center; margin-top: 20px;">
<a href="<?php echo APP_URL; ?>/dashboard.php" class="btn btn-primary">Go to Dashboard</a>
</p>
<?php else: ?>
<div class="alert alert-info">
Verifying your email...
</div>
<?php endif; ?>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
The dashboard.php file is a protected page that only authenticated users can access. It includes the header and footer templates and displays a welcome message to the logged-in user, along with a logout link.
<?php
$page_title = 'Dashboard';
require_once __DIR__ . '/includes/header.php';
// Check if user is authenticated
if (!isAuthenticated()) {
header('Location: ' . APP_URL . '/login.php');
exit();
}
$user = getUserById($conn, $_SESSION['user_id']);
// Get login history
$stmt = $conn->prepare("
SELECT login_time, ip_address, user_agent
FROM login_history
WHERE user_id = ?
ORDER BY login_time DESC
LIMIT 10
");
$stmt->bind_param("i", $_SESSION['user_id']);
$stmt->execute();
$login_history = $stmt->get_result();
$stmt->close();
?>
<div class="dashboard-container">
<div class="dashboard-header">
<h1>Welcome, <?php echo htmlspecialchars($user['name']); ?>!</h1>
<p class="dashboard-subtitle">You are securely logged in</p>
</div>
<div class="dashboard-content">
<div class="info-card">
<h2>Account Information</h2>
<div class="info-row">
<span class="info-label">Email:</span>
<span class="info-value"><?php echo htmlspecialchars($user['email']); ?></span>
</div>
<div class="info-row">
<span class="info-label">Name:</span>
<span class="info-value"><?php echo htmlspecialchars($user['name']); ?></span>
</div>
<div class="info-row">
<span class="info-label">Member Since:</span>
<span class="info-value"><?php echo date('F j, Y', strtotime($user['created_at'])); ?></span>
</div>
</div>
<div class="info-card">
<h2>Recent Login History</h2>
<?php if ($login_history->num_rows > 0): ?>
<div class="table-responsive">
<table class="history-table">
<thead>
<tr>
<th>Date & Time</th>
<th>IP Address</th>
<th>User Agent</th>
</tr>
</thead>
<tbody>
<?php while ($row = $login_history->fetch_assoc()): ?>
<tr>
<td><?php echo date('M j, Y H:i:s', strtotime($row['login_time'])); ?></td>
<td><?php echo htmlspecialchars($row['ip_address'] ?? 'Unknown'); ?></td>
<td class="truncate"><?php echo htmlspecialchars($row['user_agent'] ?? 'Unknown', ENT_QUOTES); ?></td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p>No login history available yet.</p>
<?php endif; ?>
</div>
<div class="action-card">
<h2>Account Actions</h2>
<a href="<?php echo APP_URL; ?>/logout.php" class="btn btn-danger">Logout</a>
</div>
</div>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
The logout.php file handles user logout by invalidating the session token and destroying the session. It then redirects the user back to the home page.
<?php
/**
* Logout Script
*/
require_once __DIR__ . '/config/database.php';
require_once __DIR__ . '/includes/functions.php';
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Destroy session
if (isset($_SESSION['user_id'])) {
$user_id = $_SESSION['user_id'];
// Optional: Log logout activity
// logLogoutActivity($conn, $user_id);
}
// Clear session data
$_SESSION = array();
// Destroy the session cookie
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Destroy session
session_destroy();
// Redirect to home page
header('Location: ' . APP_URL . '/index.php?logout=success');
exit();
?>
In this passwordless login system using PHP and MySQLi, we have implemented several security best practices to ensure the safety of user data and prevent unauthorized access. Here are some key security measures to consider when building a passwordless authentication system:
Youβve now built a secure passwordless login system in PHP using magic links. By leveraging magic links sent via email, you have created a modern authentication system that eliminates the need for passwords, enhancing security and user experience. This modern authentication approach improves both security and user experience, making it ideal for SaaS apps, admin panels, and membership systems. You can further enhance this system by adding features like multi-factor authentication, social login integration, and more robust user management capabilities. Happy coding!
Looking for expert assistance to implement or extend this scriptβs functionality? Submit a Service Request
π° Budget-friendly β’ π Global clients β’ π Production-ready solutions