Logging WordPress authentication attempts to a file

Have you ever needed to log authentication attempts (successful and failed) somewhere for your WordPress website? Perhaps you have considered installing a plugin which logs this information to the database, but didn’t like this approach because one of the following:

  • You don’t want to bloat your database with log data
  • You need to access your authentication logs from another service (e.g. Fail2Ban, Rsyslog, etc.)
  • You just want authentication logging and nothing else

Well, by rolling a Must-Use (MU) Plugin you can add lightweight and flexibile authentication logging to your WordPress website. A ready-to-use example of this would be the `tripoint-security` plugin that I wrote:

https://github.com/DeltaSystems/tripointhosting-security/blob/master/tripointhosting-security.php

You can just drop this plugin into your wp-content/mu-plugins/ directory and adjust the logging directory by setting the `TRIPOINT_LOG_PATH` constant. The default log path is ABSPATH . ‘../logs/authentication_log’.

define('TRIPOINT_LOG_PATH', '/your/path/authentication_log');

However, let’s break down this plugin in case you would like to roll your own:

WordPress Hooks

There are two applicable WordPress hooks that we will listen for:

// https://codex.wordpress.org/Plugin_API/Action_Reference/wp_login
add_action('wp_login', 'login_succeeded', 10, 2);

// https://codex.wordpress.org/Plugin_API/Action_Reference/wp_login_failed
add_action('wp_login_failed', 'login_failed', 10, 2);

We can stub out our functions for these hooks:

function login_succeeded( string $username, WP_User $user ) {
  // @todo: implement
}

function login_failed( string $username , WP_Error $error ) {
  // @todo: implement
}
If that's all you wanted to know, you can flesh out those functions to your need. If you would like to log this data to a local file, I will cover this in the next section.

Logging attempts to a log file

We only need to decide a couple of questions for our log file. Where will we locate it and what will the format be?

For the location, I suggest locating this file outside the public path of your WordPress installation. The most likely location would be an existing directory for your log files. On my servers this is at /var/www/vhosts/mywebsite.com/logs which can be defined as:

if (defined('AUTHENTICATION_LOG_PATH') == false){
  define('AUTHENTICATION_LOG_PATH', ABSPATH . '../logs/authentication_log');
}

For the format, I chose this format. Each entry will be a newline.

“wordpress” “{$ip_address}” “success|fail” “{$username }” “{$log_message}”

If we go back to our stubbed functions, we can finish building them.
function login_succeeded( string $username, WP_User $user ) {

    // get ip address
    $ip_address = $_SERVER['REMOTE_ADDR'];

    // sanitize username
    $username = sanitize_user($username);

    // message
    $log_message = addslashes("WordPress successful login for {$username} from {$ip_address}");

    // line
    // format: application ip_address status username message
    $log_text = "\"wordpress\" \"{$ip_address}\" \"success\" \"{$username }\" \"{$log_message}\"" . PHP_EOL;

    file_put_contents(TRIPOINT_LOG_PATH, $log_text, FILE_APPEND);

}

function login_failed( string $username , WP_Error $error ) {

    // get ip address
    $ip_address = $_SERVER['REMOTE_ADDR'];

    // sanitize username
    $username = sanitize_user($username);

    // message
    $log_message = addslashes("WordPress failed login for {$username} from {$ip_address}");

    // line
    // format: application ip_address status username message
    $log_text = "\"wordpress\" \"{$ip_address}\" \"fail\" \"{$username }\" \"{$log_message}\"" . PHP_EOL;

    file_put_contents(TRIPOINT_LOG_PATH, $log_text, FILE_APPEND);

}

For the most part, you are done. Just drop your file into the `wp-contents/my-plugins` directory and test the log path by making a few login attempts.

Which IP Address to use?

The simplest way to get a user’s IP address with PHP is to use the $_SERVER[‘REMOTE_ADDR’] global variable. This is what’s used in the functions we defined. However, depending on your server configuration, this isn’t always the correct way to get the your user’s IP address. If you are using a loadbalancer or reverse proxy (e.g. CloudFlare) then REMOTE_ADDR may just contain the IP address of your loadbalancer or reverse proxy.

If this is your case then most likely the user’s IP address was forwarded in either the HTTP_X_FORWARDED_FOR or HTTP_X_REAL_IP header. You will need to modify your functions for your scenario. You can also use this function below and replace using REMOTE_ADDR with this function.

function get_ip_address() {

    // Grab the real IP address
    if ( isset($_SERVER['HTTP_X_REAL_IP'])
            && empty($_SERVER['HTTP_X_REAL_IP']) === false ) {
        $ip_address = $_SERVER['HTTP_X_REAL_IP'];
    } elseif( isset($_SERVER['HTTP_X_FORWARDED_FOR'])
                     && empty($_SERVER['HTTP_X_FORWARDED_FOR']) === false ) {
        $ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
    }else {
        $ip_address = $_SERVER['REMOTE_ADDR'];
    }

    // If multiple IP addresses, extract the first one
    $ip_addresses = explode(',', $ip_address);

    if( is_array($ip_addresses) ){
        $ip_address = $ip_addresses[0];
    }

    return $ip_address;
}

Leave a Reply

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

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>