- add rotating admin refresh tokens and password gates\n- expand ticket galleries, exports, and client resources\n- improve IP allocation auditing and dashboard data\n- enforce encrypted client access tokens on public routes
154 lines
5.4 KiB
PHP
154 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Api\Admin;
|
|
|
|
use App\Models\AdminRefreshToken;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Tests\TestCase;
|
|
use Tymon\JWTAuth\Facades\JWTAuth;
|
|
|
|
class RememberLoginTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private const REFRESH_COOKIE = 'admin_refresh_token';
|
|
|
|
private User $admin;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->admin = User::factory()->create([
|
|
'username' => 'remember-admin',
|
|
'password' => Hash::make('Password123!'),
|
|
]);
|
|
}
|
|
|
|
public function test_login_only_returns_refresh_token_when_remember_is_enabled(): void
|
|
{
|
|
$credentials = [
|
|
'username' => $this->admin->username,
|
|
'password' => 'Password123!',
|
|
];
|
|
|
|
$this->postJson('/api/v1/administrator/login', $credentials)
|
|
->assertOk()
|
|
->assertJsonMissingPath('data.refresh_token')
|
|
->assertCookieExpired(self::REFRESH_COOKIE);
|
|
|
|
$response = $this->postJson('/api/v1/administrator/login', [...$credentials, 'remember' => true])
|
|
->assertOk()
|
|
->assertJsonPath('data.refresh_expires_in', 2592000)
|
|
->assertJsonMissingPath('data.refresh_token')
|
|
->assertCookie(self::REFRESH_COOKIE)
|
|
->assertCookieNotExpired(self::REFRESH_COOKIE);
|
|
|
|
$cookie = $response->getCookie(self::REFRESH_COOKIE, false);
|
|
|
|
$this->assertTrue($cookie->isHttpOnly());
|
|
$this->assertTrue($cookie->isSecure());
|
|
$this->assertSame('strict', $cookie->getSameSite());
|
|
$this->assertSame('/api/v1/administrator', $cookie->getPath());
|
|
$this->assertNotSame($response->json('data.token'), $cookie->getValue());
|
|
$this->assertNull(JWTAuth::setToken($response->json('data.token'))->getPayload()->get('remember_until'));
|
|
}
|
|
|
|
public function test_remembered_session_can_refresh_without_a_valid_access_token(): void
|
|
{
|
|
$login = $this->postJson('/api/v1/administrator/login', [
|
|
'username' => $this->admin->username,
|
|
'password' => 'Password123!',
|
|
'remember' => true,
|
|
])->assertOk();
|
|
|
|
$refreshToken = $login->getCookie(self::REFRESH_COOKIE, false)->getValue();
|
|
|
|
$this->withCredentials()
|
|
->withUnencryptedCookie(self::REFRESH_COOKIE, $refreshToken)
|
|
->postJson('/api/v1/administrator/refresh', [], $this->refreshHeaders())
|
|
->assertOk()
|
|
->assertJsonMissingPath('data.refresh_token')
|
|
->assertJsonStructure(['data' => ['token', 'expires_in']])
|
|
->assertCookie(self::REFRESH_COOKIE);
|
|
}
|
|
|
|
public function test_remembered_session_can_refresh_before_thirty_days(): void
|
|
{
|
|
$login = $this->postJson('/api/v1/administrator/login', [
|
|
'username' => $this->admin->username,
|
|
'password' => 'Password123!',
|
|
'remember' => true,
|
|
])->assertOk();
|
|
|
|
$this->travelTo(now()->addDays(29));
|
|
|
|
$this->withCredentials()
|
|
->withUnencryptedCookie(
|
|
self::REFRESH_COOKIE,
|
|
$login->getCookie(self::REFRESH_COOKIE, false)->getValue(),
|
|
)->postJson('/api/v1/administrator/refresh', [], $this->refreshHeaders())->assertOk();
|
|
}
|
|
|
|
public function test_remembered_session_cannot_refresh_after_thirty_days(): void
|
|
{
|
|
$login = $this->postJson('/api/v1/administrator/login', [
|
|
'username' => $this->admin->username,
|
|
'password' => 'Password123!',
|
|
'remember' => true,
|
|
])->assertOk();
|
|
|
|
$this->travelTo(now()->addDays(30)->addSecond());
|
|
|
|
$this->withCredentials()
|
|
->withUnencryptedCookie(
|
|
self::REFRESH_COOKIE,
|
|
$login->getCookie(self::REFRESH_COOKIE, false)->getValue(),
|
|
)->postJson('/api/v1/administrator/refresh', [], $this->refreshHeaders())
|
|
->assertUnauthorized()
|
|
->assertCookieExpired(self::REFRESH_COOKIE);
|
|
}
|
|
|
|
public function test_refresh_rejects_body_tokens_and_non_ajax_requests(): void
|
|
{
|
|
$this->postJson('/api/v1/administrator/refresh', [
|
|
'refresh_token' => 'exposed-token',
|
|
])->assertForbidden();
|
|
|
|
$this->postJson('/api/v1/administrator/refresh', [
|
|
'refresh_token' => 'exposed-token',
|
|
], $this->refreshHeaders())->assertUnprocessable();
|
|
}
|
|
|
|
public function test_logout_revokes_the_http_only_refresh_token(): void
|
|
{
|
|
$login = $this->postJson('/api/v1/administrator/login', [
|
|
'username' => $this->admin->username,
|
|
'password' => 'Password123!',
|
|
'remember' => true,
|
|
])->assertOk();
|
|
|
|
$refreshToken = $login->getCookie(self::REFRESH_COOKIE, false)->getValue();
|
|
|
|
$this->assertTrue(AdminRefreshToken::where('token', hash('sha256', $refreshToken))->exists());
|
|
|
|
$this->withCredentials()
|
|
->withToken($login->json('data.token'))
|
|
->withUnencryptedCookie(self::REFRESH_COOKIE, $refreshToken)
|
|
->postJson('/api/v1/administrator/logout')
|
|
->assertOk()
|
|
->assertCookieExpired(self::REFRESH_COOKIE);
|
|
|
|
$this->assertFalse(AdminRefreshToken::where('token', hash('sha256', $refreshToken))->exists());
|
|
}
|
|
|
|
private function refreshHeaders(): array
|
|
{
|
|
return ['X-Requested-With' => 'XMLHttpRequest'];
|
|
}
|
|
}
|