WP_REST_Server::READABLE, 'permission_callback' => '__return_true', 'callback' => array( __CLASS__, 'challenge' ), 'args' => array( 'purpose' => array( 'type' => 'string', 'default' => SPA_Challenge::PURPOSE_LOGIN, ), ), ) ); register_rest_route( self::NS, '/login', array( 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => '__return_true', 'callback' => array( __CLASS__, 'login' ), 'args' => self::signature_args(), ) ); register_rest_route( self::NS, '/register', array( 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => '__return_true', 'callback' => array( __CLASS__, 'register' ), 'args' => self::signature_args(), ) ); register_rest_route( self::NS, '/confirm', array( 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => function () { return is_user_logged_in(); }, 'callback' => array( __CLASS__, 'confirm' ), 'args' => self::signature_args(), ) ); } private static function signature_args() { return array( 'nonce' => array( 'type' => 'string', 'required' => true, ), 'signature' => array( 'type' => 'string', 'required' => true, ), ); } /** Hand out something to sign. */ public static function challenge( WP_REST_Request $request ) { $limited = SPA_Challenge::check_rate_limit( 'challenge', 60, 300 ); if ( is_wp_error( $limited ) ) { return $limited; } $purpose = (string) $request->get_param( 'purpose' ); if ( ! SPA_Challenge::is_known_purpose( $purpose ) ) { return new WP_Error( 'sirius_bad_purpose', __( 'Unknown purpose.', 'sirius-press' ), array( 'status' => 400 ) ); } if ( SPA_Challenge::PURPOSE_REGISTER === $purpose && ! SP_Settings::open_registration() ) { return new WP_Error( 'sirius_closed', __( 'This site is not accepting new accounts.', 'sirius-press' ), array( 'status' => 403 ) ); } $nonce = SPA_Challenge::issue(); return rest_ensure_response( array( 'nonce' => $nonce, 'message' => SPA_Challenge::message( $nonce, $purpose ), 'purpose' => $purpose, 'expires_in' => SPA_Challenge::TTL, 'prefix' => SP_Settings::prefix(), ) ); } public static function login( WP_REST_Request $request ) { $limited = SPA_Challenge::check_rate_limit( 'login' ); if ( is_wp_error( $limited ) ) { return $limited; } $address = SPA_Challenge::verify( (string) $request->get_param( 'nonce' ), (string) $request->get_param( 'signature' ), SPA_Challenge::PURPOSE_LOGIN ); if ( is_wp_error( $address ) ) { return self::with_status( $address, 401 ); } $user = SP_Identity::user_by_address( $address ); if ( ! $user ) { return new WP_Error( 'sirius_no_account', __( 'No account on this site uses that wallet.', 'sirius-press' ), array( 'status' => 404, 'address' => $address, ) ); } $remember = (bool) $request->get_param( 'remember' ); wp_set_current_user( $user->ID ); wp_set_auth_cookie( $user->ID, $remember ); do_action( 'wp_login', $user->user_login, $user ); do_action( 'sirius_press_wallet_authenticated', $user, $address ); return rest_ensure_response( array( 'ok' => true, 'user_id' => (int) $user->ID, 'address' => $address, 'redirect' => user_can( $user, 'read' ) ? admin_url() : home_url( '/' ), ) ); } public static function register( WP_REST_Request $request ) { if ( ! SP_Settings::open_registration() ) { return new WP_Error( 'sirius_closed', __( 'This site is not accepting new accounts.', 'sirius-press' ), array( 'status' => 403 ) ); } $limited = SPA_Challenge::check_rate_limit( 'register', 10, 900 ); if ( is_wp_error( $limited ) ) { return $limited; } // Required, for the reason set out in class-spa-challenge.php: recovery // alone would happily mint an account for an address the caller cannot // sign with. $claimed = (string) $request->get_param( 'address' ); if ( '' === $claimed ) { return new WP_Error( 'sirius_missing_address', __( 'Send the address you signed with, so the signature can be checked against it.', 'sirius-press' ), array( 'status' => 400 ) ); } $address = SPA_Challenge::verify( (string) $request->get_param( 'nonce' ), (string) $request->get_param( 'signature' ), SPA_Challenge::PURPOSE_REGISTER, $claimed ); if ( is_wp_error( $address ) ) { return self::with_status( $address, 400 ); } $user_id = SP_Identity::create_user( $address, (string) $request->get_param( 'user_login' ) ); if ( is_wp_error( $user_id ) ) { return self::with_status( $user_id, 409 ); } wp_set_current_user( $user_id ); wp_set_auth_cookie( $user_id, false ); return rest_ensure_response( array( 'ok' => true, 'user_id' => (int) $user_id, 'address' => $address, ) ); } /** * Re-prove the current user's key. * * Returns 403 rather than 200-with-false when the signature belongs to * somebody else's wallet, so a caller that forgets to check the body still * fails closed. */ public static function confirm( WP_REST_Request $request ) { $limited = SPA_Challenge::check_rate_limit( 'confirm', 30, 300 ); if ( is_wp_error( $limited ) ) { return $limited; } $address = SPA_Challenge::verify( (string) $request->get_param( 'nonce' ), (string) $request->get_param( 'signature' ), SPA_Challenge::PURPOSE_CONFIRM ); if ( is_wp_error( $address ) ) { return self::with_status( $address, 400 ); } $mine = SP_Identity::address_of( get_current_user_id() ); if ( '' === $mine || ! hash_equals( $mine, $address ) ) { return new WP_Error( 'sirius_wrong_wallet', __( 'That signature is from a different wallet than the one signed in.', 'sirius-press' ), array( 'status' => 403 ) ); } /** * Fires when a signed-in user re-proves their key. * * @param int $user_id * @param string $address */ do_action( 'sirius_press_wallet_confirmed', get_current_user_id(), $address ); return rest_ensure_response( array( 'ok' => true, 'address' => $address, ) ); } /** Attach an HTTP status to a WP_Error that was created without one. */ private static function with_status( WP_Error $error, $status ) { $data = $error->get_error_data(); if ( ! is_array( $data ) ) { $data = array(); } if ( empty( $data['status'] ) ) { $data['status'] = $status; } $error->add_data( $data, $error->get_error_code() ); return $error; } }