Plugin Directory

Changeset 3781981


Ignore:
Timestamp:
11/03/2026 02:11:25 PM (63 minutes ago)
Author:
simpliteca
Message:

3.10.0: Remote support access (one-click admin SSO login)

Location:
simpliroot-lead-capture/trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • simpliroot-lead-capture/trunk/readme.txt

    r3771277 r3781981  
    55Tested up to: 7.0
    66Requires PHP: 7.4
    7 Stable tag: 3.9.0
     7Stable tag: 3.10.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    5353Paste your project token and pick what to track. "Test connection" and "Send test lead" buttons confirm everything works.
    5454
     55= Optional: one-click support access =
     56
     57If you work with the SimpliRoot team, you can let an authorized SimpliRoot panel user open your WordPress admin from the panel with one click — without you ever sharing a password. This is **off by default** and must be switched on by a WordPress administrator on the SimpliRoot settings screen (under "Remote support access"). When on, the plugin creates a dedicated `simpliroot-support` administrator account and signs each login request with your project token, which never travels over the network. Every entry is logged on the settings screen and in your panel. Turn the toggle off, or delete the `simpliroot-support` user, to revoke access immediately.
     58
    5559= Requirements =
    5660
    … …  
    7781
    78827. **When you click "Send test lead"** (admin only): a sample lead is sent to the same lead endpoint so you can verify the connection.
     83
     848. **Regular check-in**: the plugin periodically calls `https://app.simpliroot.com/api/dni/ping` with your project id, site domain and plugin version to fetch the project's current settings. If you enable "Remote support access" (one-click support login, off by default — see above), this check-in also reports that the option is on, so the panel can offer the login. No login happens unless an authorized panel user initiates it and the request is cryptographically verified by the plugin.
    7985
    8086Your use of the SimpliRoot service is governed by its terms and privacy policy:
    … …  
    122128
    123129
     130
     131= 3.10.0 =
     132* New optional "Remote support access": let an authorized SimpliRoot panel user open your wp-admin with one click, without sharing a password. Off by default; a WordPress administrator opts in on the settings screen. Each login is signed with your project token (never sent over the network), uses a dedicated `simpliroot-support` administrator account, is one-time and expires in seconds, and is logged on both sides. Revoke any time by turning the toggle off or deleting the support user.
    124133
    125134= 3.9.0 =
  • simpliroot-lead-capture/trunk/simpliroot-lead-capture.php

    r3771277 r3781981  
    44 * Plugin URI:  https://app.simpliroot.com
    55 * Description: Live chat with an AI bot, plus lead capture (Contact Form 7, Elementor, WPForms, Gravity, Fluent + any other form) and call tracking — all sent to your SimpliRoot CRM server-side (no cookies/GTM needed), with full ad-platform attribution. Configure with just your project token.
    6  * Version:           3.9.0
     6 * Version:           3.10.0
    77 * Requires at least: 5.7
    88 * Requires PHP:      7.4
    … …  
    1616if (!defined('ABSPATH')) exit;
    1717
    18 define('SIMPLIROOT_VERSION',  '3.9.0');
     18define('SIMPLIROOT_VERSION',  '3.10.0');
    1919define('SIMPLIROOT_API_BASE', defined('SIMPLIROOT_API_BASE') ? SIMPLIROOT_API_BASE : 'https://app.simpliroot.com');
    2020define('SIMPLIROOT_OPT',      'simpliroot_settings'); // option key
    … …  
    5555        'domain'        => '',
    5656        'last_check'    => 0,
     57        // Remote one-click admin login (SSO) from the SimpliRoot panel. OFF by
     58        // default: the site admin must opt in here, which is the client's consent.
     59        // Every gate fails CLOSED — see the /sso endpoint at the bottom.
     60        'enable_sso'    => 0,
    5761    ]);
    5862}
    … …  
    111115            'domain'     => wp_parse_url(home_url(), PHP_URL_HOST),
    112116            'v'          => SIMPLIROOT_VERSION,
     117            // Report local SSO consent so the panel shows the "Enter WordPress"
     118            // button only when this site has actually opted in.
     119            'sso'        => !empty($s['enable_sso']) ? 1 : 0,
    113120        ],
    114121    ]);
    … …  
    12291236    $s = simpliroot_local_settings();
    12301237    $s['token']        = sanitize_text_field(wp_unslash($_POST['token'] ?? ''));
     1238    // Local consent toggle: the WP admin opts in to one-click remote admin login.
     1239    // Unchecked box sends nothing, so an absent key means OFF.
     1240    $s['enable_sso']   = empty($_POST['enable_sso']) ? 0 : 1;
    12311241    // enable_forms/enable_calls are NOT edited here anymore — the SimpliRoot
    12321242    // panel controls them (see simpliroot_server_config). Local values stay at
    … …  
    15491559        </div>
    15501560
     1561        <div class="card" style="max-width:760px;padding:4px 16px 16px">
     1562          <h2>Remote support access</h2>
     1563          <table class="form-table">
     1564            <tr>
     1565              <th scope="row">One-click admin login</th>
     1566              <td>
     1567                <label>
     1568                  <input type="checkbox" name="enable_sso" value="1" <?php checked(!empty($s['enable_sso'])); ?> <?php disabled(!$connected); ?>>
     1569                  Let the SimpliRoot team open this site's admin from the SimpliRoot panel, with one click, without you sharing a password.
     1570                </label>
     1571                <p class="description">
     1572                  <strong>Off by default.</strong> When on, an authorized SimpliRoot panel user can be logged in to <code>wp-admin</code> as a dedicated
     1573                  <code>simpliroot-support</code> administrator account that this plugin creates. Each entry is cryptographically signed with your project
     1574                  token (the same secret already used by this plugin — it never travels over the network) and is logged here and in the panel.
     1575                  Turn this off, or delete the <code>simpliroot-support</code> user, to revoke access immediately.
     1576                  <?php if (!$connected): ?><br><em>Connect a project token first to enable this.</em><?php endif; ?>
     1577                </p>
     1578                <?php
     1579                  $log = get_option('simpliroot_sso_log', []);
     1580                  if (is_array($log) && $log) {
     1581                      echo '<p class="description" style="margin-top:10px"><strong>Recent logins:</strong></p><ul style="margin:0 0 0 4px;font-size:12px;color:#555">';
     1582                      foreach (array_slice(array_reverse($log), 0, 5) as $e) {
     1583                          echo '<li>' . esc_html(gmdate('Y-m-d H:i', (int) ($e['t'] ?? 0))) . ' UTC — '
     1584                              . esc_html($e['email'] ?? '?') . ' — ' . esc_html($e['ip'] ?? '?')
     1585                              . ' — ' . esc_html($e['result'] ?? '?') . '</li>';
     1586                      }
     1587                      echo '</ul>';
     1588                  }
     1589                ?>
     1590              </td>
     1591            </tr>
     1592          </table>
     1593        </div>
     1594
    15511595      </form>
    15521596    </div>
    … …  
    16201664    ], 200);
    16211665}
     1666
     1667
     1668// ═════════════════════════════════════════════════════════════════════════════
     1669// SSO — one-click admin login from the SimpliRoot panel ("Enter WordPress")
     1670// ═════════════════════════════════════════════════════════════════════════════
     1671//
     1672// An authorized user in the SimpliRoot panel clicks "Enter WordPress" for a
     1673// project. The panel mints a SHORT-LIVED, SINGLE-USE ticket signed with the
     1674// project token (the same shared secret used by /invalidate — it never travels
     1675// over the network, only the signature computed from it) and the browser POSTs
     1676// it here. We verify, log the panel user in as a DEDICATED support account and
     1677// redirect to wp-admin. No password ever changes hands.
     1678//
     1679// Wire contract (panel → browser form POST → here):
     1680//
     1681//   POST https://{domena_klienta}/wp-json/simpliroot/v1/sso   (form-encoded)
     1682//     p = base64url( json {
     1683//           "pu_id":     <int>    panel user id
     1684//           "pu_email":  <string> panel user email (for the audit log)
     1685//           "project_id":<int>    must match this site's project
     1686//           "ts":        <int>    unix seconds
     1687//           "nonce":     <string> random, one-time
     1688//         } )
     1689//     s = hex( hmac_sha256( p, project_token ) )
     1690//
     1691// POST (not a GET link) on purpose: the ticket rides in the request body, so it
     1692// never lands in access logs, the Referer header or browser history.
     1693//
     1694// EVERY gate fails CLOSED. SSO off, no token, wrong project, bad signature, a
     1695// ticket older than 90 s, or a reused nonce → nothing happens but a logged 401/
     1696// 403. Rotating the project token (same path as /invalidate) kills old tickets.
     1697// The client revokes access any time by unticking the toggle or deleting the
     1698// simpliroot-support user.
     1699
     1700add_action('rest_api_init', function () {
     1701    register_rest_route('simpliroot/v1', '/sso', [
     1702        'methods'             => 'POST',
     1703        'permission_callback' => '__return_true', // own HMAC verification below
     1704        'callback'            => 'simpliroot_rest_sso',
     1705    ]);
     1706});
     1707
     1708// base64url → raw. Tolerates missing padding.
     1709function simpliroot_b64url_decode($in) {
     1710    $in = strtr((string) $in, '-_', '+/');
     1711    $pad = strlen($in) % 4;
     1712    if ($pad) $in .= str_repeat('=', 4 - $pad);
     1713    return base64_decode($in, true);
     1714}
     1715
     1716function simpliroot_client_ip() {
     1717    $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
     1718    return $ip !== '' ? $ip : '?';
     1719}
     1720
     1721// Append an audit entry (kept trimmed) and mirror it to the PHP error log.
     1722function simpliroot_sso_log($email, $result) {
     1723    $log = get_option('simpliroot_sso_log', []);
     1724    if (!is_array($log)) $log = [];
     1725    $log[] = [
     1726        't'      => time(),
     1727        'email'  => (string) $email,
     1728        'ip'     => simpliroot_client_ip(),
     1729        'result' => (string) $result,
     1730    ];
     1731    if (count($log) > 50) $log = array_slice($log, -50);
     1732    update_option('simpliroot_sso_log', $log, false);
     1733    error_log(sprintf('[simpliroot] SSO %s for %s from %s', $result, $email ?: '?', simpliroot_client_ip()));
     1734}
     1735
     1736// Get (or lazily create) the dedicated support administrator. Created ONLY while
     1737// SSO is on, so the account never exists without the client's consent.
     1738function simpliroot_sso_support_user() {
     1739    $login = 'simpliroot-support';
     1740    $user  = get_user_by('login', $login);
     1741    if ($user) return $user;
     1742
     1743    $host  = wp_parse_url(home_url(), PHP_URL_HOST) ?: 'site';
     1744    $email = 'support+' . preg_replace('/[^a-z0-9.-]/i', '', $host) . '@simpliroot.com';
     1745    if (email_exists($email)) $email = 'support+' . wp_generate_password(10, false) . '@simpliroot.com';
     1746
     1747    $uid = wp_insert_user([
     1748        'user_login'   => $login,
     1749        'user_pass'    => wp_generate_password(40, true, true),
     1750        'user_email'   => $email,
     1751        'display_name' => 'SimpliRoot Support',
     1752        'role'         => 'administrator',
     1753    ]);
     1754    if (is_wp_error($uid)) return null;
     1755    update_user_meta($uid, 'simpliroot_managed', 1);
     1756    return get_user_by('id', $uid);
     1757}
     1758
     1759function simpliroot_rest_sso(WP_REST_Request $req) {
     1760    $s = simpliroot_local_settings();
     1761
     1762    // Gate 1: consent + connection. Fail closed.
     1763    if (empty($s['enable_sso']) || empty($s['project_id']) || empty($s['token'])) {
     1764        simpliroot_sso_log('', 'blocked_disabled');
     1765        return new WP_REST_Response(['ok' => false, 'reason' => 'sso_disabled'], 403);
     1766    }
     1767
     1768    $p = (string) $req->get_param('p');
     1769    $sig = (string) $req->get_param('s');
     1770    if ($p === '' || $sig === '') {
     1771        simpliroot_sso_log('', 'bad_request');
     1772        return new WP_REST_Response(['ok' => false, 'reason' => 'missing_ticket'], 400);
     1773    }
     1774
     1775    // Gate 2: signature over the exact payload string.
     1776    $expect = hash_hmac('sha256', $p, (string) $s['token']);
     1777    if (!hash_equals($expect, $sig)) {
     1778        simpliroot_sso_log('', 'bad_signature');
     1779        return new WP_REST_Response(['ok' => false, 'reason' => 'bad_signature'], 401);
     1780    }
     1781
     1782    $raw  = simpliroot_b64url_decode($p);
     1783    $data = $raw ? json_decode($raw, true) : null;
     1784    if (!is_array($data) || !isset($data['project_id'], $data['ts'], $data['nonce'])) {
     1785        simpliroot_sso_log('', 'bad_payload');
     1786        return new WP_REST_Response(['ok' => false, 'reason' => 'bad_payload'], 400);
     1787    }
     1788
     1789    $email = isset($data['pu_email']) ? sanitize_email((string) $data['pu_email']) : '';
     1790
     1791    // Gate 3: ticket is for THIS project.
     1792    if ((int) $data['project_id'] !== (int) $s['project_id']) {
     1793        simpliroot_sso_log($email, 'project_mismatch');
     1794        return new WP_REST_Response(['ok' => false, 'reason' => 'project_mismatch'], 403);
     1795    }
     1796
     1797    // Gate 4: freshness (±90 s).
     1798    if (abs(time() - (int) $data['ts']) > 90) {
     1799        simpliroot_sso_log($email, 'stale');
     1800        return new WP_REST_Response(['ok' => false, 'reason' => 'stale'], 401);
     1801    }
     1802
     1803    // Gate 5: single use. A reused nonce is a replay — refuse.
     1804    $nkey = 'simpliroot_sso_n_' . md5((string) $data['nonce']);
     1805    if (get_transient($nkey)) {
     1806        simpliroot_sso_log($email, 'replay');
     1807        return new WP_REST_Response(['ok' => false, 'reason' => 'replay'], 401);
     1808    }
     1809    set_transient($nkey, 1, 180); // outlive the 90 s window comfortably
     1810
     1811    $user = simpliroot_sso_support_user();
     1812    if (!$user) {
     1813        simpliroot_sso_log($email, 'user_error');
     1814        return new WP_REST_Response(['ok' => false, 'reason' => 'user_error'], 500);
     1815    }
     1816
     1817    // Log the panel user in as the support account and send them to wp-admin.
     1818    wp_set_auth_cookie($user->ID, false, is_ssl());
     1819    wp_set_current_user($user->ID);
     1820    simpliroot_sso_log($email, 'ok');
     1821
     1822    nocache_headers();
     1823    wp_safe_redirect(admin_url());
     1824    exit;
     1825}
Note: See TracChangeset for help on using the changeset viewer.