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:
parent
684821e04b
commit
5225fade4f
6 changed files with 230 additions and 120 deletions
|
@ -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
171
lib/nonce.php
Normal 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' ) );
|
||||
}
|
||||
}
|
|
@ -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' );
|
||||
|
|
|
@ -25,7 +25,7 @@ function discourse_sso_alter_login_form() {
|
|||
return;
|
||||
}
|
||||
|
||||
printf( '<p>%s</p><p> </p>', wp_kses_data( get_discourse_sso_url() ) );
|
||||
printf( '<p>%s</p><p> </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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' );
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue