Skip to content

Commit

Permalink
Add webhook verification (#99)
Browse files Browse the repository at this point in the history
* Separate Webhook handling from Subscriptions

* Add ability to verify webhooks
  • Loading branch information
thinkverse authored Jul 28, 2023
1 parent 7069ba5 commit 6f9397d
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 32 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ JWT_SECRET=Jrsweag3Mf0srOqDizRkhjWm5CEFcrBy
PADDLE_VENDOR_ID=
PADDLE_VENDOR_AUTH_CODE=
PADDLE_ENV=sandbox
PADDLE_PUBLIC_KEY=

WAVE_DOCS=true
WAVE_DEMO=false
Expand Down
3 changes: 2 additions & 1 deletion config/wave.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
'paddle' => [
'vendor' => env('PADDLE_VENDOR_ID', ''),
'auth_code' => env('PADDLE_VENDOR_AUTH_CODE', ''),
'env' => env('PADDLE_ENV', 'sandbox')
'env' => env('PADDLE_ENV', 'sandbox'),
'public_key' => env('PADDLE_PUBLIC_KEY', ''),
]

];
2 changes: 1 addition & 1 deletion wave/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
Route::view('pricing', 'theme::pricing')->name('wave.pricing');

/***** Billing Routes *****/
Route::post('paddle/webhook', '\Wave\Http\Controllers\SubscriptionController@webhook');
Route::post('paddle/webhook', '\Wave\Http\Controllers\WebhookController');
Route::post('checkout', '\Wave\Http\Controllers\SubscriptionController@checkout')->name('checkout');

Route::get('test', '\Wave\Http\Controllers\SubscriptionController@test');
Expand Down
30 changes: 0 additions & 30 deletions wave/src/Http/Controllers/SubscriptionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,36 +31,6 @@ public function __construct(){
$this->paddle_vendors_url = (config('wave.paddle.env') == 'sandbox') ? 'https://sandbox-vendors.paddle.com/api' : 'https://vendors.paddle.com/api';
}


public function webhook(Request $request){

// Which alert/event is this request for?
$alert_name = $request->alert_name;
$subscription_id = $request->subscription_id;
$status = $request->status;


// Respond appropriately to this request.
switch($alert_name) {

case 'subscription_created':
break;
case 'subscription_updated':
break;
case 'subscription_cancelled':
$this->cancelSubscription($subscription_id);
return response()->json(['status' => 1]);
break;
case 'subscription_payment_succeeded':
break;
case 'subscription_payment_failed':
$this->cancelSubscription($subscription_id);
return response()->json(['status' => 1]);
break;
}

}

public function cancel(Request $request){
$this->cancelSubscription($request->id);
return response()->json(['status' => 1]);
Expand Down
51 changes: 51 additions & 0 deletions wave/src/Http/Controllers/WebhookController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Wave\Http\Controllers;

use Illuminate\Http\Request;
use TCG\Voyager\Models\Role;
use Wave\PaddleSubscription;
use Illuminate\Support\Carbon;
use App\Http\Controllers\Controller;
use Wave\Http\Middleware\VerifyWebhook;

class WebhookController extends Controller
{
public function __construct()
{
if (config('wave.paddle.public_key')) {
$this->middleware(VerifyWebhook::class);
}
}

public function __invoke(Request $request)
{
$method = match ($request->get('alert_name', null)) {
'subscription_cancelled',
'subscription_payment_failed' => 'subscriptionCancelled',
default => null,
};

if (method_exists($this, $method)) {
try {
$this->{$method}($request);
} catch (\Exception $e) {
return response('Webhook failed');
}
}

return response('Webhook handled');
}

protected function subscriptionCancelled(Request $request)
{
$subscription = PaddleSubscription::where('subscription_id', $request->subscription_id)->firstOrFail();
$subscription->cancelled_at = Carbon::now();
$subscription->status = 'cancelled';
$subscription->save();
$user = config('wave.user_model')::find($subscription->user_id);
$cancelledRole = Role::where('name', '=', 'cancelled')->first();
$user->role_id = $cancelledRole->id;
$user->save();
}
}
43 changes: 43 additions & 0 deletions wave/src/Http/Middleware/VerifyWebhook.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Wave\Http\Middleware;

use Closure;
use InvalidArgumentException;

class VerifyWebhook
{
/**
* Handle an incoming webhook request.
*
* @see https://developer.paddle.com/webhook-reference/ZG9jOjI1MzUzOTg2-verifying-webhooks
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$signature = $request->get('p_signature');
$fields = $request->except('p_signature');

ksort($fields);

foreach ($fields as $k => $v) {
if (!in_array(gettype($v), array('object', 'array'))) {
$fields[$k] = "$v";
}
}

if (openssl_verify(
serialize($fields),
base64_decode($signature),
openssl_get_publickey(config('wave.paddle.public_key')),
OPENSSL_ALGO_SHA1
) !== 1) {
throw new InvalidArgumentException('Webhook signature is invalid.');
}

return $next($request);
}
}

0 comments on commit 6f9397d

Please sign in to comment.