1, 'redirect_to' => rawurlencode( $redirect ) ), self::callback_url( $provider ) ); return is_user_logged_in() ? wp_nonce_url( $url, 'prps_link_' . $provider ) : $url; } public static function route() { $provider = (string) get_query_var( self::VAR ); if ( '' === $provider ) { return; } nocache_headers(); /** * Свой обработчик способа входа (Pro: MAX). * * @param bool $handled Обработано. * @param string $provider Способ входа. */ if ( apply_filters( 'prps/route', false, $provider ) ) { exit; } if ( ! in_array( $provider, array( 'vk', 'yandex' ), true ) || ! isset( PRPS_Settings::methods()[ $provider ] ) ) { self::fail( __( 'Этот способ входа выключен.', 'propusk' ) ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- OAuth: защита от подделки — параметр state. if ( isset( $_GET['error'] ) ) { self::fail( __( 'Вход отменён.', 'propusk' ) ); } if ( empty( $_GET['code'] ) ) { // Привязка к уже вошедшему аккаунту начинается только по подписанной ссылке из профиля. if ( is_user_logged_in() && ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'prps_link_' . $provider ) ) ) { self::fail( __( 'Ссылка для привязки устарела. Откройте профиль и нажмите кнопку ещё раз.', 'propusk' ) ); } $redirect = isset( $_GET['redirect_to'] ) ? esc_url_raw( wp_unslash( $_GET['redirect_to'] ) ) : ''; wp_redirect( self::authorize_url( $provider, $redirect ) ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- адрес VK ID или Яндекс ID. exit; } $state = isset( $_GET['state'] ) ? sanitize_key( wp_unslash( $_GET['state'] ) ) : ''; $code = sanitize_text_field( wp_unslash( $_GET['code'] ) ); $dev = isset( $_GET['device_id'] ) ? sanitize_text_field( wp_unslash( $_GET['device_id'] ) ) : ''; // phpcs:enable $saved = '' !== $state ? get_transient( 'prps_st_' . $state ) : false; delete_transient( 'prps_st_' . $state ); $cookie = isset( $_COOKIE[ self::COOKIE ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE ] ) ) : ''; self::clear_cookie(); // state действует только в том браузере и для того пользователя, кто начал вход (защита от подмены ответа — CSRF). if ( ! is_array( $saved ) || $saved['provider'] !== $provider || '' === $cookie || ! hash_equals( (string) $saved['browser'], hash_hmac( 'sha256', $cookie, wp_salt( 'nonce' ) ) ) || (int) $saved['user'] !== get_current_user_id() ) { self::fail( __( 'Ссылка для входа устарела. Попробуйте ещё раз.', 'propusk' ) ); } $who = 'vk' === $provider ? self::vk_user( $code, $dev, $state, $saved['verifier'] ) : self::yandex_user( $code ); if ( is_wp_error( $who ) ) { self::fail( $who->get_error_message() ); } $to = PRPS_Users::sign_in( $who, (string) $saved['redirect'] ); if ( is_wp_error( $to ) ) { self::fail( $to->get_error_message() ); } wp_safe_redirect( $to ); exit; } /** Cookie браузера, начавшего вход: SameSite=Lax пропускает её при возврате с VK ID / Яндекс ID. */ private static function set_cookie( $value, $expires ) { $args = array( 'expires' => $expires, 'path' => COOKIEPATH ? COOKIEPATH : '/', 'domain' => COOKIE_DOMAIN ? COOKIE_DOMAIN : '', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ); setcookie( self::COOKIE, $value, $args ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie } private static function clear_cookie() { if ( isset( $_COOKIE[ self::COOKIE ] ) ) { self::set_cookie( '', time() - HOUR_IN_SECONDS ); } } /** Ошибка — на страницу входа с сообщением. */ public static function fail( $message ) { set_transient( 'prps_err_' . md5( PRPS_Phone::ip() ), $message, 120 ); wp_safe_redirect( is_user_logged_in() ? admin_url( 'profile.php#prps-profile' ) : add_query_arg( 'prps_error', 1, wp_login_url() ) ); exit; } /** Сообщение об ошибке для показа (один раз). */ public static function take_error() { $key = 'prps_err_' . md5( PRPS_Phone::ip() ); $msg = get_transient( $key ); delete_transient( $key ); return is_string( $msg ) ? $msg : ''; } private static function b64url( $bin ) { return rtrim( strtr( base64_encode( $bin ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- PKCE по RFC 7636. } private static function authorize_url( $provider, $redirect ) { $state = strtolower( wp_generate_password( 32, false, false ) ); $verifier = self::b64url( random_bytes( 48 ) ); $browser = wp_generate_password( 40, false, false ); set_transient( 'prps_st_' . $state, array( 'provider' => $provider, 'verifier' => $verifier, 'redirect' => $redirect, 'browser' => hash_hmac( 'sha256', $browser, wp_salt( 'nonce' ) ), 'user' => get_current_user_id(), ), 15 * MINUTE_IN_SECONDS ); self::set_cookie( $browser, time() + 15 * MINUTE_IN_SECONDS ); $s = PRPS_Settings::all(); if ( 'vk' === $provider ) { return add_query_arg( array( 'response_type' => 'code', 'client_id' => rawurlencode( $s['vk_client_id'] ), 'redirect_uri' => rawurlencode( self::callback_url( 'vk' ) ), 'state' => $state, 'code_challenge' => self::b64url( hash( 'sha256', $verifier, true ) ), 'code_challenge_method' => 'S256', 'scope' => rawurlencode( 'vkid.personal_info email phone' ), ), 'https://id.vk.ru/authorize' ); } return add_query_arg( array( 'response_type' => 'code', 'client_id' => rawurlencode( $s['ya_client_id'] ), 'redirect_uri' => rawurlencode( self::callback_url( 'yandex' ) ), 'state' => $state, ), 'https://oauth.yandex.ru/authorize' ); } private static function json( $res ) { if ( is_wp_error( $res ) ) { return new WP_Error( 'network', __( 'Сервис входа недоступен. Попробуйте позже.', 'propusk' ) ); } $data = json_decode( wp_remote_retrieve_body( $res ), true ); return is_array( $data ) ? $data : new WP_Error( 'network', __( 'Сервис входа вернул неожиданный ответ.', 'propusk' ) ); } /** VK ID: код → токен → данные пользователя. */ private static function vk_user( $code, $device, $state, $verifier ) { $client = (string) PRPS_Settings::get( 'vk_client_id' ); $token = self::json( wp_remote_post( 'https://id.vk.ru/oauth2/auth', array( 'timeout' => 15, 'body' => array( 'grant_type' => 'authorization_code', 'code' => $code, 'code_verifier' => $verifier, 'client_id' => $client, 'device_id' => $device, 'redirect_uri' => self::callback_url( 'vk' ), 'state' => $state, ), ) ) ); if ( is_wp_error( $token ) || empty( $token['access_token'] ) ) { return new WP_Error( 'vk', __( 'VK ID не подтвердил вход. Попробуйте ещё раз.', 'propusk' ) ); } $info = self::json( wp_remote_post( 'https://id.vk.ru/oauth2/user_info', array( 'timeout' => 15, 'body' => array( 'client_id' => $client, 'access_token' => $token['access_token'], ), ) ) ); $u = is_array( $info ) ? ( $info['user'] ?? array() ) : array(); if ( empty( $u['user_id'] ) ) { return new WP_Error( 'vk', __( 'VK ID не передал данные пользователя.', 'propusk' ) ); } return array( 'provider' => 'vk', 'id' => (string) $u['user_id'], 'phone' => (string) ( $u['phone'] ?? '' ), 'email' => (string) ( $u['email'] ?? '' ), 'first_name' => (string) ( $u['first_name'] ?? '' ), 'last_name' => (string) ( $u['last_name'] ?? '' ), ); } /** Яндекс ID: код → токен → данные пользователя. */ private static function yandex_user( $code ) { $s = PRPS_Settings::all(); $token = self::json( wp_remote_post( 'https://oauth.yandex.ru/token', array( 'timeout' => 15, 'body' => array( 'grant_type' => 'authorization_code', 'code' => $code, 'client_id' => $s['ya_client_id'], 'client_secret' => $s['ya_secret'], ), ) ) ); if ( is_wp_error( $token ) || empty( $token['access_token'] ) ) { return new WP_Error( 'yandex', __( 'Яндекс ID не подтвердил вход. Попробуйте ещё раз.', 'propusk' ) ); } $u = self::json( wp_remote_get( 'https://login.yandex.ru/info?format=json', array( 'timeout' => 15, 'headers' => array( 'Authorization' => 'OAuth ' . $token['access_token'] ), ) ) ); if ( is_wp_error( $u ) || empty( $u['id'] ) ) { return new WP_Error( 'yandex', __( 'Яндекс ID не передал данные пользователя.', 'propusk' ) ); } return array( 'provider' => 'yandex', 'id' => (string) $u['id'], 'phone' => (string) ( $u['default_phone']['number'] ?? '' ), 'email' => (string) ( $u['default_email'] ?? '' ), 'first_name' => (string) ( $u['first_name'] ?? '' ), 'last_name' => (string) ( $u['last_name'] ?? '' ), ); } }