Skip to content

Commit

Permalink
Add Middleware for verifying a Plain signature
Browse files Browse the repository at this point in the history
  • Loading branch information
claudiodekker committed Dec 11, 2024
1 parent 0b58b22 commit 95e1fe1
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 2 deletions.
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"providers": [
"LemonSqueezy\\PlainUiComponents\\PlainUiComponentsServiceProvider"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
13 changes: 13 additions & 0 deletions config/plain.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

return [

/**
* Your Plain workspace's global HMAC secret.
*
* This secret can be viewed and (re)generated by Plain workspace admins in Settings → Request signing.
* This will be used to verify that request were made by Plain, and not a third party.
*/
'secret' => env('PLAIN_SECRET'),

];
32 changes: 32 additions & 0 deletions src/PlainUiComponentsServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace LemonSqueezy\PlainUiComponents;

use Illuminate\Support\ServiceProvider;

class PlainUiComponentsServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../config/plain.php', 'plain');
}

/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/plain.php' => config_path('plain.php'),
]);
}
}
}
23 changes: 23 additions & 0 deletions src/VerifyPlainSignatureMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace LemonSqueezy\PlainUiComponents;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Response;

class VerifyPlainSignatureMiddleware
{
/**
* Handle the incoming request.
*/
public function handle(Request $request, Closure $next): Response
{
abort_unless($signature = $request->header('plain-request-signature'), 400, 'Missing webhook signature.');

Check failure on line 17 in src/VerifyPlainSignatureMiddleware.php

View workflow job for this annotation

GitHub Actions / larastan

Parameter #1 $boolean of function abort_unless expects bool, string|null given.
abort_unless($secret = Config::get('plain.secret'), 403, 'No webhook secret configured.');
abort_unless(hash_equals(hash_hmac('sha256', $request->getContent(), $secret), $signature), 403, 'Invalid signature.');

return $next($request);
}
}
11 changes: 9 additions & 2 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

namespace LemonSqueezy\PlainUiComponents\Tests;

class TestCase extends \PHPUnit\Framework\TestCase
use LemonSqueezy\PlainUiComponents\PlainUiComponentsServiceProvider;

class TestCase extends \Orchestra\Testbench\TestCase
{
//
protected function getPackageProviders($app): array
{
return [
PlainUiComponentsServiceProvider::class,
];
}
}
80 changes: 80 additions & 0 deletions tests/VerifyPlainSignatureMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace LemonSqueezy\PlainUiComponents\Tests;

use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Route;
use LemonSqueezy\PlainUiComponents\VerifyPlainSignatureMiddleware;

class VerifyPlainSignatureMiddlewareTest extends TestCase
{
/** @test */
public function it_blocks_the_request_when_the_plain_signature_header_is_not_provided(): void
{
Config::set('plain.secret', 'my-plain-secret');

$called = false;
Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
$called = true;
});

$response = $this->post('/', ['example' => 'content']);

$this->assertFalse($called);
$response->assertStatus(400);
}

/** @test */
public function it_blocks_the_request_when_the_middleware_is_applied_without_a_secret_being_configured(): void
{
Config::set('plain.secret', '');

$called = false;
Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
$called = true;
});

$response = $this->post('/', ['example' => 'content'], [
'plain-request-signature' => 'example-signature',
]);

$this->assertFalse($called);
$response->assertStatus(403);
}

/** @test */
public function it_blocks_the_request_when_the_plain_signature_header_does_not_match_the_configured_secret(): void
{
Config::set('plain.secret', 'my-plain-secret');

$called = false;
Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
$called = true;
});

$response = $this->post('/', ['example' => 'content'], [
'plain-request-signature' => 'example-signature',
]);

$this->assertFalse($called);
$response->assertStatus(403);
}

/** @test */
public function it_allows_the_request_when_the_plain_signature_header_matches_the_configured_secret(): void
{
Config::set('plain.secret', 'my-plain-secret');

$called = false;
Route::middleware(VerifyPlainSignatureMiddleware::class)->post('/', function () use (&$called) {
$called = true;
});

$response = $this->post('/', ['example' => 'content'], [
'plain-request-signature' => 'e85ab1c2f80714be422adfc9f446f9c48a018c971df12527fba9b2a2819cd17c',
]);

$this->assertTrue($called);
$response->assertStatus(200);
}
}

0 comments on commit 95e1fe1

Please sign in to comment.