Until now, an account switched off or a token revoked at the identity server kept working here: bearer tokens until the introspection cache ran out, panel sessions until they ended, and machine tokens for good, as they only look at the local account. POST /api/v1/webhooks/pireon takes user.deactivated (the account goes inactive, which stops its machine tokens and the panel; its sessions end and its cached tokens are forgotten) and token.revoked (sessions and cached tokens go). Pireon signs each delivery with the secret of the client that registered the webhook, so either PIREON_WEBHOOK_SECRET (password client) or PIREON_REDIRECT_WEBHOOK_SECRET («Entrar con Pireon») verifies it; with neither, the endpoint refuses everything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
3.9 KiB
PHP
111 lines
3.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Auth;
|
|
|
|
use Database\Seeders\RolePermissionSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Testing\TestResponse;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\FakesPireon;
|
|
use Tests\TestCase;
|
|
|
|
class PireonWebhookTest extends TestCase
|
|
{
|
|
use FakesPireon, RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->seed(RolePermissionSeeder::class);
|
|
config(['pireon.webhook_secrets' => ['password-secret', 'redirect-secret']]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
*/
|
|
protected function deliver(array $payload, ?string $secret = 'password-secret'): TestResponse
|
|
{
|
|
$body = (string) json_encode($payload);
|
|
$headers = ['CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'];
|
|
|
|
if ($secret !== null) {
|
|
$headers['HTTP_X_WEBHOOK_SIGNATURE'] = hash_hmac('sha256', $body, $secret);
|
|
}
|
|
|
|
return $this->call('POST', route('api.v1.webhooks.pireon'), [], [], [], $headers, $body);
|
|
}
|
|
|
|
#[Test]
|
|
public function it_refuses_everything_without_a_secret(): void
|
|
{
|
|
config(['pireon.webhook_secrets' => []]);
|
|
|
|
$this->deliver(['event' => 'token.revoked', 'data' => ['user_id' => 1]])->assertUnauthorized();
|
|
}
|
|
|
|
#[Test]
|
|
public function it_refuses_a_delivery_that_is_not_signed_with_either_secret(): void
|
|
{
|
|
$user = $this->userWithRoles('editor', ['oauth_id' => 77]);
|
|
|
|
$this->deliver(['event' => 'user.deactivated', 'data' => ['user_id' => 77]], 'someone-else')->assertUnauthorized();
|
|
$this->deliver(['event' => 'user.deactivated', 'data' => ['user_id' => 77]], null)->assertUnauthorized();
|
|
|
|
$this->assertTrue($user->fresh()->is_active);
|
|
}
|
|
|
|
#[Test]
|
|
public function a_deactivated_account_goes_inactive_and_loses_its_sessions(): void
|
|
{
|
|
$user = $this->userWithRoles('editor', ['oauth_id' => 77]);
|
|
config(['session.driver' => 'database']);
|
|
DB::table('sessions')->insert([
|
|
'id' => 'a-session', 'user_id' => $user->id, 'payload' => '', 'last_activity' => time(),
|
|
]);
|
|
|
|
// Signed by the redirect client: either secret is good.
|
|
$this->deliver(['event' => 'user.deactivated', 'data' => ['user_id' => 77]], 'redirect-secret')
|
|
->assertNoContent();
|
|
|
|
$this->assertFalse($user->fresh()->is_active);
|
|
$this->assertDatabaseMissing('sessions', ['id' => 'a-session']);
|
|
}
|
|
|
|
#[Test]
|
|
public function a_revoked_token_stops_working_at_once_instead_of_after_the_cache(): void
|
|
{
|
|
$user = $this->userWithRoles('editor', ['oauth_id' => 77]);
|
|
$headers = $this->apiHeadersFor($user);
|
|
|
|
$this->getJson(route('api.v1.me'), $headers)->assertOk();
|
|
|
|
// Revoked upstream: the cached answer would still let it through…
|
|
$this->pireonIdentities = array_fill_keys(['nobody'], []);
|
|
$this->app['auth']->forgetGuards();
|
|
$this->getJson(route('api.v1.me'), $headers)->assertOk();
|
|
|
|
// …until Pireon says so.
|
|
$this->deliver(['event' => 'token.revoked', 'data' => ['user_id' => 77]])->assertNoContent();
|
|
$this->app['auth']->forgetGuards();
|
|
|
|
$this->getJson(route('api.v1.me'), $headers)->assertUnauthorized();
|
|
$this->assertTrue($user->fresh()->is_active);
|
|
}
|
|
|
|
#[Test]
|
|
public function other_events_and_unknown_people_change_nothing(): void
|
|
{
|
|
$user = $this->userWithRoles('editor', ['oauth_id' => 77]);
|
|
|
|
$this->deliver(['event' => 'user.updated', 'data' => ['user_id' => 77]])->assertNoContent();
|
|
$this->deliver(['event' => 'user.deactivated', 'data' => ['user_id' => 999]])->assertNoContent();
|
|
$this->deliver(['data' => ['user_id' => 77]])->assertStatus(400);
|
|
|
|
$this->assertTrue($user->fresh()->is_active);
|
|
}
|
|
}
|