Plugin Directory

Changeset 2701770


Ignore:
Timestamp:
03/30/2022 09:46:30 AM (4 years ago)
Author:
jimseconde
Message:

Released Version 1.0.0

Location:
vonage-2fa
Files:
18 added
2 edited

Legend:

Unmodified
Added
Removed
  • vonage-2fa/trunk/readme.txt

    r2701042 r2701770  
    33Requires at least: 5.7
    44Tested up to: 5.9.2
    5 Stable tag: 0.0.1
     5Stable tag: 1.0.0
    66Requires PHP: 8.0
    77License: Apache 2.0
     
    1010This plugin enables your WordPress site to use Two-Factor Authentication using Vonage's Verify API service.
    1111
     12== Screenshots ==
     13
     141. The Vonage WordPress 2FA Plugin Admin Menu
     152. This is where you can get your API keys from the Vonage Dashboard
     163. Vonage 2FA Settings in the User profile screen
     173. New PIN entry login form for 2FA
     18
    1219== Description ==
    1320
    1421# Vonage Two-Factor Authentication Plugin for WordPress
    15 
    16 ![Vonage and WordPress logos](./assets/vonagexwordpress.png)
    1722
    1823This plugin enables your WordPress site to use Two-Factor Authentication
     
    27321. Install this plugin in your WordPress plugins directory.
    28332. Active the plugin under your administrator account.
    29 3. Your 2FA admin menu should now be accessible from the WordPress menu:
    30 
    31 ![Screenshot of the Vonage WordPress 2FA Plugin admin menu](./assets/admin-menu.png)
     343. Your 2FA admin menu should now be accessible from the WordPress menu
    3235
    33364. Populate your API Key and Secret in order to connect to Vonage's [Verify API]() that
    34 this plugin uses. You can get these credentials from your [Vonage Dashboard](https://dashboard.nexmo.com/), located here:
    35 
    36 ![Screenshot of Vonage Dashboard](./assets/vonage-api-screenshot.png)
     37this plugin uses. You can get these credentials from your [Vonage Dashboard](https://dashboard.nexmo.com/).
    3738
    38395. Each user of your WordPress site can now enable 2 Factor Authentication from their
    39 user settings. A new section is available in the Profile screen, which will look like this:
    40 
    41 ![Screenshot of WordPress settings menu](./assets/vonage-user-settings.png)
     40user settings. A new section is available in the Profile screen.
    4241
    4342> Please note that this phone number must contain the *full* international dialling code
     
    4544
    46456. For users with 2FA enabled, the first login attempt will ask for the PIN sent out
    47 to the phone number entered:
    48 
    49 ![Screenshot of new PIN code entry](./assets/vonage-login-pin.png)
     46to the phone number entered.
    5047
    51487. Once the user has entered a valid PIN, login will finish.
     
    6158or by email you can reach me at jim.seconde at vonage.com. Alternatively, you
    6259can email the Developer Relations team at devrel at vonage.com.
     60
  • vonage-2fa/trunk/vonage2fa.php

    r2701042 r2701770  
    66  Description: Use Vonage's APIs for 2FA
    77  Author: James Seconde
    8   Version: 0.0.1
     8  Version: 1.0.0
    99  Author URI: https://developer.vonage.com/
    1010*/
    1111
     12const PLUGIN_VERSION ='1.0.0';
    1213const RESPONSE_PIN_OK = "0";
    1314const RESPONSE_PIN_INVALID = "16";
     
    1617const RESPONSE_REQUEST_INSUFFICIENT_FUNDS = '9';
    1718
     19global $wp_version;
     20define( "VONAGE_USER_AGENT_STRING", 'vonage-wordpress/' . $wp_version . '/' . PLUGIN_VERSION );
     21
    1822function vonage_2fa_setup_menu() {
    19     add_menu_page(
    20         'Test Plugin Page',
    21         'Vonage 2FA',
    22         'manage_options',
    23         'vonage_2fa_plugin',
    24         'vonage_2fa_load_admin_settings',
    25         'data:image/svg+xml;base64,' . base64_encode('<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
     23    add_menu_page(
     24        'Vonage 2FA Plugin Page',
     25        'Vonage 2FA',
     26        'manage_options',
     27        'vonage_2fa_plugin',
     28        'vonage_2fa_load_admin_settings',
     29        'data:image/svg+xml;base64,' . base64_encode('<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
    2630             width="300.000000pt" height="261.000000pt" viewBox="0 0 300.000000 261.000000"
    2731             preserveAspectRatio="xMidYMid meet">
     
    3741            </svg>
    3842            ')
    39     );
     43    );
    4044}
    4145
    4246function vonage_2fa_register_settings() {
    43     register_setting( 'vonage_api_settings_options', 'vonage_api_settings_options', 'vonage_api_settings_options_validate' );
    44     add_settings_section( 'api_credentials', 'Vonage API Credentials', 'vonage_2fa_plugin_text_helper', 'vonage_2fa_plugin' );
    45 
    46     add_settings_field( 'api_credentials_key', 'API Key', 'vonage_2fa_api_credentials_key', 'vonage_2fa_plugin', 'api_credentials' );
    47     add_settings_field( 'api_credentials_secret', 'API Secret', 'vonage_2fa_api_credentials_secret', 'vonage_2fa_plugin', 'api_credentials' );
     47    register_setting( 'vonage_api_settings_options', 'vonage_api_settings_options', 'vonage_api_settings_options_validate' );
     48    add_settings_section( 'api_credentials', 'Vonage API Credentials', 'vonage_2fa_plugin_text_helper', 'vonage_2fa_plugin' );
     49
     50    add_settings_field( 'api_credentials_key', 'API Key', 'vonage_2fa_api_credentials_key', 'vonage_2fa_plugin', 'api_credentials' );
     51    add_settings_field( 'api_credentials_secret', 'API Secret', 'vonage_2fa_api_credentials_secret', 'vonage_2fa_plugin', 'api_credentials' );
    4852}
    4953
    5054function vonage_2fa_plugin_text_helper() {
    51     echo '<p>You will need your master API Key/Secret credentials from your Vonage Dashboard.</p>';
     55    echo '<p>You will need your master API Key/Secret credentials from your Vonage Dashboard.</p>';
    5256}
    5357
    5458function vonage_2fa_api_credentials_key() {
    55     $options = get_option( 'vonage_api_settings_options' );
    56     echo "<input id='api_credentials_key' name='vonage_api_settings_options[api_credentials_key]' type='text' value='" . esc_attr( $options['api_credentials_key'] ) . "' />";
     59    $options = get_option( 'vonage_api_settings_options' );
     60    echo "<input id='api_credentials_key' name='vonage_api_settings_options[api_credentials_key]' type='text' value='" . esc_attr( $options['api_credentials_key'] ) . "' />";
    5761}
    5862
    5963function vonage_2fa_api_credentials_secret() {
    60     $options = get_option( 'vonage_api_settings_options' );
    61     echo "<input id='api_credentials_secret' name='vonage_api_settings_options[api_credentials_secret]' type='text' value='" . esc_attr( $options['api_credentials_secret'] ) . "' />";
     64    $options = get_option( 'vonage_api_settings_options' );
     65    echo "<input id='api_credentials_secret' name='vonage_api_settings_options[api_credentials_secret]' type='text' value='" . esc_attr( $options['api_credentials_secret'] ) . "' />";
    6266}
    6367
    6468function vonage_2fa_load_admin_settings() {
    65     echo "
     69    echo "
    6670    <img src='" . plugin_dir_url(__FILE__ ) . "assets/logo-large.png' alt='Vonage logo'>
    6771    <h1>Built in 2FA</h1>
     
    6973    <div>
    7074        <form action='options.php' method='post'>";
    71             settings_fields( 'vonage_api_settings_options');
    72             do_settings_sections('vonage_2fa_plugin');
    73             submit_button();
    74     echo "
     75    settings_fields( 'vonage_api_settings_options');
     76    do_settings_sections('vonage_2fa_plugin');
     77    submit_button();
     78    echo "
    7579        </form>
    7680    </div>
     
    7983
    8084function vonage_2fa_user_settings( $user ) {
    81     $mobileValue = get_the_author_meta( 'vonage_2fa_user_mobile_number_data', $user->ID );
    82     $enabled = get_the_author_meta( 'vonage_2fa_user_enabled_data', $user->ID );
    83     $checkedString = $enabled === '1' ? 'checked' : '';
    84 
    85     echo "
     85    $mobileValue = get_the_author_meta( 'vonage_2fa_user_mobile_number_data', $user->ID );
     86    $enabled = get_the_author_meta( 'vonage_2fa_user_enabled_data', $user->ID );
     87    $checkedString = $enabled === '1' ? 'checked' : '';
     88
     89    echo "
    8690        <br />
    8791        <h3>Vonage Two-Factor Authentication Settings</h3>
     
    108112function vonage_2fa_mobile_number_taken( $user_id, $mobile ) {
    109113
    110     if ( !$mobile ) {
    111         return false;
    112     }
    113 
    114     $users = get_users(
    115         [
    116             'meta_key' => 'vonage_2fa_user_mobile_number',
    117             'meta_value' => $mobile,
    118             'number' => 1
    119         ]
    120     );
    121 
    122     return 0 < count( $users ) && $user_id !== $users[0]->ID;
     114    if ( !$mobile ) {
     115        return false;
     116    }
     117
     118    $users = get_users(
     119        [
     120            'meta_key' => 'vonage_2fa_user_mobile_number',
     121            'meta_value' => $mobile,
     122            'number' => 1
     123        ]
     124    );
     125
     126    return 0 < count( $users ) && $user_id !== $users[0]->ID;
    123127}
    124128
    125129function vonage_2fa_valid_mobile( $mobile ) {
    126     $validMatch = preg_match('/^\+(?:[0-9]?){6,14}[0-9]$/', $mobile);
    127     $validTrim = trim($mobile) !== "";
    128 
    129     return $validMatch && $validTrim;
     130    $validMatch = preg_match('/^\+(?:[0-9]?){6,14}[0-9]$/', $mobile);
     131    $validTrim = trim($mobile) !== "";
     132
     133    return $validMatch && $validTrim;
    130134}
    131135
    132136function vonage_2fa_form_settings_validation( &$errors, $update, &$user ) {
    133     $mobile = filter_var( $_POST['vonage_2fa_user_mobile_number'], FILTER_SANITIZE_NUMBER_INT );
    134     $enabled = filter_var( isset( $_POST['vonage_2fa_user_enabled']), FILTER_SANITIZE_NUMBER_INT );
    135 
    136     if ($user && $enabled && !vonage_2fa_valid_mobile( $mobile )) {
    137         $errors->add( 'vonage_2fa_settings_update_error', 'Phone number provided is invalid, please make sure it includes international dialling code with plus sign.');
    138         update_user_meta($user->ID, 'vonage_2fa_user_mobile_number_data', "");
    139     }
    140 
    141     if ($user && $mobile && vonage_2fa_mobile_number_taken( $user->ID, $mobile )) {
    142         $errors->add( 'vonage_2fa_settings_update_error', 'Mobile number already in use.');
    143         update_user_meta($user->ID, 'vonage_2fa_user_mobile_number_data', "");
    144     }
     137    $mobile = filter_var( $_POST['vonage_2fa_user_mobile_number'], FILTER_SANITIZE_NUMBER_INT );
     138    $enabled = filter_var( isset( $_POST['vonage_2fa_user_enabled']), FILTER_SANITIZE_NUMBER_INT );
     139
     140    if ($user && $enabled && !vonage_2fa_valid_mobile( $mobile )) {
     141        $errors->add( 'vonage_2fa_settings_update_error', 'Phone number provided is invalid, please make sure it includes international dialling code with plus sign.');
     142        update_user_meta($user->ID, 'vonage_2fa_user_mobile_number_data', "");
     143    }
     144
     145    if ($user && $mobile && vonage_2fa_mobile_number_taken( $user->ID, $mobile )) {
     146        $errors->add( 'vonage_2fa_settings_update_error', 'Mobile number already in use.');
     147        update_user_meta($user->ID, 'vonage_2fa_user_mobile_number_data', "");
     148    }
    145149}
    146150
    147151function vonage_2fa_save_settings( $user_id ) {
    148152
    149     $mobile = filter_var( $_POST['vonage_2fa_user_mobile_number'], FILTER_SANITIZE_NUMBER_INT );
    150     $enabled = filter_var( isset( $_POST['vonage_2fa_user_enabled']), FILTER_SANITIZE_NUMBER_INT );
    151 
    152     if (!current_user_can( 'edit_user', $user_id ) ) {
    153         return false;
    154     }
    155 
    156     update_user_meta( $user_id, 'vonage_2fa_user_mobile_number_data', $mobile );
    157     update_user_meta( $user_id, 'vonage_2fa_user_enabled_data', $enabled );
     153    $mobile = filter_var( $_POST['vonage_2fa_user_mobile_number'], FILTER_SANITIZE_NUMBER_INT );
     154    $enabled = filter_var( isset( $_POST['vonage_2fa_user_enabled']), FILTER_SANITIZE_NUMBER_INT );
     155
     156    if (!current_user_can( 'edit_user', $user_id ) ) {
     157        return false;
     158    }
     159
     160    update_user_meta( $user_id, 'vonage_2fa_user_mobile_number_data', $mobile );
     161    update_user_meta( $user_id, 'vonage_2fa_user_enabled_data', $enabled );
    158162}
    159163
    160164function vonage_2fa_auth_intercept( $user, $username, $password ) {
    161     if ( !session_id() ) {
    162         session_start();
    163     }
    164 
    165     $wpUser = get_user_by( 'login', $username );
    166     $enabled_2fa = get_user_meta( $wpUser->ID, 'vonage_2fa_user_enabled_data', true );
    167 
    168     if (!$enabled_2fa) {
    169         return;
    170     }
    171 
    172     $options = get_option( 'vonage_api_settings_options' );
    173     $apiKey = $options['api_credentials_key'];
    174     $apiSecret = $options['api_credentials_secret'];
    175 
    176     $errors = [];
    177     $redirect_to = sanitize_url( $_POST['redirect_to'] ) ?? admin_url();
    178     $remember_me = isset( $_POST['rememberme'] ) && $_POST['rememberme'] === 'forever';
    179 
    180     $savedRequestId = sanitize_text_field($_SESSION['vonage_2fa_request_id']);
    181     $pin = isset( $_POST['vonage_2fa_pin'] ) ? sanitize_text_field( $_POST['vonage_2fa_pin'] ) : false;
    182     $requestId = isset( $_POST['vonage_2fa_request_id'] ) ? sanitize_text_field( $_POST['vonage_2fa_request_id'] ) : false;
    183 
    184     // You have submitted a PIN
    185     if ( $requestId && $pin && $savedRequestId === $requestId ) {
    186 
    187         $url = "https://api.nexmo.com/verify/check/json?&api_key=$apiKey&api_secret=$apiSecret&request_id=$requestId&code=$pin";
    188         $response = wp_remote_get( $url );
    189         $responseBody = json_decode( $response['body'], true );
    190 
    191         if ( $responseBody['status'] === RESPONSE_PIN_OK ) {
    192             wp_set_auth_cookie($wpUser->ID, $remember_me);
    193             wp_safe_redirect($redirect_to);
    194             exit;
    195         }
    196 
    197         if ( $responseBody['status'] === RESPONSE_PIN_INVALID ) {
    198             $errors[] = "Invalid PIN code";
    199         }
    200     }
    201 
    202     // Or you have a request ID saved that needs to be checked
    203     if ( $savedRequestId ) {
    204         $url = "https://api.nexmo.com/verify/search/json?&api_key=$apiKey&api_secret=$apiSecret&request_id=$savedRequestId";
    205         $response = wp_remote_get( $url );
    206         $responseBody = json_decode( $response['body'], true );
    207 
    208         if ( $responseBody['status'] === RESPONSE_VERIFICATION_PASSED ) {
    209             wp_set_auth_cookie( $wpUser->ID, $remember_me );
    210             wp_safe_redirect( $redirect_to );
    211             exit;
    212         }
    213 
    214         // The saved request ID has expired or failed
    215         $errors[] = 'Your verification has expired or there was an error logging in.';
    216         $_SESSION['vonage_2fa_request_id'] = '';
    217     }
    218 
    219     // You are trying to log in for the first time or have requested a PIN or have an invalid exiting verify
    220     if ($wpUser) {
    221         vonage_2fa_verify_user($wpUser, $redirect_to, $remember_me, $errors);
    222     }
    223 
    224     return $user;
     165    if ( !session_id() ) {
     166        session_start();
     167    }
     168
     169    $wpUser = get_user_by( 'login', $username );
     170    $enabled_2fa = get_user_meta( $wpUser->ID, 'vonage_2fa_user_enabled_data', true );
     171
     172    if (!$enabled_2fa) {
     173        return;
     174    }
     175
     176    $options = get_option( 'vonage_api_settings_options' );
     177    $apiKey = $options['api_credentials_key'];
     178    $apiSecret = $options['api_credentials_secret'];
     179
     180    $errors = [];
     181    $redirect_to = sanitize_url( $_POST['redirect_to'] ) ?? admin_url();
     182    $remember_me = isset( $_POST['rememberme'] ) && $_POST['rememberme'] === 'forever';
     183
     184    $savedRequestId = sanitize_text_field($_SESSION['vonage_2fa_request_id']);
     185    $pin = isset( $_POST['vonage_2fa_pin'] ) ? sanitize_text_field( $_POST['vonage_2fa_pin'] ) : false;
     186    $requestId = isset( $_POST['vonage_2fa_request_id'] ) ? sanitize_text_field( $_POST['vonage_2fa_request_id'] ) : false;
     187
     188    // You have submitted a PIN
     189    if ( $requestId && $pin && $savedRequestId === $requestId ) {
     190
     191        $url = "https://api.nexmo.com/verify/check/json?&api_key=$apiKey&api_secret=$apiSecret&request_id=$requestId&code=$pin";
     192        $response = wp_remote_get( $url, [
     193            'user-agent' => VONAGE_USER_AGENT_STRING
     194        ] );
     195        $responseBody = json_decode( $response['body'], true );
     196
     197        if ( $responseBody['status'] === RESPONSE_PIN_OK ) {
     198            wp_set_auth_cookie($wpUser->ID, $remember_me);
     199            wp_safe_redirect($redirect_to);
     200            exit;
     201        }
     202
     203        if ( $responseBody['status'] === RESPONSE_PIN_INVALID ) {
     204            $errors[] = "Invalid PIN code";
     205        }
     206    }
     207
     208    // Or you have a request ID saved that needs to be checked
     209    if ( $savedRequestId ) {
     210        $url = "https://api.nexmo.com/verify/search/json?&api_key=$apiKey&api_secret=$apiSecret&request_id=$savedRequestId";
     211        $response = wp_remote_get( $url, [
     212            'user-agent' => VONAGE_USER_AGENT_STRING
     213        ] );
     214        $responseBody = json_decode( $response['body'], true );
     215
     216        if ( $responseBody['status'] === RESPONSE_VERIFICATION_PASSED ) {
     217            wp_set_auth_cookie( $wpUser->ID, $remember_me );
     218            wp_safe_redirect( $redirect_to );
     219            exit;
     220        }
     221
     222        // The saved request ID has expired or failed
     223        $errors[] = 'Your verification has expired or there was an error logging in.';
     224        $_SESSION['vonage_2fa_request_id'] = '';
     225    }
     226
     227    // You are trying to log in for the first time or have requested a PIN or have an invalid exiting verify
     228    if ($wpUser) {
     229        vonage_2fa_verify_user($wpUser, $redirect_to, $remember_me, $errors);
     230    }
     231
     232    return $user;
    225233}
    226234
    227235function vonage_2fa_verify_user( $user, $redirect_to, $remember_me, $errors = [] ) {
    228     $options = get_option( 'vonage_api_settings_options' );
    229     $apiKey = $options['api_credentials_key'];
    230     $apiSecret = $options['api_credentials_secret'];
    231     $phoneNumber = get_user_meta( $user->ID, 'vonage_2fa_user_mobile_number_data', true );
    232 
    233     // You are requesting a PIN with a phone number
    234     $url = "https://api.nexmo.com/verify/json?&api_key=$apiKey&api_secret=$apiSecret&number=$phoneNumber&workflow_id=6&brand=Wordpress2FA";
    235     $response = wp_remote_post( $url );
    236     $responseBody = json_decode( $response['body'], true );
    237 
    238     // Attempt to send number has been rejected
    239     if ( $responseBody['status'] === RESPONSE_REQUEST_FAILED ) {
    240         $errors[] = $responseBody['error_text'];
    241     }
    242 
    243     if ( $responseBody['status'] === RESPONSE_REQUEST_INSUFFICIENT_FUNDS ) {
    244         $errors[] = 'Your Vonage account does not have enough balance to perform this authorisation request. Please contact your website administrator';
    245     }
    246 
    247     $requestId = $responseBody['request_id'];
    248     $_SESSION['vonage_2fa_request_id'] = $requestId;
    249 
    250     wp_logout();
    251     nocache_headers();
    252     header('Content-Type: ' . get_bloginfo( 'html_type' ) . '; charset=' . get_bloginfo( 'charset' ) );
    253     login_header('Vonage Two-Factor Authentication', '<p class="message">' . sprintf( 'Enter the PIN code sent to your 2FA phone number ending in <strong>%1$s</strong>' , substr($phoneNumber, -5) ) . '</p>');
    254 
    255     if ( !empty($errors) ) { ?>
     236    $options = get_option( 'vonage_api_settings_options' );
     237    $apiKey = $options['api_credentials_key'];
     238    $apiSecret = $options['api_credentials_secret'];
     239    $phoneNumber = get_user_meta( $user->ID, 'vonage_2fa_user_mobile_number_data', true );
     240
     241    // You are requesting a PIN with a phone number
     242    $url = "https://api.nexmo.com/verify/json?&api_key=$apiKey&api_secret=$apiSecret&number=$phoneNumber&workflow_id=6&brand=Wordpress2FA";
     243    $response = wp_remote_post( $url, [
     244        'user-agent' => VONAGE_USER_AGENT_STRING
     245    ] );
     246    $responseBody = json_decode( $response['body'], true );
     247
     248    // Attempt to send number has been rejected
     249    if ( $responseBody['status'] === RESPONSE_REQUEST_FAILED ) {
     250        $errors[] = $responseBody['error_text'];
     251    }
     252
     253    if ( $responseBody['status'] === RESPONSE_REQUEST_INSUFFICIENT_FUNDS ) {
     254        $errors[] = 'Your Vonage account does not have enough balance to perform this authorisation request. Please contact your website administrator';
     255    }
     256
     257    $requestId = $responseBody['request_id'];
     258    $_SESSION['vonage_2fa_request_id'] = $requestId;
     259
     260    wp_logout();
     261    nocache_headers();
     262    header('Content-Type: ' . get_bloginfo( 'html_type' ) . '; charset=' . get_bloginfo( 'charset' ) );
     263    login_header('Vonage Two-Factor Authentication', '<p class="message">' . sprintf( 'Enter the PIN code sent to your 2FA phone number ending in <strong>%1$s</strong>' , substr($phoneNumber, -5) ) . '</p>');
     264
     265    if ( !empty($errors) ) { ?>
    256266        <div id="login_error"><?php echo esc_html( implode( '<br />', $errors ) ) ?></div>
    257     <?php  } ?>
     267    <?php  } ?>
    258268
    259269    <form name="loginform" id="loginform" action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ) ?>" method="post" autocomplete="off">
     
    270280            <input type="hidden" name="redirect_to" value="<?php echo esc_attr( $redirect_to ) ?>" />
    271281
    272             <?php if ( $remember_me ) : ?>
     282            <?php if ( $remember_me ) : ?>
    273283                <input type="hidden" name="rememberme" value="forever" />
    274             <?php endif; ?>
     284            <?php endif; ?>
    275285        </p>
    276286    </form>
    277287
    278     <?php
    279 
    280     login_footer( 'vonage_2fa_pin' );
    281 
    282     exit;
     288    <?php
     289
     290    login_footer( 'vonage_2fa_pin' );
     291
     292    exit;
    283293}
    284294
Note: See TracChangeset for help on using the changeset viewer.