diff --git a/WP_Auth0.php b/WP_Auth0.php index ed672ae3..8900447e 100644 --- a/WP_Auth0.php +++ b/WP_Auth0.php @@ -23,7 +23,6 @@ define( 'WPA0_AUTH0_LOGIN_FORM_ID', 'auth0-login-form' ); define( 'WPA0_CACHE_GROUP', 'wp_auth0' ); -define( 'WPA0_STATE_COOKIE_NAME', 'auth0_state' ); define( 'WPA0_JWKS_CACHE_TRANSIENT_NAME', 'WP_Auth0_JWKS_cache' ); define( 'WPA0_LANG', 'wp-auth0' ); // deprecated; do not use for translations @@ -127,10 +126,6 @@ public function init() { $this->check_signup_status(); - if ( $this->a0_options->get( 'auto_login' ) ) { - WP_Auth0_Nonce_Handler::getInstance()->setCookie(); - } - WP_Auth0_Email_Verification::init(); } diff --git a/assets/js/lock-init.js b/assets/js/lock-init.js index d69ab921..4e90dfa4 100644 --- a/assets/js/lock-init.js +++ b/assets/js/lock-init.js @@ -21,6 +21,10 @@ jQuery(document).ready(function ($) { // Set state cookie to verify during callback Cookies.set( opts.stateCookieName, opts.settings.auth.params.state ); + if ( opts.settings.auth.params.nonce ) { + Cookies.set( opts.nonceCookieName, opts.settings.auth.params.nonce ); + } + // Set Lock to standard or Passwordless var Lock = opts.usePasswordless ? new Auth0LockPasswordless( opts.clientId, opts.domain, opts.settings ) diff --git a/lib/WP_Auth0_Lock10_Options.php b/lib/WP_Auth0_Lock10_Options.php index 18305113..d690fd14 100644 --- a/lib/WP_Auth0_Lock10_Options.php +++ b/lib/WP_Auth0_Lock10_Options.php @@ -55,7 +55,7 @@ public function get_state_obj( $redirect_to = null ) { $stateObj = array( 'interim' => ( isset( $_GET['interim-login'] ) && $_GET['interim-login'] == 1 ), - 'nonce' => WP_Auth0_Nonce_Handler::getInstance()->get() + 'nonce' => WP_Auth0_State_Handler::get_instance()->get_uniqid() ); if ( !empty( $redirect_to ) ) { @@ -154,7 +154,7 @@ public function get_sso_options() { unset( $options["authParams"] ); $options["state"] = $this->get_state_obj( $redirect_to ); - $options["nonce"] = WP_Auth0_Nonce_Handler::getInstance()->get(); + $options["nonce"] = WP_Auth0_Nonce_Handler::get_instance()->get_uniqid(); return $options; } @@ -182,7 +182,7 @@ public function get_lock_options() { $extraOptions["auth"]["responseType"] = 'id_token'; $extraOptions["auth"]["redirectUrl"] = $this->get_implicit_callback_url(); $extraOptions["autoParseHash"] = false; - $extraOptions["auth"]["params"]["nonce"] = WP_Auth0_Nonce_Handler::getInstance()->get(); + $extraOptions["auth"]["params"]["nonce"] = WP_Auth0_Nonce_Handler::get_instance()->get_uniqid(); } else { $extraOptions["auth"]["responseType"] = 'code'; $extraOptions["auth"]["redirectUrl"] = $this->get_code_callback_url(); diff --git a/lib/WP_Auth0_Lock_Options.php b/lib/WP_Auth0_Lock_Options.php index 441c93fa..406d8f36 100644 --- a/lib/WP_Auth0_Lock_Options.php +++ b/lib/WP_Auth0_Lock_Options.php @@ -110,7 +110,7 @@ public function modal_button_name() { public function get_state_obj( $redirect_to = null ) { $stateObj = array( 'interim' => ( isset( $_GET['interim-login'] ) && $_GET['interim-login'] == 1 ), - 'nonce' => WP_Auth0_Nonce_Handler::getInstance()->get() + 'nonce' => WP_Auth0_State_Handler::get_instance()->get_uniqid() ); if ( !empty( $redirect_to ) ) { $stateObj["redirect_to"] = addslashes( $redirect_to ); diff --git a/lib/WP_Auth0_LoginManager.php b/lib/WP_Auth0_LoginManager.php index 4f9906b6..9a59c692 100755 --- a/lib/WP_Auth0_LoginManager.php +++ b/lib/WP_Auth0_LoginManager.php @@ -42,22 +42,6 @@ class WP_Auth0_LoginManager { */ protected $users_repo; - /** - * State value returned from successful Auth0 login. - * - * @var string - * - * @see WP_Auth0_Lock10_Options::get_state_obj() - */ - protected $state; - - /** - * Decoded version of $this>state. - * - * @var object - */ - protected $state_decoded; - /** * WP_Auth0_LoginManager constructor. * @@ -103,31 +87,36 @@ public function init() { */ public function login_auto() { if ( - // Nothing to do - ( ! $this->a0_options->get( 'auto_login', FALSE ) ) - // Auth0 is not ready to process logins + // Nothing to do. + ( ! $this->a0_options->get( 'auto_login', false ) ) + // Auth0 is not ready to process logins. || ! WP_Auth0::ready() - // Do not redirect POST requests + // Do not redirect POST requests. || strtolower( $_SERVER['REQUEST_METHOD'] ) !== 'get' - // Do not redirect login page override + // Do not redirect login page override. || isset( $_GET['wle'] ) - // Do not redirect log out action + // Do not redirect log out action. || ( isset( $_GET['action'] ) && 'logout' === $_GET['action'] ) - // Do not redirect Auth0 login processing + // Do not redirect Auth0 login processing. || null !== $this->query_vars( 'auth0' ) - // Do not redirect if already authenticated + // Do not redirect if already authenticated. || is_user_logged_in() ) { return; } - $connection = apply_filters( 'auth0_get_auto_login_connection', $this->a0_options->get( 'auto_login_method' ) ); + $connection = apply_filters( 'auth0_get_auto_login_connection', $this->a0_options->get( 'auto_login_method' ) ); $auth_params = self::get_authorize_params( $connection ); $auth_url = 'https://' . $this->a0_options->get( 'domain' ) . '/authorize'; $auth_url = add_query_arg( array_map( 'rawurlencode', $auth_params ), $auth_url ); - setcookie( WPA0_STATE_COOKIE_NAME, $auth_params['state'], time() + WP_Auth0_Nonce_Handler::COOKIE_EXPIRES, '/' ); + WP_Auth0_State_Handler::get_instance()->set_state_cookie( $auth_params['state'] ); + + if ( isset( $auth_params['nonce'] ) ) { + WP_Auth0_Nonce_Handler::get_instance()->set_cookie(); + } + wp_redirect( $auth_url ); exit; } @@ -146,15 +135,16 @@ public function init_auth0() { // Catch any incoming errors and stop the login process. // See https://auth0.com/docs/libraries/error-messages for more info. - if ( $this->query_vars( 'error' ) || $this->query_vars( 'error_description' ) ) { - $error_msg = sanitize_text_field( rawurldecode( $_REQUEST[ 'error_description' ] ) ); - $error_code = sanitize_text_field( rawurldecode( $_REQUEST[ 'error' ] ) ); + if ( ! empty( $_REQUEST['error'] ) || ! empty( $_REQUEST['error_description'] ) ) { + $error_msg = sanitize_text_field( rawurldecode( $_REQUEST['error_description'] ) ); + $error_code = sanitize_text_field( rawurldecode( $_REQUEST['error'] ) ); $this->die_on_login( $error_msg, $error_code ); } // Check for valid state nonce, set in WP_Auth0_Lock10_Options::get_state_obj(). // See https://auth0.com/docs/protocols/oauth2/oauth-state for more info. - if ( ! $this->validate_state() ) { + $state_returned = isset( $_REQUEST['state'] ) ? rawurldecode( $_REQUEST['state'] ) : null; + if ( ! $state_returned || ! WP_Auth0_State_Handler::get_instance()->validate( $state_returned ) ) { $this->die_on_login( __( 'Invalid state', 'wp-auth0' ) ); } @@ -228,8 +218,7 @@ public function redirect_login() { // Look for clues as to what went wrong. $e_message = ! empty( $data->error_description ) ? $data->error_description : __( 'Unknown error', 'wp-auth0' ); - $e_code = ! empty( $data->error ) ? $data->error : $exchange_resp_code; - throw new WP_Auth0_LoginFlowValidationException( $e_message, $e_code ); + throw new WP_Auth0_LoginFlowValidationException( $e_message, $exchange_resp_code ); } $access_token = $data->access_token; @@ -272,7 +261,7 @@ public function redirect_login() { $userinfo = json_decode( $userinfo_resp_body ); if ( $this->login_user( $userinfo, $id_token, $access_token, $refresh_token ) ) { - $state_decoded = $this->get_state( true ); + $state_decoded = $this->get_state(); if ( ! empty( $state_decoded->interim ) ) { include WPA0_PLUGIN_DIR . 'templates/login-interim.php'; } else { @@ -326,7 +315,7 @@ public function implicit_login() { // Validate the nonce if one was included in the request if using auto-login. $nonce = isset( $decoded_token->nonce ) ? $decoded_token->nonce : null; - if ( ! WP_Auth0_Nonce_Handler::getInstance()->validate( $nonce ) ) { + if ( ! WP_Auth0_Nonce_Handler::get_instance()->validate( $nonce ) ) { throw new WP_Auth0_LoginFlowValidationException( __( 'Invalid nonce', 'wp-auth0' ) ); @@ -338,7 +327,7 @@ public function implicit_login() { if ( $this->login_user( $decoded_token, $id_token ) ) { // Validated above in $this->init_auth0(). - $state_decoded = $this->get_state( true ); + $state_decoded = $this->get_state(); if ( ! empty( $state_decoded->interim ) ) { include WPA0_PLUGIN_DIR . 'templates/login-interim.php'; @@ -541,7 +530,6 @@ public function logout() { } /** - * * Outputs JS on wp-login.php to log a user in if an Auth0 session is found. * Hooked to `login_message` filter. * IMPORTANT: Internal callback use only, do not call this function directly! @@ -581,8 +569,8 @@ public function auth0_singlelogout_footer() { /** * End the PHP session. - * - * TODO: Deprecate + * + * TODO: Deprecate */ public function end_session() { if ( session_id() ) { @@ -591,9 +579,9 @@ public function end_session() { } /** - * Get and filter the scope used for access and ID tokens + * Get and filter the scope used for access and ID tokens. * - * @param string $context - how are the scopes being used? + * @param string $context - how the scopes are being used. * * @return string */ @@ -612,30 +600,30 @@ public static function get_userinfo_scope( $context = '' ) { * @return array */ public static function get_authorize_params( $connection = null, $redirect_to = null ) { - $params = array(); - $options = WP_Auth0_Options::Instance(); + $params = array(); + $options = WP_Auth0_Options::Instance(); $lock_options = new WP_Auth0_Lock10_Options(); - $is_implicit = (bool) $options->get( 'auth0_implicit_workflow', FALSE ); - $nonce = WP_Auth0_Nonce_Handler::getInstance()->get(); + $is_implicit = (bool) $options->get( 'auth0_implicit_workflow', false ); + $nonce = WP_Auth0_Nonce_Handler::get_instance()->get_uniqid(); - $params[ 'client_id' ] = $options->get( 'client_id' ); - $params[ 'scope' ] = self::get_userinfo_scope( 'authorize_url' ); - $params[ 'response_type' ] = $is_implicit ? 'id_token': 'code'; - $params[ 'redirect_uri' ] = $is_implicit + $params['client_id'] = $options->get( 'client_id' ); + $params['scope'] = self::get_userinfo_scope( 'authorize_url' ); + $params['response_type'] = $is_implicit ? 'id_token' : 'code'; + $params['redirect_uri'] = $is_implicit ? $lock_options->get_implicit_callback_url() : $options->get_wp_auth0_url( null ); if ( $is_implicit ) { - $params[ 'nonce' ] = $nonce; + $params['nonce'] = $nonce; } if ( ! empty( $connection ) ) { - $params[ 'connection' ] = $connection; + $params['connection'] = $connection; } // Get the telemetry header. - $telemetry = WP_Auth0_Api_Client::get_info_headers(); - $params[ 'auth0Client' ] = $telemetry[ 'Auth0-Client' ]; + $telemetry = WP_Auth0_Api_Client::get_info_headers(); + $params['auth0Client'] = $telemetry['Auth0-Client']; // Where should the user be redirected after logging in? if ( empty( $redirect_to ) && ! empty( $_GET['redirect_to'] ) ) { @@ -645,11 +633,15 @@ public static function get_authorize_params( $connection = null, $redirect_to = } // State parameter, checked during login callback. - $params[ 'state' ] = base64_encode( json_encode( array( - 'interim' => false, - 'nonce' => $nonce, - 'redirect_to' => filter_var( $redirect_to, FILTER_SANITIZE_URL ), - ) ) ); + $params['state'] = base64_encode( + json_encode( + array( + 'interim' => false, + 'nonce' => $nonce, + 'redirect_to' => filter_var( $redirect_to, FILTER_SANITIZE_URL ), + ) + ) + ); return $params; } @@ -675,41 +667,13 @@ protected function query_vars( $key ) { /** * Get the state value returned from Auth0 during login processing. * - * @param bool $decoded - pass `true` to return decoded state, leave blank for raw string. - * * @return string|object|null */ - protected function get_state( $decoded = false ) { - - if ( empty( $this->state ) ) { - // Get and store base64 encoded state. - $state_val = isset( $_REQUEST['state'] ) ? $_REQUEST['state'] : ''; - $state_val = urldecode( $state_val ); - $this->state = $state_val; - - // Decode and store the state. - $state_val = base64_decode( $state_val ); - $this->state_decoded = json_decode( $state_val ); - } - - if ( $decoded ) { - return is_object( $this->state_decoded ) ? $this->state_decoded : null; - } else { - return $this->state; - } - } - - /** - * Check the state send back from Auth0 with the one stored in the user's browser. - * - * @return bool - */ - protected function validate_state() { - $valid = isset( $_COOKIE[ WPA0_STATE_COOKIE_NAME ] ) - ? $_COOKIE[ WPA0_STATE_COOKIE_NAME ] === $this->get_state() - : false; - setcookie( WPA0_STATE_COOKIE_NAME, '', 0, '/' ); - return $valid; + protected function get_state() { + $state_val = rawurldecode( $_REQUEST['state'] ); + $state_val = base64_decode( $state_val ); + $state_val = json_decode( $state_val ); + return $state_val; } /** diff --git a/lib/WP_Auth0_Nonce_Handler.php b/lib/WP_Auth0_Nonce_Handler.php index d3fcb117..f8943e70 100644 --- a/lib/WP_Auth0_Nonce_Handler.php +++ b/lib/WP_Auth0_Nonce_Handler.php @@ -1,134 +1,180 @@ init(); - } - - /** - * Private to prevent cloning - */ - private function __clone() {} - - /** - * Private to prevent serializing - */ - private function __sleep() {} - - /** - * Private to prevent unserializing - */ - private function __wakeup() {} - - /** - * Start-up process to make sure we have a nonce stored - */ - private function init() { - if ( isset( $_COOKIE[ self::COOKIE_NAME ] ) ) { - // Have a nonce cookie, don't want to generate a new one - $this->_uniqid = $_COOKIE[ self::COOKIE_NAME ]; - } else { - // No nonce cookie, need to create one - $this->_uniqid = $this->generateNonce(); - } - } - - /** - * Get the internal instance of the singleton - * - * @return WP_Auth0_Nonce_Handler - */ - public static final function getInstance() { - if ( null === self::$_instance ) { - self::$_instance = new WP_Auth0_Nonce_Handler(); - } - return self::$_instance; - } - - /** - * Return the unique ID used for nonce validation - * - * @return string - */ - public function get() { - return $this->_uniqid; - } - - /** - * Check if the stored nonce matches a specific value - * - * @param string $nonce - the nonce to validate against the stored value - * - * @return bool - */ - public function validate( $nonce ) { - $valid = isset( $_COOKIE[ self::COOKIE_NAME ] ) ? $_COOKIE[ self::COOKIE_NAME ] === $nonce : FALSE; - $this->reset(); - return $valid; - } - - /** - * Set the nonce cookie value - * - * @return bool - */ - public function setCookie() { - $_COOKIE[ self::COOKIE_NAME ] = $this->_uniqid; - return setcookie( self::COOKIE_NAME, $this->_uniqid, time() + self::COOKIE_EXPIRES, '/' ); - } - - /** - * Reset the nonce cookie value - * - * @return bool - */ - public function reset() { - return setcookie( self::COOKIE_NAME, '', 0 ); - } - - /** - * Generate a random ID - * If using on PHP 7, it will be cryptographically secure - * - * @see https://secure.php.net/manual/en/function.random-bytes.php - * - * @param int $bytes - number of bytes to generate - * - * @return string - */ - public function generateNonce( $bytes = 32 ) { - $nonce_bytes = function_exists( 'random_bytes' ) ? random_bytes( $bytes ) : openssl_random_pseudo_bytes( $bytes ); - return bin2hex( $nonce_bytes ); - } -} \ No newline at end of file +/** + * Contains WP_Auth0_Nonce_Handler. + * + * @package WP-Auth0 + */ + +/** + * Class WP_Auth0_Nonce_Handler for generating and storing nonce-type values. + */ +class WP_Auth0_Nonce_Handler { + + /** + * Cookie name used for storage and verification. + * + * @var string + */ + const UNIQID_COOKIE_NAME = 'auth0_nonce_uniqid'; + + /** + * Time, in seconds, for the cookie to last. + * Added to time() to determine expiration time. + * + * @var integer + */ + const COOKIE_EXPIRES = HOUR_IN_SECONDS; + + /** + * Singleton class instance. + * + * @var WP_Auth0_Nonce_Handler|null + */ + protected static $_instance = null; + + /** + * Unique ID used as a nonce or salting. + * + * @var string + */ + private $_uniqid; + + /** + * Private to prevent cloning. + */ + private function __clone() {} + + /** + * Private to prevent unserializing. + */ + private function __wakeup() {} + + /** + * WP_Auth0_Nonce_Handler constructor. + * Private to prevent new instances of this class. + */ + private function __construct() { + $this->init(); + } + + /** + * Start-up process to make sure we have something stored. + */ + private function init() { + if ( isset( $_COOKIE[ static::UNIQID_COOKIE_NAME ] ) ) { + // Have a cookie, don't want to generate a new one. + $this->_uniqid = $_COOKIE[ static::UNIQID_COOKIE_NAME ]; + } else { + // No cookie, need to create one. + $this->_uniqid = $this->generate_nonce(); + } + } + + /** + * Get the internal instance of the singleton. + * + * @return WP_Auth0_State_Handler|WP_Auth0_Nonce_Handler|WP_Auth0_Nonce_Handler + */ + final public static function get_instance() { + if ( is_null( static::$_instance ) ) { + static::$_instance = new static(); + } + return static::$_instance; + } + + /** + * Return the unique ID used for validation. + * + * @return string + */ + public function get_uniqid() { + return $this->_uniqid; + } + + /** + * Return the cookie expiration time to set. + * + * @return integer + */ + public function get_cookie_exp() { + return time() + self::COOKIE_EXPIRES; + } + + /** + * Set the random storage cookie. + * + * @return bool + */ + public function set_cookie() { + return $this->handle_cookie( static::UNIQID_COOKIE_NAME, $this->_uniqid, $this->get_cookie_exp() ); + } + + /** + * Validate a received value against the stored value. + * + * @param string $value - value to validate against what was stored. + * + * @return bool + */ + public function validate( $value ) { + $cookie_name = $this->get_validation_cookie_name(); + $valid = isset( $_COOKIE[ $cookie_name ] ) ? $_COOKIE[ $cookie_name ] === $value : false; + $this->reset(); + return $valid; + } + + /** + * Reset/delete a cookie. + * + * @return bool + */ + public function reset() { + return $this->handle_cookie( static::UNIQID_COOKIE_NAME, '', 0 ); + } + + /** + * Generate a random ID to use. + * If using on PHP 7, it will be cryptographically secure. + * + * @see https://secure.php.net/manual/en/function.random-bytes.php + * + * @param int $bytes - number of bytes to generate. + * + * @return string + */ + public function generate_nonce( $bytes = 32 ) { + $nonce_bytes = function_exists( 'random_bytes' ) + // phpcs:ignore + ? random_bytes( $bytes ) + : openssl_random_pseudo_bytes( $bytes ); + return bin2hex( $nonce_bytes ); + } + + /** + * Set or delete a cookie value. + * + * @param string $cookie_name - name of the cookie to set. + * @param mixed $cookie_value - value to set for the cookie. + * @param int $cookie_exp - cookie expiration, pass any value less than now to delete the cookie. + * + * @return bool + */ + protected function handle_cookie( $cookie_name, $cookie_value, $cookie_exp ) { + if ( $cookie_exp <= time() ) { + unset( $_COOKIE[ $cookie_name ] ); + $cookie_exp = 0; + } else { + $_COOKIE[ $cookie_name ] = $cookie_value; + } + return setcookie( $cookie_name, $cookie_value, $cookie_exp, '/' ); + } + + /** + * Get the name of the cookie to validate. + * + * @return string + */ + protected function get_validation_cookie_name() { + return static::UNIQID_COOKIE_NAME; + } +} diff --git a/lib/WP_Auth0_State_Handler.php b/lib/WP_Auth0_State_Handler.php new file mode 100644 index 00000000..89188f39 --- /dev/null +++ b/lib/WP_Auth0_State_Handler.php @@ -0,0 +1,64 @@ +set_cookie(); + return $this->handle_cookie( self::STATE_COOKIE_NAME, $value, $this->get_cookie_exp() ); + } + + /** + * Delete the state and uniqid cookies. + * + * @return bool + */ + public function reset() { + parent::reset(); + return $this->handle_cookie( self::STATE_COOKIE_NAME, '', 0 ); + } + + /** + * Get the name of the cookie to validate. + * + * @return string + */ + protected function get_validation_cookie_name() { + return self::STATE_COOKIE_NAME; + } +} diff --git a/templates/auth0-sso-handler-lock10.php b/templates/auth0-sso-handler-lock10.php index bdcfee7a..3b1cfcd7 100644 --- a/templates/auth0-sso-handler-lock10.php +++ b/templates/auth0-sso-handler-lock10.php @@ -36,6 +36,8 @@ var $input2=$(document.createElement('input')).attr('name','state').val(authResult.state); $form.append($input).append($input2); $("body").append($form); + Cookies.set( '', authResult.state ); + Cookies.set( '', authResult.idTokenPayload.nonce ); $form.submit(); }); } diff --git a/templates/login-form.php b/templates/login-form.php index aab42514..b5e3b179 100755 --- a/templates/login-form.php +++ b/templates/login-form.php @@ -21,7 +21,8 @@ function renderAuth0Form( $canShowLegacyLogin = true, $specialSettings = array() 'ready' => WP_Auth0::ready(), 'domain' => $options->get( 'domain' ), 'clientId' => $options->get( 'client_id' ), - 'stateCookieName' => WPA0_STATE_COOKIE_NAME, + 'stateCookieName' => WP_Auth0_State_Handler::STATE_COOKIE_NAME, + 'nonceCookieName' => WP_Auth0_Nonce_Handler::UNIQID_COOKIE_NAME, 'usePasswordless' => $use_passwordless, 'loginFormId' => WPA0_AUTH0_LOGIN_FORM_ID, 'showAsModal' => ! empty( $specialSettings['show_as_modal'] ),