Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Building a SAML Honeypot: How to Capture and Analyze Authentication Flows from Azure AD

Why Build a SAML Capture Server?

Note : This has been built not for malicious purposes, but learning purposes - this application does not modify or play around with your authentication token whatsoever.

This guide demonstrates how to create a SAML honeypot that captures authentication attempts in real-time, providing unprecedented visibility into who is actually clicking links and authenticating to your applications - this does not intercept or alter credentials - this is just live accurate activity data that requires the user to click on a link in order to initiate the user authentication flow flow

If you have email protection that tries to visit links on behalf of the user to assess if they’re malicious, then, in this scenario, you are still protected from false positives because the sandbox service will not have your credentials, so it will simply fail to write a valid sign in log.

Visuals of the Solution

This will give you some visuals of the solution, this is the default page that advises of configuration setup required:


This is the ACL URL when you visit it without a valid SAML request:


This is the result if you vist the ACS website with a valid SAML request, here we have captured the data from the SAML request to the log:


This is the log file when you use the "view log file" option, so see a live view of the logs:


If you test the SAML process it will look like this, notice the this-is-a-test in the relay state:


You will then see the "test" data in the log file as below:
[2025-08-11 06:56:03] === POST DATA ===
[2025-08-11 06:56:03] SAMLResponse received (length: 88 characters)
[2025-08-11 06:56:03] SAMLResponse decoded preview: <saml:Assertion><test>Demo SAML ResVnse</test></saml:Assertion>...
[2025-08-11 06:56:03] POST RelayState: this-is-a-test
[2025-08-11 06:56:03] === GET DATA ===
[2025-08-11 06:56:03] === END SAML ACS ACCESS ===
[2025-08-11 06:56:03] 
Then finally you can review the signin attempts using the sign-in logs on the Enterprise Application as below:


Problem : Unreliable Reporting

Traditional email and SMS campaigns lack reliable attribution - you might know someone opened an email or clicked a link, but you can't definitively prove who actually engaged with your content, in many of these tests, if you have the URL that link will be unique to the user - this means if you click on the link, or the actual user clicks on the link, the original user will get the reporting against them.

SAML : Accurate Data

By creating an Azure AD Enterprise Application connected to a custom capture server, you can:

  • Guarantee Identity Verification: When users click your link, they authenticate with their real Azure AD credentials
  • Capture Detailed Logs: Record exact usernames, email addresses, timestamps, and browser details
  • Monitor Curious Behavior: See who clicks suspicious links without understanding the consequences
  • Track Engagement: Get definitive proof of who interacted with your content

Real-World Applications

Security Awareness Testing: Send employees a link to a "new company portal" and see who blindly authenticates without verification

Phishing Simulation: Test if users will authenticate credentials on unfamiliar domains

Threat Intelligence: Monitor for credential stuffing or suspicious authentication patterns

How It Works

  1. Create Enterprise Application in Azure AD with your custom ACS endpoint
  2. Generate User Access URL from Azure AD (looks legitimate to users)
  3. Embed in Shortened URL using services like bit.ly for stealth
  4. Send via Email/SMS to target audience
  5. Capture Real Credentials when users authenticate
  6. Analyze Results through detailed server logs

What does the user authentication flow look like?

We have two options here, you can either have a conditional access policy that will apply and require MFA everywhere, or with the other option, you have “trusted” named locations so MFA is not required when you access this application from certain networks 

Strict MFA

If you have MFA enabled for all your services and applications and you have not “trusted” named locations - good move that is the most secure option, if this is the case, then this is how the User authentication flow will work with this application:


Relaxed MFA

If you have opted for the relaxed MFA security model where MFA is required only when coming from outside your trusted networks, that is good news for this particular scenario because it means you are not asked to authenticate your credentials with MFA.

Unfortunately, it does mean you’ve removed an extra layer of defense between you and something malicious, this makes it even more security problem if all your phones and devices then obtain an address that makes it look like they’re inside the network - say with a VPN solution or an iOS/Android app that forces traffic through your corporate proxy that’s in the same trusted network.

If you have chosen this option, then users will very rarely get the MFA prompt and this extra layer of defense will only apply when they’re outside your trusted locations like at home.

MFA Is no longer an optional security feature it’s now a requirement and overriding it because it’s a bit of an inconvenience is not a good place to be from a security point of view - people need to get used to doing MFA authentication as the more you do it, the more you get used to it.

MFA : Is that just not the extra inconvenience?

No, absolutely not, I have heard countless people tell me they’ve lost access to the email/service accounts because they haven’t turned MFA on, when you get to this point, I find it very hard to feel sorry for the person because essentially it’s their own fault.

I have had to then listen to a list of reasons why MFA was not enabled - I always think to myself if you just enabled MFA and didn’t find excuses not to enable it you wouldn’t be having this conversation  - if you have services that do not support MFA - do not store any personal or sensitive information on those services unless you’re willing to lose it or have it made publicly available!

MFA is also no longer that secure anymore, and there are many exploits to bypass that now, you really need to be considering passkeys or phishing resistant hardware tokens like FIDO2..

Anyway, I digress…

The user authentication flow without MFA because you’re in a trusted location looks like this:


Implementation time

You are not able to do this with a simple static webpage because that will only support the HTTP GET command - this is not sufficient to successfully authenticate with SAML you need HTTP POST, let’s get cracking.

This guide will walk you through setting up a secure Linux server with Nginx that captures SAML login details from Azure AD Enterprise Applications, remember you need to substitute SAML.a6n.co.uk for the actual address of your website

Step 1: Create Ubuntu VM in Azure

Warning : I will be using SSH password authentication as I will not be exposing SSH to the public Internet, if you were looking to expose SSH to the Internet, you should only be using passkeys and even then, it should be limited to trusted addresses on your NSG - allowing a virtual machine with password based SSH is not a good idea!!!

  1. Azure Portal → "Create a resource" → "Virtual Machine"
  2. Configure:
    • Image: Ubuntu Server 22.04 LTS
    • Size: Standard B2s (recommended for SSL)
    • Authentication: Password (I will connect with Twingate)
    • Username: azureuser
    • Password: Create a strong password
  3. Networking tab:
    • Public IP: Create new
    • Public inbound ports: HTTP (80), HTTPS (443)
  4. Create the VM

Step 2: Point Your Domain to the VM

  1. Get your VM's public IP from Azure Portal
  2. In your DNS provider:
    • Create A record: saml.a6n.co.ukYOUR-VM-PUBLIC-IP
    • Wait 5-10 minutes for DNS propagation

Step 3: Connect and Install Software

# SSH into your VM
ssh azureuser@YOUR-VM-IP

# Update system
sudo apt update && sudo apt upgrade -y

# Install Nginx, PHP, and SSL tools
sudo apt install nginx php-fpm certbot python3-certbot-nginx -y

# Start services
sudo systemctl start nginx
sudo systemctl start php8.1-fpm
sudo systemctl enable nginx
sudo systemctl enable php8.1-fpm

Step 4: Configure Nginx

# Remove default nginx config
sudo rm /etc/nginx/sites-enabled/default

# Create initial Nginx site configuration (HTTP only)
cat << 'EOF' | sudo tee /etc/nginx/sites-available/saml
server {
    listen 80;
    server_name saml.a6n.co.uk;
    root /var/www/saml;
    index index.php index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
    }
}
EOF

# Enable the site
sudo ln -s /etc/nginx/sites-available/saml /etc/nginx/sites-enabled/

# Test nginx configuration
sudo nginx -t

# Reload nginx
sudo systemctl reload nginx

Step 5: Create Web Directory and Files

# Create the web directory
sudo mkdir -p /var/www/saml

Create CSS File

cat << 'EOF' | sudo tee /var/www/saml/style.css
body { 
    font-family: Arial, sans-serif; 
    text-align: center; 
    padding: 50px; 
    background-color: #f5f5f5; 
}

.container { 
    background: white; 
    padding: 40px; 
    border-radius: 10px; 
    max-width: 600px; 
    margin: 0 auto; 
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.btn { 
    background-color: #0078d4; 
    color: white; 
    padding: 12px 24px; 
    text-decoration: none; 
    border-radius: 5px; 
    margin: 10px; 
    display: inline-block; 
}

.btn.secondary { 
    background-color: #6c757d; 
}

.btn:hover {
    opacity: 0.9;
}

.info { 
    background-color: #e7f3ff; 
    padding: 15px; 
    border-radius: 5px; 
    margin: 20px 0; 
    font-size: 14px; 
    text-align: left; 
}

.success { 
    color: #28a745; 
}

.timestamp { 
    color: #666; 
    font-size: 0.9em; 
    margin-top: 20px; 
}

.logs { 
    background-color: #000; 
    color: #00ff00; 
    padding: 20px; 
    border-radius: 5px; 
    font-family: monospace; 
    font-size: 12px; 
    height: 500px; 
    overflow-y: scroll; 
    white-space: pre-wrap; 
}

textarea {
    width: 100%;
    height: 150px;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ddd;
    border-radius: 5px;
    font-family: monospace;
}

input[type="submit"] {
    background: #28a745;
    color: white;
    padding: 12px 24px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
}

input[type="text"] {
    width: 100%;
    padding: 10px;
    margin: 10px 0;
    border: 1px solid #ddd;
    border-radius: 5px;
}

code {
    background-color: #f8f9fa;
    padding: 2px 4px;
    border-radius: 3px;
    font-family: monospace;
}
EOF

Create Main Page

cat << 'EOF' | sudo tee /var/www/saml/index.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SAML Login Demo</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>SAML Login Demo</h1>
        <p>This server captures login details when users authenticate via Azure AD.</p>
        
        <div class="info">
            <strong>Azure AD Configuration URLs:</strong><br><br>
            <strong>Identifier (Entity ID):</strong><br>
            <code>https://saml.a6n.co.uk/metadata.xml</code><br><br>
            <strong>Reply URL (ACS URL):</strong><br>
            <code>https://saml.a6n.co.uk/saml-acs.php</code><br><br>
            <strong>Sign-on URL:</strong><br>
            <em>Leave blank for IdP-initiated flow</em>
        </div>
        
        <a href="test-form.php" class="btn">Test SAML Form</a>
        <a href="view-logs.php" class="btn secondary">View Recent Logs</a>
    </div>
</body>
</html>
EOF

Create SAML ACS Handler (The Core Capture Script)

cat << 'EOF' | sudo tee /var/www/saml/saml-acs.php
<?php
$timestamp = date('Y-m-d H:i:s');
$logFile = '/var/log/nginx/saml_capture.log';

function logData($message) {
    global $logFile, $timestamp;
    file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND | LOCK_EX);
}

logData("=== SAML ACS ENDPOINT ACCESS ===");
logData("Method: " . $_SERVER['REQUEST_METHOD']);
logData("URI: " . $_SERVER['REQUEST_URI']);
logData("Query String: " . ($_SERVER['QUERY_STRING'] ?? 'None'));

// Log all headers
logData("=== HEADERS ===");
foreach (getallheaders() as $name => $value) {
    logData("Header $name: $value");
}

logData("=== POST DATA ===");
foreach ($_POST as $key => $value) {
    if ($key === 'SAMLResponse') {
        logData("SAMLResponse received (length: " . strlen($value) . " characters)");
        
        // Try to decode and extract user info
        try {
            $decoded = base64_decode($value);
            logData("SAMLResponse decoded preview: " . substr($decoded, 0, 500) . "...");
            
            // Extract email if present
            if (preg_match('/emailaddress[^>]*>([^<]+)</', $decoded, $matches)) {
                logData("Extracted Email: " . $matches[1]);
            }
            
            // Extract name if present
            if (preg_match('/<saml:NameID[^>]*>([^<]+)<\/saml:NameID>/', $decoded, $matches)) {
                logData("Extracted NameID: " . $matches[1]);
            }
            
        } catch (Exception $e) {
            logData("Error decoding SAMLResponse: " . $e->getMessage());
        }
    } else {
        logData("POST $key: $value");
    }
}

logData("=== GET DATA ===");
foreach ($_GET as $key => $value) {
    logData("GET $key: $value");
}

logData("=== END SAML ACS ACCESS ===");
logData(""); // Empty line for readability

$hasSamlData = !empty($_POST['SAMLResponse']) || !empty($_GET['SAMLResponse']);
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SAML Response Received</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <?php if ($hasSamlData): ?>
            <h1 class="success">✓ Your details have been logged, thanks!</h1>
            <p>You have successfully signed in and your login details have been captured.</p>
        <?php else: ?>
            <h1>ACS Endpoint Accessed</h1>
            <p>The SAML ACS endpoint was accessed but no SAML response was received.</p>
            <p>This might be a direct access or a configuration test.</p>
        <?php endif; ?>
        
        <div class="timestamp">Logged at: <?php echo $timestamp; ?></div>
        <br>
        <a href="view-logs.php" class="btn">View Captured Logs</a>
        <a href="index.php" class="btn secondary">← Back to Home</a>
    </div>
</body>
</html>
EOF

Create Test Form

cat << 'EOF' | sudo tee /var/www/saml/test-form.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test SAML Response</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Test SAML Response Handler</h1>
        <p>This form simulates what Azure AD sends to your server:</p>
        
        <form action="saml-acs.php" method="POST">
            <label for="SAMLResponse">SAML Response (Base64):</label>
            <textarea name="SAMLResponse" placeholder="Paste SAML response here (or leave empty for demo test)">PHNhbWw6QXNzZXJ0aW9uPjx0ZXN0PkRlbW8gU0FNTCBSZXNWB25zZTwvdGVzdD48L3NhbWw6QXNzZXJ0aW9uPg==</textarea>
            
            <label for="RelayState">Relay State (optional):</label>
            <input type="text" name="RelayState" placeholder="Optional relay state">
            
            <br><br>
            <a href="index.php" class="btn secondary">← Back</a>
            <input type="submit" value="Submit Test SAML Response">
        </form>
    </div>
</body>
</html>
EOF

Create Log Viewer

cat << 'EOF' | sudo tee /var/www/saml/view-logs.php
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SAML Logs</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Recent SAML Capture Logs</h1>
        <a href="index.php" class="btn secondary">← Back to Home</a>
        <a href="view-logs.php" class="btn">Refresh</a>
        <br><br>
        <div class="logs">
<?php
$logFile = '/var/log/nginx/saml_capture.log';
if (file_exists($logFile) && is_readable($logFile)) {
    $logs = file_get_contents($logFile);
    if (!empty($logs)) {
        echo htmlspecialchars($logs);
    } else {
        echo "Log file exists but is empty. No captures yet.";
    }
} else {
    echo "No logs found yet. Try submitting the test form first.\n\n";
    echo "Log file location: $logFile\n";
    echo "File exists: " . (file_exists($logFile) ? "Yes" : "No") . "\n";
    echo "File readable: " . (is_readable($logFile) ? "Yes" : "No") . "\n";
}
?>
        </div>
    </div>
</body>
</html>
EOF

Create SAML Metadata

cat << 'EOF' | sudo tee /var/www/saml/metadata.xml
<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://saml.a6n.co.uk/metadata.xml">
    <md:SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://saml.a6n.co.uk/saml-acs.php" index="1"/>
        <md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://saml.a6n.co.uk/saml-acs.php" index="2"/>
    </md:SPSSODescriptor>
</md:EntityDescriptor>
EOF

Step 6: Set Permissions and Create Log File

# Set correct ownership and permissions
sudo chown -R www-data:www-data /var/www/saml
sudo chmod 644 /var/www/saml/*.php
sudo chmod 644 /var/www/saml/*.xml
sudo chmod 644 /var/www/saml/*.css

# Create log file with correct permissions
sudo touch /var/log/nginx/saml_capture.log
sudo chown www-data:www-data /var/log/nginx/saml_capture.log
sudo chmod 644 /var/log/nginx/saml_capture.log

Step 7: Generate SSL Certificate

# Generate SSL certificate with Let's Encrypt
sudo certbot --nginx -d saml.a6n.co.uk

During the process:

  • Enter your email address
  • Agree to terms of service
  • Choose whether to share email (your choice)
  • Certbot will automatically configure HTTPS

Step 8: Test Your Server

  1. Visit: https://saml.a6n.co.uk
  2. Verify SSL certificate (should show lock icon)
  3. Test the form → click "Test SAML Form" and submit
  4. Check logs → click "View Recent Logs"

Step 9: Create Azure AD Enterprise Application

  1. Go to Azure Portal → "Azure Active Directory"
  2. Click "Enterprise applications" → "New application"
  3. Click "Create your own application"
  4. Name it (e.g., "SAML Login Capture")
  5. Select "Integrate any other application you don't find in the gallery"
  6. Click "Create"

Step 10: Configure SAML Single Sign-On

  1. In your new application, click "Single sign-on"

  2. Select "SAML"

  3. Click "Edit" in Basic SAML Configuration

  4. Enter these exact URLs:

    • Identifier (Entity ID): https://saml.a6n.co.uk/metadata.xml
    • Reply URL (ACS URL): https://saml.a6n.co.uk/saml-acs.php
    • Sign-on URL: Leave blank (this is crucial for IdP-initiated flow)
  5. Click "Save"

Test the Complete Flow

  1. Go to your Enterprise Application → Overview
  2. Click "Test this application"
  3. Sign in with your Azure AD credentials
  4. You should see: "Your details have been logged, thanks!"
  5. Check captured data: https://saml.a6n.co.uk/view-logs.php

View Recent Logs

# View last 50 lines
sudo tail -50 /var/log/nginx/saml_capture.log

# Watch logs in real-time
sudo tail -f /var/log/nginx/saml_capture.log

Clear Log File

# Clear logs to start fresh
sudo truncate -s 0 /var/log/nginx/saml_capture.log

Check Services

# Check nginx status
sudo systemctl status nginx

# Check PHP-FPM status
sudo systemctl status php8.1-fpm

What’s Captured?

When users authenticate through Azure AD, you'll capture:

  • User email address and name
  • Complete SAML response with all attributes
  • Timestamps of authentication
  • User's IP address and browser details
  • Any custom attributes configured in Azure AD

This setup creates a production-ready SAML login capture system that will securely log all authentication attempts from your Azure AD Enterprise Application.

Previous Post Next Post

نموذج الاتصال