MikroTik Hotspot with RADIUS and Custom Portal


๐Ÿ“Œ Objective

Create a wireless hotspot network where users can sign up or log in via a custom splash page hosted on an external Linux web server, which integrates with a FreeRADIUS server for authentication.


๐Ÿ”ง 1. Infrastructure Overview

Devices and Services:

ComponentRoleIP Address
MikroTik RouterHotspot gateway and CAPsMAN controllerWAN 10.2.0.6 LAN (GUEST) 10.0.30.1/24
Web Server (Linux)Hosts login.html, signup.php, style.css10.2.0.9
DNS HostnameSplash page hostnamehotspot.mikrotikmasters.com
FreeRADIUSAuthentication backend (not covered here)10.2.0.9

NB: FreeRADIUS and Login server running on the same Ubuntu Server

To test logging a user in you can put this URL in the browser:

http://<router-ip-or-hostname>/login?username=<username>&password=<password>

๐ŸŒ 2. DNS and Host Resolution

To ensure users get redirected correctly to the splash page, the MikroTik hotspot system resolves:

hotspot.mikrotikmasters.com โ†’ 10.2.0.6
login.mikrotikmasters.com โ†’ 10.2.0.9

On the Web Server:

Edit /etc/hosts:

sudo nano /etc/hosts

Add:

10.2.0.6    hotspot.mikrotikmasters.com

This ensures PHP CURL requests from the server can resolve the hostname during login.

On the mikroTik

/ip dns static
add address=10.2.0.9 name=login.mikrotikmasters.com type=A

๐Ÿ“ก 3. Hotspot Configuration

MikroTik config:

  • Hotspot is configured on a VLAN/subinterface (e.g., vlan30::GUEST)
  • Uses a DNS name: hotspot.mikrotikmasters.com
  • The login page is hosted externally (index.html not on router)
  • RADIUS authentication is active for the hotspot
  • CAPsMAN is used to control access points with a virtual AP broadcasting the Hotspot SSID

Hotspot Redirect

In order to get the hotspot to redirect to your login server, we need to replace the existing hotspot/login.html file with this:

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="refresh" content="0; url=http://login.mikrotikmasters.com/login/">
  </head>
  <body>
    <p>Redirecting...</p>
  </body>
</html>

๐Ÿงพ 4. Web Portal Files and Functions

๐Ÿ”น index.php

  • This is the custom splash page shown to users when they connect to WiFi.
  • It has:
    • White background
    • Login form inside a centered box (440 x 415px)
    • Rounded edges, logo at top (img/login-logo.png)
    • Responsive for mobile/tablet
  • Action: sends POST to http://hotspot.mikrotikmasters.com/login (MikroTik router)
  • Optional button: links to signup.php
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>WiFi Login</title>
  <link rel="stylesheet" href="style.css">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<?php
$prefill_username = isset($_GET['username']) ? htmlspecialchars($_GET['username']) : '';
$prefill_password = isset($_GET['password']) ? htmlspecialchars($_GET['password']) : '';

$link_login = $_GET['link-login'] ?? 'http://hotspot.mikrotikmasters.com/login';
$dst = $_GET['dst'] ?? 'http://example.com';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = $_POST['username'];
    $password = $_POST['password'];

    $data = [
        'username' => $username,
        'password' => $password
    ];

    $ch = curl_init($link_login);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
    $response = curl_exec($ch);
    curl_close($ch);

    // Optional: check for login success in $response
    header("Location: $dst");
    exit;
}
?>

<body>
  <div class="login-box">
    <img src="img/login-logo.png" alt="Logo" class="logo">
    <form method="post" action="http://hotspot.mikrotikmasters.com/login">
      <input type="text" name="username" placeholder="Username" value="<?= $prefill_username ?>" required>
      <input type="password" name="password" placeholder="Password" value="<?= $prefill_password ?>" required>
      <input type="submit" value="Login">
    </form>
    <form method="get" action="signup.php">
      <input type="submit" value="Sign Up">
    </form>
  </div>
</body>
</html>

๐Ÿ”น style.css

  • Centralised styling for login.html and signup.php
  • Ensures consistent appearance for:
    • Logo
    • Form width
    • Buttons
    • Box shadow, rounded borders, spacing
body {
  margin: 0;
  padding: 0;
  background: white;
  font-family: Arial, sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.login-box {
  width: 90%;
  max-width: 440px;
  height: auto;
  padding: 20px;
  border: 1px solid #555;
  border-radius: 10px;
  box-sizing: border-box;
  text-align: center;
}

.logo {
  width: 315px;
  margin-bottom: 20px;
}

form {
  margin: 10px 0;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="submit"] {
  width: 100%;
  padding: 12px;
  margin: 8px 0;
  box-sizing: border-box;
  font-size: 16px;
}

input[type="submit"] {
  background-color: #333;
  color: white;
  border: none;
  cursor: pointer;
  border-radius: 5px;
}

input[type="submit"]:hover {
  background-color: #555;
}

๐Ÿ”น signup.php

A custom user registration handler. Steps:

  1. Form fields:
    • Full Name
    • Email
    • Username
    • Password
    • Accept Terms (checkbox)
  2. Validation:
    • Ensures no fields are empty
    • Ensures terms are accepted
    • Checks if the username already exists in the radcheck table
  3. Database Insertion:
    • Inserts name and email into a custom customers table
    • Inserts username and password into radcheck table for RADIUS authentication
  4. Auto-login:
    • If signup succeeds, generates a hidden form POST to: http://hotspot.mikrotikmasters.com/login which logs the user in directly (no need to go to login page again)
<?php
// Database config
$dbHost = 'localhost';
$dbUser = 'radius';
$dbPass = 'helloworld123';
$dbName = 'radius';

// Connect to MySQL
$conn = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
if ($conn->connect_error) {
    die("DB connection failed: " . $conn->connect_error);
}

$error = '';
$dst = isset($_GET['dst']) ? $_GET['dst'] : 'http://example.com';

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $name     = trim($_POST['name']);
    $email    = trim($_POST['email']);
    $username = trim($_POST['username']);
    $password = trim($_POST['password']);
    $terms    = isset($_POST['terms']);

    // Validate
    if (!$name || !$email || !$username || !$password || !$terms) {
        $error = "Please fill in all fields and accept the terms.";
    } else {
        // Check if username exists
        $stmt = $conn->prepare("SELECT id FROM radcheck WHERE username = ?");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();

        if ($stmt->num_rows > 0) {
            $error = "Username already exists. Please choose another.";
        } else {
            // Save to 'customers'
            $stmt = $conn->prepare("INSERT INTO customers (name, email) VALUES (?, ?)");
            $stmt->bind_param("ss", $name, $email);
            $stmt->execute();

            // Save to 'radcheck'
            $stmt = $conn->prepare("INSERT INTO radcheck (username, attribute, op, value) VALUES (?, 'Cleartext-Password', ':=', ?)");
            $stmt->bind_param("ss", $username, $password);
            $stmt->execute();

            $stmt->close();
            $conn->close();

            // Auto login form
            echo "
            <form id='autoLogin' method='post' action='http://hotspot.mikrotikmasters.com/login'>
                <input type='hidden' name='username' value='" . htmlspecialchars($username) . "'>
                <input type='hidden' name='password' value='" . htmlspecialchars($password) . "'>
                <input type='hidden' name='dst' value='" . htmlspecialchars($dst) . "'>
            </form>
            <script>document.getElementById('autoLogin').submit();</script>";
            exit;
        }
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Signup</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="login-box">
        <img src="img/login-logo.png" alt="Logo" class="logo">
        <form method="post" action="signup.php">
            <?php if ($error): ?>
                <div class="error"><?= htmlspecialchars($error) ?></div>
            <?php endif; ?>
            <input type="text" name="name" placeholder="Full Name" required>
            <input type="email" name="email" placeholder="Email" required>
            <input type="text" name="username" placeholder="Username" required>
            <input type="password" name="password" placeholder="Password" required>
            <label style="display:block; font-size: 13px; margin: 5px 0;">
                <input type="checkbox" name="terms" required> I accept the <a href="#">terms & conditions</a>
            </label>
            <input type="submit" value="Sign Up">
            <a href="login.html"><button type="button">Back to Login</button></a>
        </form>
    </div>
</body>
</html>

โœ… MySQL Table Setup

Run this SQL on your radius database:

CREATE TABLE customers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

๐Ÿ”น PHP login submission (Auto POST)

Both signup.php and index.php eventually POST to:

http://hotspot.mikrotikmasters.com/login

MikroTik expects:

  • username
  • password
  • Optional dst (destination after login)

On successful RADIUS authentication, the user is redirected to dst.


๐Ÿ” 5. Flow Summary: From Connect to Internet

๐Ÿ”„ User Flow

  1. Connect to Hotspot SSID (managed by CAPsMAN)
  2. Gets IP in 10.0.30.0/24 (Hotspot pool)
  3. Browser auto-redirects to hotspot.mikrotikmasters.com/login
  4. User sees styled login.html:
    • Can log in OR click Sign up
  5. If signed up:
    • Credentials stored in MySQL
    • Auto-logged in to MikroTik via POST
  6. MikroTik sends credentials to RADIUS
  7. If accepted, user is granted internet access
  8. Redirected to their original URL (dst)

โœ… 6. Security & Recommendations

  • Always use HTTPS for the splash page (setup Let’s Encrypt on web server)
  • Sanitize and hash passwords if you use them beyond FreeRADIUS
  • Consider Google or Facebook OAuth (when ready) by integrating with signup/login flow
  • Protect your database and restrict access from specific MikroTik IPs

โœ… 7. Next Steps (Optional Enhancements)

  • Add session tracking (PHP/MySQL)
  • Add custom usage limits and expiry logic
  • Integrate social login (Google/Facebook)
  • Add logo and T&Cs links dynamically

Leave a Comment

Your email address will not be published. Required fields are marked *