2
0
Fork 0
mirror of https://github.com/discourse/wp-discourse.git synced 2025-10-04 09:01:05 +08:00

Added truly unique nonces

- renamed filters to reflect their REAL scope: it should read `client`, not `provider`
This commit is contained in:
Ionut Staicu 2017-02-07 09:17:53 +02:00
parent 684821e04b
commit 5225fade4f
6 changed files with 230 additions and 120 deletions

View file

@ -67,7 +67,7 @@ class DiscourseExternalSSO {
*/
private function update_user( $user_id ) {
$query = $this->get_sso_response();
$nonce = DiscourseUtilities::verify_nonce( $query['nonce'], '_discourse_sso' );
$nonce = \WPDiscourse\Nonce::get_instance()->verify( $query['nonce'], '_discourse_sso' );

if ( ! $nonce ) {
return new \WP_Error( 'expired_nonce' );
@ -82,7 +82,7 @@ class DiscourseExternalSSO {
'first_name' => $query['name'],
);

$updated_user = apply_filters( 'discourse/sso/provider/updated_user', $updated_user, $query );
$updated_user = apply_filters( 'discourse/sso/client/updated_user', $updated_user, $query );

wp_update_user( $updated_user );

@ -95,7 +95,7 @@ class DiscourseExternalSSO {
* @param WP_Error $error WP_Error object.
*/
private function handle_errors( $error ) {
$redirect_to = apply_filters( 'discourse/sso/provider/redirect_after_failed_login', wp_login_url() );
$redirect_to = apply_filters( 'discourse/sso/client/redirect_after_failed_login', wp_login_url() );

$redirect_to = add_query_arg( 'discourse_sso_error', $error->get_error_code(), $redirect_to );

@ -148,7 +148,8 @@ class DiscourseExternalSSO {
wp_set_auth_cookie( $user_id );
do_action( 'wp_login', $query['username'], $query['email'] );

$redirect_to = apply_filters( 'discourse/sso/provider/redirect_after_login', $query['return_sso_url'] );
$redirect_to = apply_filters( 'discourse/sso/client/redirect_after_login', $query['return_sso_url'] );

wp_safe_redirect( $redirect_to );
}


171
lib/nonce.php Normal file
View file

@ -0,0 +1,171 @@
<?php
/**
* Nonce generator & validator.
*
* @package WPDiscourse
*/

namespace WPDiscourse;

/**
* Nonce generator
*/
class Nonce {

/**
* Database Verson of nonce table
*
* @var string
*/
private $db_version = '1.0.1';

/**
* Nonce Class Instance
*
* @var mixed
*/
private static $instance = null;

/**
* Constructor
*
* @method __construct
*/
private function __construct() {
global $wpdb;
$this->wpdb = $wpdb;

/**
* One can override the default nonce life.
*
* The default is set to 10 minutes, which is plenty for most of the cases
*
* @var int
*/
$this->nonce_life = apply_filters( 'discourse/nonce_life', 600 );

$this->maybe_create_db();
}


/**
* Singleton instance
*
* @method get_instance
*
* @return \WPDiscourse\Nonce the instance.
*/
public static function get_instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new SELF;
}

return self::$instance;
}

/**
* Get the Database Name
*
* @method get_table_name
*
* @return string the db name.
*/
private function get_table_name() {
return "{$this->wpdb->prefix}discourse_nonce";
}

/**
* Db shouldn't be created/updated unless if it's an old version.
*
* @method maybe_create_db
*/
private function maybe_create_db() {
if ( version_compare( get_option( 'wpdiscourse_nonce_db_version', -1 ), $this->db_version ) !== 1 ) {
require_once ABSPATH . 'wp-admin/includes/upgrade.php';

$table_name = $this->get_table_name();
$charset = $this->wpdb->get_charset_collate();

dbDelta("CREATE TABLE {$table_name} (
id mediumint(9) NOT NULL AUTO_INCREMENT,
added_on datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
nonce varchar(255) DEFAULT '' NOT NULL,
action varchar(255) DEFAULT '' NOT NULL,
PRIMARY KEY (id)
) $charset;");

update_option( 'wpdiscourse_nonce_db_version', $this->db_version );
}

$this->purge_expired_nonces();
}

/**
* Will purge expired nonces.
*
* @method purge_expired_nonces
*/
private function purge_expired_nonces() {
$table_name = $this->get_table_name();

$expired_nonces = $this->wpdb->get_results( "SELECT id FROM {$table_name} WHERE added_on < DATE_SUB(NOW(), INTERVAL {$this->nonce_life} SECOND)" );

if ( count( $expired_nonces ) ) {
$expired_nonces = wp_list_pluck( $expired_nonces, 'id' );
$expired_nonces = implode( ',', $expired_nonces );
$this->wpdb->get_results( "DELETE FROM {$table_name} WHERE id IN ({$expired_nonces})" );
}
}

/**
* Creates a truly unique nonce based on the provided action
*
* @method create
*
* @param string|int $action Scalar value to add context to the nonce.
*
* @return string
*/
public function create( $action = -1 ) {
$nonce = wp_hash( uniqid( $action, true ), 'nonce' );

$this->wpdb->insert( $this->get_table_name(), array( 'nonce' => $nonce, 'action' => $action ), array( '%s', '%s' ) );

return $nonce;
}

/**
* Verify a nonce if it's valid and it will invalidate it
*
* @method verify
*
* @param string $nonce the nonce to be validated.
* @param string|int $action Scalar value to add context to the nonce.
*
* @return bool
*/
public function verify( $nonce, $action = -1 ) {
$table_name = $this->get_table_name();

$valid_nonce = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT id FROM {$table_name} WHERE nonce = %s AND action = %s", $nonce, $action ) );

if ( ! empty( $valid_nonce ) ) {
return ( bool ) $this->invalidate_nonce( $valid_nonce->id );
}

return false;
}

/**
* Delete the nonce from the DB once it is used
*
* @method invalidate_nonce
*
* @param int $id the nonce ID that needs to be invalidated.
*
* @return boolean
*/
private function invalidate_nonce( $id ) {
return $this->wpdb->delete( $this->get_table_name(), array( 'id' => $id ), array( '%d' ) );
}
}

View file

@ -9,16 +9,30 @@ use \WPDiscourse\Utilities\Utilities as DiscourseUtilities;

add_filter( 'query_vars', 'discourse_sso_custom_query_vars' );

function discourse_sso_custom_query_vars($vars)
{
/**
* Adds the `discourse_sso` value to the wp_query
*
* @method discourse_sso_custom_query_vars
*
* @param array $vars query vars.
*
* @return array
*/
function discourse_sso_custom_query_vars( $vars ) {
$vars[] = 'discourse_sso';
return $vars;
}

add_action( 'parse_query', 'discourse_sso_url_redirect' );

function discourse_sso_url_redirect($wp)
{
/**
* Redirect user to the SSO provider
*
* @method discourse_sso_url_redirect
*
* @param object $wp the wp_query.
*/
function discourse_sso_url_redirect( $wp ) {
if ( empty( $wp->query['discourse_sso'] ) ) {
return;
}
@ -27,13 +41,13 @@ function discourse_sso_url_redirect($wp)
$is_user_logged_in = is_user_logged_in();

if ( ! empty( $_GET['redirect_to'] ) ) {
$redirect_to = esc_url( $_GET['redirect_to'] );
$redirect_to = sanitize_text_field( wp_unslash( $_GET['redirect_to'] ) );
} else {
$redirect_to = home_url( '/' );
}

$payload = base64_encode(http_build_query(array(
'nonce' => DiscourseUtilities::create_nonce( '_discourse_sso' ),
'nonce' => \WPDiscourse\Nonce::get_instance()->create( '_discourse_sso' ),
'return_sso_url' => $redirect_to,
)
));
@ -48,14 +62,38 @@ function discourse_sso_url_redirect($wp)
wp_redirect( $sso_login_url );
exit;
}

/**
* Gets the auth URL for discourse
*
* @param array $options anchor, link.
*
* @return string.
*/
function get_discourse_sso_url( $options = array() ) {
function get_discourse_sso_url() {
$is_user_logged_in = is_user_logged_in();

$redirect_to = get_permalink();

if ( empty( $redirect_to ) ) {
$redirect_to = $is_user_logged_in ? admin_url( 'profile.php' ) : home_url( '/' );
}

return add_query_arg( array(
'discourse_sso' => 1,
'redirect_to' => $redirect_to,
), home_url( '/' ) );
}


/**
* Generates the markup for SSO link
*
* @method get_discourse_sso_link_markup
*
* @param array $options anchor, link.
*
* @return string
*/
function get_discourse_sso_link_markup( $options = array() ) {
$is_user_logged_in = is_user_logged_in();

if ( $is_user_logged_in ) {
@ -67,21 +105,11 @@ function get_discourse_sso_url( $options = array() ) {
$anchor = ! empty( $options['login'] ) ? $options['login'] : __( 'Log in with Discourse', 'wp-discourse' );
}


$redirect_to = get_permalink();

if ( empty( $redirect_to ) ) {
$redirect_to = $is_user_logged_in ? admin_url( 'profile.php' ) : home_url( '/' );
}

$sso_login_url = add_query_arg( array(
'discourse_sso' => 1,
'redirect_to' => $redirect_to,
), home_url( '/' ) );
$sso_login_url = get_discourse_sso_url();

$anchor = sprintf( '<a href="%s">%s</a>', $sso_login_url, $anchor );

return apply_filters( 'discourse/sso/provider/login_anchor', $anchor, $sso_login_url, $options );
return apply_filters( 'discourse/sso/client/login_anchor', $anchor, $sso_login_url, $options );
}

/**
@ -99,7 +127,7 @@ function discourse_sso_shortcode( $atts = array() ) {
'link' => null,
), $atts);

return get_discourse_sso_url( $options );
return get_discourse_sso_link_markup( $options );
}

add_shortcode( 'discourse_sso', 'discourse_sso_shortcode' );

View file

@ -25,7 +25,7 @@ function discourse_sso_alter_login_form() {
return;
}

printf( '<p>%s</p><p>&nbsp;</p>', wp_kses_data( get_discourse_sso_url() ) );
printf( '<p>%s</p><p>&nbsp;</p>', wp_kses_data( get_discourse_sso_link_markup() ) );
}

add_action( 'login_form', 'discourse_sso_alter_login_form' );
@ -36,7 +36,7 @@ add_action( 'login_form', 'discourse_sso_alter_login_form' );
*/
function discourse_sso_alter_user_profile() {
$auto_inject_button = discourse_sso_auto_inject_button();
if ( ! apply_filters( 'discourse/sso/provider/add_link_buttons_on_profile', $auto_inject_button ) ) {
if ( ! apply_filters( 'discourse/sso/client/add_link_buttons_on_profile', $auto_inject_button ) ) {
return;
}

@ -49,7 +49,7 @@ function discourse_sso_alter_user_profile() {
if ( DiscourseUtilities::user_is_linked_to_sso() ) {
esc_html_e( 'You\'re already linked to discourse!', 'wp-discourse' );
} else {
echo wp_kses_data( get_discourse_sso_url() );
echo wp_kses_data( get_discourse_sso_link_markup() );
}
?>
</td>

View file

@ -140,95 +140,4 @@ class Utilities {

return get_user_meta( $user->ID, 'discourse_sso_user_id', true );
}

/**
* Get the time-dependent variable for nonce creation.
*
* Overrides the default WP nonce_tick function to allow smaller lifespan.
*
* @method nonce_tick
*
* @return integer
*/
private static function nonce_tick() {
/**
* One can override the default nonce life.
*
* The default is set to 10 minutes, which is plenty for most of the cases
*
* @var int
*/
$nonce_life = apply_filters( 'discourse/nonce_life', 600 );

return ceil( time() / ( $nonce_life / 2 ) );
}

/**
* Provides a wrapper of the default WP nonce system, one that allows setting an expiring time
*
* @method create_nonce
*
* @param string|int $action Scalar value to add context to the nonce.
*
* @return string
*/
public static function create_nonce( $action = -1 ) {
$user = wp_get_current_user();
$uid = (int) $user->ID;
if ( ! $uid ) {
/** This filter is documented in wp-includes/pluggable.php */
$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
}

$token = wp_get_session_token();
$i = self::nonce_tick();

return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}

/**
* Verify that correct nonce was used with time limit.
*
* The user is given an amount of time to use the token, so therefore, since the
* UID and $action remain the same, the independent variable is the time.
*
* @param string $nonce Nonce that was used in the form to verify.
* @param string|int $action Should give context to what is taking place and be the same when nonce was created.
* @return false|int False if the nonce is invalid, 1 if the nonce is valid and generated in the
* first half of the nonce_tick, 2 if the nonce is valid and generated in the second half of the nonce_tick.
*/
public static function verify_nonce( $nonce, $action = -1 ) {
$nonce = (string) $nonce;
$user = wp_get_current_user();
$uid = (int) $user->ID;
if ( ! $uid ) {
/** This filter is documented in wp-includes/pluggable.php */
$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
}

if ( empty( $nonce ) ) {
return false;
}

$token = wp_get_session_token();
$i = self::nonce_tick();

// Nonce generated in the first half of the time returned by `nonce_tick` passed.
$expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 1;
}

// Nonce generated in the second half of the time returned by `nonce_tick` passed.
$expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
if ( hash_equals( $expected, $nonce ) ) {
return 2;
}

/** This filter is documented in wp-includes/pluggable.php */
do_action( 'wp_verify_nonce_failed', $nonce, $action, $user, $token );

// Invalid nonce.
return false;
}
}

View file

@ -37,6 +37,7 @@ define( 'MIN_PHP_VERSION', '5.4.0' );
define( 'WPDISCOURSE_VERSION', '1.1.1' );

require_once( __DIR__ . '/lib/utilities.php' );
require_once( __DIR__ . '/lib/nonce.php' );
require_once( __DIR__ . '/templates/html-templates.php' );
require_once( __DIR__ . '/templates/template-functions.php' );
require_once( __DIR__ . '/lib/discourse.php' );