Files
WooNooW/.agent/reports/license-activation-research-2026-01-31.md
Dwindi Ramadhana a0b5f8496d feat: Implement OAuth license activation flow
- Add LicenseConnect.tsx focused OAuth confirmation page in customer SPA
- Add /licenses/oauth/validate and /licenses/oauth/confirm API endpoints
- Update App.tsx to render license-connect outside BaseLayout (no header/footer)
- Add license_activation_method field to product settings in Admin SPA
- Create LICENSING_MODULE.md with comprehensive OAuth flow documentation
- Update API_ROUTES.md with license module endpoints
2026-01-31 22:22:22 +07:00

16 KiB

OAuth-Style License Activation Research Report

Date: January 31, 2026
Objective: Design a strict license activation system requiring vendor site authentication


Executive Summary

After researching Elementor Pro, Tutor LMS, EDD Software Licensing, and industry standards, the redirect-based OAuth-like activation flow is the most secure and user-friendly approach. This pattern:

  • Prevents license key sharing by tying activation to user accounts
  • Provides better UX than manual key entry
  • Enables flexible license management
  • Creates an anti-piracy layer beyond just key validation

Industry Analysis

1. Elementor Pro

Aspect Implementation
Flow "Connect & Activate" button → redirect to Elementor.com → login required → authorize connection → return to WP Admin
Why Listed Market leader with 5M+ users; sets the standard for premium plugin activation
Anti-Piracy Account-tied activation; no ability to share just a license key
Fallback Manual key entry via hidden URL parameter ?mode=manually

Key Pattern: Elementor never shows the license key in the normal flow—users authenticate with their account, not a key.


2. Tutor LMS (Themeum)

Aspect Implementation
Flow License settings → Enter key → "Connect" button → redirect to Themeum → login → confirm connection
Why Listed Popular LMS plugin; hybrid approach (key + account verification)
Anti-Piracy License keys tied to specific domains registered in user account
License Display Keys visible in account dashboard for copy-paste

Key Pattern: Requires domain registration in vendor account before activation works.


3. Easy Digital Downloads (EDD) Software Licensing

Aspect Implementation
Flow API-based: plugin sends key + site URL to vendor → server validates → returns activation status
Why Listed Powers many WordPress plugin vendors (WPForms, MonsterInsights, etc.)
Anti-Piracy Activation limits (e.g., 1 site, 5 sites, unlimited); site URL tracking
Management Customer can manage activations in their EDD account

Key Pattern: Traditional key-based but with strict activation limits and site tracking.


4. WooCommerce Software License Manager

Aspect Implementation
Flow REST API with key + secret authentication
Why Listed Common for WooCommerce-based vendors
Anti-Piracy API-key authentication; activation records

Key Pattern: Programmatic API access, less user-facing UX focus.


Best Practices Identified

Anti-Piracy Measures

Measure Effectiveness UX Impact
Account authentication required ★★★★★ Minor inconvenience
Activation limits per license ★★★★☆ None
Domain/URL binding ★★★★☆ None
Tying updates/support to valid license ★★★★★ Incentivizes purchase
Periodic license re-validation ★★★☆☆ Can cause issues
Encrypted API communication (HTTPS) ★★★★★ None

UX Considerations

Consideration Priority
One-click activation (minimal friction) High
Clear error messages High
License status visibility in WP Admin Medium
Easy deactivation for site migrations High
Fallback manual activation Medium

Security Comparison

Method Piracy Resistance Implementation Complexity
Simple key validation Low Simple
Key + site URL binding Medium Medium
Key + activation limits Medium-High Medium
OAuth redirect + account tie High Complex
OAuth + key + activation limits Very High Complex

Your Proposed Flow Analysis

Original Flow Points

  1. User navigates to license page → clicks [ACTIVATE]
  2. Redirect to vendor site (licensing.woonoow.com or similar)
  3. Vendor site: login required
  4. Vendor shows licenses for user's account, filtered by product
  5. User selects license to connect
  6. Click "Connect This Site"
  7. Return to return_url after short delay

Identified Gaps

Gap Risk Solution
No state parameter CSRF attack possible Add signed state token
No nonce verification Replay attacks Include one-time nonce
Return URL manipulation Redirect hijacking Validate return URL on server
No deactivation flow User can't migrate Add disconnect button

Perfected Implementation Plan

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        WP ADMIN (Client Site)                   │
├─────────────────────────────────────────────────────────────────┤
│  Settings → License                                              │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Status: Not Connected                                    │   │
│  │  [🔗 Connect & Activate]                                  │   │
│  └──────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘
                        │
                        ▼ Redirect with signed params
┌──────────────────────────────────────────────────────────────────┐
│                    VENDOR SITE (License Server)                  │
├──────────────────────────────────────────────────────────────────┤
│  /license/connect?                                               │
│    product_id=woonoow-pro&                                       │
│    site_url=https://customer-site.com&                           │
│    return_url=https://customer-site.com/wp-admin/...&            │
│    state=<signed_token>&                                         │
│    nonce=<one_time_code>                                         │
├──────────────────────────────────────────────────────────────────┤
│  1. Force login if not authenticated                             │
│  2. Show licenses owned by user for this product                 │
│  3. User selects: "Pro License (3/5 sites used)"                 │
│  4. Click [Connect This Site]                                    │
│  5. Server records activation                                    │
│  6. Redirect back with activation token                          │
└──────────────────────────────────────────────────────────────────┘
                        │
                        ▼ Callback with activation token
┌──────────────────────────────────────────────────────────────────┐
│                        WP ADMIN (Client Site)                    │
├──────────────────────────────────────────────────────────────────┤
│  Callback handler:                                               │
│  1. Verify state matches stored value                            │
│  2. Exchange activation_token for license_key via API            │
│  3. Store license_key securely                                   │
│  4. Show success: "License activated successfully!"              │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  Status: ✅ Active                                        │   │
│  │  License: Pro (expires Dec 31, 2026)                      │   │
│  │  Sites: 4/5 activated                                     │   │
│  │  [Disconnect]                                             │   │
│  └──────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘

Detailed Flow

Phase 1: Initiation (Client Plugin)

// User clicks "Connect & Activate"
$params = [
    'product_id'  => 'woonoow-pro',
    'site_url'    => home_url(),
    'return_url'  => admin_url('admin.php?page=woonoow-license&action=callback'),
    'nonce'       => wp_create_nonce('woonoow_license_connect'),
    'state'       => $this->generate_state_token(), // Signed, stored in transient
    'timestamp'   => time(),
];

$redirect_url = 'https://licensing.woonoow.com/connect?' . http_build_query($params);
wp_redirect($redirect_url);

Phase 2: Authentication (Vendor Server)

  1. Login Gate: If user not logged in → redirect to login with ?redirect=/connect?...
  2. Validate Request: Check state, nonce, timestamp (reject if >10 min old)
  3. Fetch User Licenses: Query licenses owned by authenticated user for product_id
  4. Display License Selector:
    ┌─────────────────────────────────────────────────┐
    │  Connect site-name.com to your license          │
    ├─────────────────────────────────────────────────┤
    │  ○ WooNooW Pro - Agency (Unlimited sites)       │
    │  ● WooNooW Pro - Business (3/5 sites) ←selected │
    │  ○ WooNooW Pro - Personal (1/1 sites) [FULL]    │
    ├─────────────────────────────────────────────────┤
    │  [Cancel]              [Connect This Site]      │
    └─────────────────────────────────────────────────┘
    
  5. Record Activation: Insert into license_activations table
  6. Generate Callback: Redirect to return_url with:
    • activation_token: Short-lived token (5 min expiry)
    • state: Original state for verification

Phase 3: Callback (Client Plugin)

// Handle callback
$activation_token = sanitize_text_field($_GET['activation_token']);
$state = sanitize_text_field($_GET['state']);

// 1. Verify state matches stored transient
if (!$this->verify_state_token($state)) {
    wp_die('Invalid state. Possible CSRF attack.');
}

// 2. Exchange token for license details via secure API
$response = wp_remote_post('https://licensing.woonoow.com/api/v1/token/exchange', [
    'body' => [
        'activation_token' => $activation_token,
        'site_url' => home_url(),
    ],
]);

// 3. Store license data
$license_data = json_decode(wp_remote_retrieve_body($response), true);
update_option('woonoow_license', [
    'key'        => $license_data['license_key'],
    'status'     => 'active',
    'expires'    => $license_data['expires_at'],
    'tier'       => $license_data['tier'],
    'sites_used' => $license_data['sites_used'],
    'sites_max'  => $license_data['sites_max'],
]);

// 4. Redirect with success
wp_redirect(admin_url('admin.php?page=woonoow-license&activated=1'));

Security Parameters

Parameter Purpose Implementation
state CSRF protection HMAC-signed, stored in transient, expires 10 min
nonce Replay prevention One-time use, verified on server
timestamp Request freshness Reject requests >10 min old
activation_token Secure exchange Short-lived (5 min), single-use
site_url Domain binding Stored with activation record

Database Schema (Vendor Server)

CREATE TABLE license_activations (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    license_id BIGINT NOT NULL,
    site_url VARCHAR(255) NOT NULL,
    activation_token VARCHAR(64),
    token_expires_at DATETIME,
    activated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_check DATETIME,
    status ENUM('active', 'deactivated') DEFAULT 'active',
    metadata JSON,
    UNIQUE KEY unique_license_site (license_id, site_url),
    FOREIGN KEY (license_id) REFERENCES licenses(id)
);

Deactivation Flow

Client: [Disconnect] button clicked
  → POST /api/v1/license/deactivate
  → Body: { license_key, site_url }
  → Server removes activation record
  → Client clears stored license
  → Show "Disconnected" status

Periodic Validation

// Cron check every 24 hours
add_action('woonoow_daily_license_check', function() {
    $license = get_option('woonoow_license');
    if (!$license) return;
    
    $response = wp_remote_post('https://licensing.woonoow.com/api/v1/license/validate', [
        'body' => [
            'license_key' => $license['key'],
            'site_url' => home_url(),
        ],
    ]);
    
    $data = json_decode(wp_remote_retrieve_body($response), true);
    
    if ($data['status'] !== 'active') {
        update_option('woonoow_license', ['status' => 'invalid']);
        // Optionally disable premium features
    }
});

API Endpoints (Vendor Server)

Endpoint Method Purpose
/connect GET OAuth-like authorization page
/api/v1/token/exchange POST Exchange activation token for license
/api/v1/license/validate POST Validate license status
/api/v1/license/deactivate POST Remove site activation
/api/v1/license/info GET Get license details

Comparison: Your Flow vs. Perfected

Aspect Your Original Perfected
CSRF Protection None State token
Replay Prevention None Nonce + timestamp
Token Exchange Direct return Secure exchange
Return URL Security Unvalidated Server whitelist
Deactivation Not mentioned Full flow
Periodic Validation Not mentioned Daily cron
Fallback None Manual key entry

Implementation Phases

Phase 1: Server-Side (Licensing Portal)

  1. Create /connect authorization page
  2. Build license selection UI
  3. Implement activation recording
  4. Create token exchange API

Phase 2: Client-Side (WooNooW Plugin)

  1. Create Settings → License admin page
  2. Implement connect redirect
  3. Handle callback and token exchange
  4. Store license securely
  5. Add disconnect functionality

Phase 3: Validation & Updates

  1. Implement periodic license checks
  2. Gate premium features behind valid license
  3. Integrate with plugin update checker

References

Source Relevance
Elementor Pro Activation Primary reference for UX flow
Tutor LMS / Themeum Hybrid key+account approach
OAuth 2.0 Authorization Code Flow Security pattern basis
EDD Software Licensing Activation limits pattern
OWASP API Security State/nonce implementation