124 lines
4.4 KiB
PHP
124 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Testing\TestResponse;
|
|
use Laravel\Passport\Client;
|
|
use Tests\TestCase;
|
|
|
|
class ClientSecretRotationTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private User $owner;
|
|
private Client $client;
|
|
private string $originalSecret;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->owner = User::factory()->approved()->create();
|
|
|
|
// Se crea vía el endpoint real para tener un secret en texto plano
|
|
// válido y un cliente client_credentials real, igual que en producción.
|
|
$response = $this->actingAs($this->owner)->postJson('/web-api/clients', [
|
|
'name' => 'Rotation Test Client',
|
|
'grant_type' => 'client_credentials',
|
|
]);
|
|
|
|
$response->assertCreated();
|
|
$this->originalSecret = $response->json('plain_secret');
|
|
$this->client = Client::find($response->json('client.id'));
|
|
}
|
|
|
|
public function test_original_secret_works_before_rotation(): void
|
|
{
|
|
$this->requestToken($this->originalSecret)->assertOk()->assertJsonStructure(['access_token']);
|
|
}
|
|
|
|
public function test_rotating_returns_a_new_secret_and_grace_period(): void
|
|
{
|
|
$response = $this->actingAs($this->owner)->postJson("/web-api/clients/{$this->client->id}/rotate-secret");
|
|
|
|
$response->assertOk()->assertJsonStructure(['plain_secret', 'previous_secret_expires_at']);
|
|
$this->assertNotEmpty($response->json('plain_secret'));
|
|
$this->assertNotSame($this->originalSecret, $response->json('plain_secret'));
|
|
|
|
$this->assertDatabaseHas('audit_logs', [
|
|
'action' => 'client.secret_rotated',
|
|
'subject_id' => $this->client->id,
|
|
]);
|
|
}
|
|
|
|
public function test_both_old_and_new_secret_work_during_the_grace_period(): void
|
|
{
|
|
$response = $this->actingAs($this->owner)->postJson("/web-api/clients/{$this->client->id}/rotate-secret");
|
|
$newSecret = $response->json('plain_secret');
|
|
|
|
$this->requestToken($newSecret)->assertOk();
|
|
$this->requestToken($this->originalSecret)->assertOk();
|
|
}
|
|
|
|
public function test_old_secret_stops_working_after_the_grace_period_expires(): void
|
|
{
|
|
$this->actingAs($this->owner)->postJson("/web-api/clients/{$this->client->id}/rotate-secret");
|
|
|
|
$this->client->forceFill(['previous_secret_expires_at' => now()->subMinute()])->save();
|
|
|
|
$this->requestToken($this->originalSecret)->assertStatus(401);
|
|
}
|
|
|
|
public function test_rotating_again_invalidates_the_first_old_secret_immediately(): void
|
|
{
|
|
// Solo se guarda UN secret anterior — rotar de nuevo pisa el que
|
|
// había, no acumula un historial de secrets viejos válidos.
|
|
$firstResponse = $this->actingAs($this->owner)->postJson("/web-api/clients/{$this->client->id}/rotate-secret");
|
|
$secondSecret = $firstResponse->json('plain_secret');
|
|
|
|
$this->actingAs($this->owner)->postJson("/web-api/clients/{$this->client->id}/rotate-secret");
|
|
|
|
$this->requestToken($this->originalSecret)->assertStatus(401);
|
|
$this->requestToken($secondSecret)->assertOk();
|
|
}
|
|
|
|
public function test_non_owner_cannot_rotate_another_users_client_secret(): void
|
|
{
|
|
$stranger = User::factory()->approved()->create();
|
|
|
|
$this->actingAs($stranger)
|
|
->postJson("/web-api/clients/{$this->client->id}/rotate-secret")
|
|
->assertNotFound();
|
|
|
|
$this->requestToken($this->originalSecret)->assertOk();
|
|
}
|
|
|
|
public function test_cannot_rotate_a_public_clients_secret(): void
|
|
{
|
|
$response = $this->actingAs($this->owner)->postJson('/web-api/clients', [
|
|
'name' => 'Public Client',
|
|
'grant_type' => 'authorization_code',
|
|
'redirect_uris' => ['https://example.test/callback'],
|
|
'confidential' => false,
|
|
]);
|
|
$response->assertCreated();
|
|
$publicClient = Client::find($response->json('client.id'));
|
|
|
|
$this->actingAs($this->owner)
|
|
->postJson("/web-api/clients/{$publicClient->id}/rotate-secret")
|
|
->assertStatus(422);
|
|
}
|
|
|
|
private function requestToken(string $secret): TestResponse
|
|
{
|
|
return $this->postJson('/oauth/token', [
|
|
'grant_type' => 'client_credentials',
|
|
'client_id' => $this->client->id,
|
|
'client_secret' => $secret,
|
|
'scope' => 'users:read',
|
|
]);
|
|
}
|
|
}
|