Files
oauth/tests/Feature/Auth/PasswordGrantTwoFactorTest.php
2026-07-27 17:20:02 +02:00

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));
}
}