# 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)

```php
// 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)

```php
// 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)

```sql
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

```php
// 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 |
