116 lines
3.9 KiB
PHP
116 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Auth;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use PragmaRX\Google2FA\Google2FA;
|
|
use Tests\TestCase;
|
|
|
|
class PasswordGrantTwoFactorTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private string $clientId;
|
|
|
|
private string $clientSecret;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$owner = User::factory()->approved()->create();
|
|
$response = $this->actingAs($owner)->postJson('/web-api/clients', [
|
|
'name' => 'Password Grant Test Client',
|
|
'grant_type' => 'password',
|
|
]);
|
|
$response->assertCreated();
|
|
|
|
$this->clientId = $response->json('client.id');
|
|
$this->clientSecret = $response->json('plain_secret');
|
|
}
|
|
|
|
public function test_password_grant_still_works_for_users_without_two_factor(): void
|
|
{
|
|
User::factory()->approved()->create([
|
|
'email' => 'no2fa@example.com',
|
|
'password' => Hash::make('password123'),
|
|
]);
|
|
|
|
$this->requestToken('no2fa@example.com', 'password123')
|
|
->assertOk()->assertJsonStructure(['access_token']);
|
|
}
|
|
|
|
public function test_password_grant_is_rejected_without_otp_when_two_factor_is_enabled(): void
|
|
{
|
|
User::factory()->approved()->create([
|
|
'email' => 'has2fa@example.com',
|
|
'password' => Hash::make('password123'),
|
|
'two_factor_secret' => (new Google2FA())->generateSecretKey(),
|
|
'two_factor_confirmed_at' => now(),
|
|
]);
|
|
|
|
$this->requestToken('has2fa@example.com', 'password123')->assertStatus(400);
|
|
}
|
|
|
|
public function test_password_grant_is_rejected_with_wrong_otp(): void
|
|
{
|
|
User::factory()->approved()->create([
|
|
'email' => 'has2fa@example.com',
|
|
'password' => Hash::make('password123'),
|
|
'two_factor_secret' => (new Google2FA())->generateSecretKey(),
|
|
'two_factor_confirmed_at' => now(),
|
|
]);
|
|
|
|
$this->requestToken('has2fa@example.com', 'password123', '000000')->assertStatus(400);
|
|
}
|
|
|
|
public function test_password_grant_succeeds_with_valid_totp_code(): void
|
|
{
|
|
$secret = (new Google2FA())->generateSecretKey();
|
|
User::factory()->approved()->create([
|
|
'email' => 'has2fa@example.com',
|
|
'password' => Hash::make('password123'),
|
|
'two_factor_secret' => $secret,
|
|
'two_factor_confirmed_at' => now(),
|
|
]);
|
|
|
|
$otp = (new Google2FA())->getCurrentOtp($secret);
|
|
|
|
$this->requestToken('has2fa@example.com', 'password123', $otp)
|
|
->assertOk()->assertJsonStructure(['access_token']);
|
|
}
|
|
|
|
public function test_password_grant_succeeds_with_a_recovery_code_and_consumes_it(): void
|
|
{
|
|
$user = User::factory()->approved()->create([
|
|
'email' => 'has2fa@example.com',
|
|
'password' => Hash::make('password123'),
|
|
'two_factor_secret' => (new Google2FA())->generateSecretKey(),
|
|
'two_factor_recovery_codes' => ['AAAA-1111', 'BBBB-2222'],
|
|
'two_factor_confirmed_at' => now(),
|
|
]);
|
|
|
|
$this->requestToken('has2fa@example.com', 'password123', 'AAAA-1111')->assertOk();
|
|
|
|
$this->assertNotContains('AAAA-1111', $user->fresh()->two_factor_recovery_codes);
|
|
|
|
// El mismo código de recuperación no sirve una segunda vez.
|
|
$this->requestToken('has2fa@example.com', 'password123', 'AAAA-1111')->assertStatus(400);
|
|
}
|
|
|
|
private function requestToken(string $email, string $password, ?string $otp = null)
|
|
{
|
|
return $this->postJson('/oauth/token', array_filter([
|
|
'grant_type' => 'password',
|
|
'client_id' => $this->clientId,
|
|
'client_secret' => $this->clientSecret,
|
|
'username' => $email,
|
|
'password' => $password,
|
|
'otp' => $otp,
|
|
'scope' => 'users:read',
|
|
], fn ($v) => $v !== null));
|
|
}
|
|
}
|