Take Pireon's webhooks: a deactivation or a revocation counts at once
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>
This commit is contained in:
61
app/Http/Controllers/Api/V1/PireonWebhookController.php
Normal file
61
app/Http/Controllers/Api/V1/PireonWebhookController.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Actions\Auth\HandlePireonWebhook;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
/**
|
||||
* Receives `user.deactivated` and `token.revoked` from the Pireon identity
|
||||
* server, called by it and not by apps.
|
||||
*
|
||||
* `X-Webhook-Signature` is the HMAC-SHA256 (hex) of the raw body with the
|
||||
* secret of the Pireon client that registered the webhook. This application
|
||||
* has two clients (password and redirect), so a delivery is good if it
|
||||
* verifies with either secret. With none configured the endpoint is off: an
|
||||
* open one would let anyone switch accounts off.
|
||||
*/
|
||||
class PireonWebhookController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request, HandlePireonWebhook $handle): Response|JsonResponse
|
||||
{
|
||||
$secrets = (array) config('pireon.webhook_secrets', []);
|
||||
|
||||
if ($secrets === []) {
|
||||
return $this->refuse('Pireon webhooks are disabled (PIREON_WEBHOOK_SECRET is not set).');
|
||||
}
|
||||
|
||||
$signature = preg_replace('/^sha256=/', '', (string) $request->header('X-Webhook-Signature'));
|
||||
$body = $request->getContent();
|
||||
|
||||
$valid = $signature !== '' && collect($secrets)->contains(
|
||||
fn (string $secret): bool => hash_equals(hash_hmac('sha256', $body, $secret), $signature),
|
||||
);
|
||||
|
||||
if (! $valid) {
|
||||
return $this->refuse('Missing or invalid signature.');
|
||||
}
|
||||
|
||||
$event = (string) ($request->input('event') ?? $request->input('type') ?? '');
|
||||
|
||||
if ($event === '') {
|
||||
return response()->json(['message' => 'Missing event name.'], 400);
|
||||
}
|
||||
|
||||
$data = (array) ($request->input('data') ?? $request->input('payload') ?? $request->all());
|
||||
|
||||
$handle->handle($event, $data);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
protected function refuse(string $message): JsonResponse
|
||||
{
|
||||
return response()->json(['message' => $message], 401);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user