feat: harden admin auth and ticket workflows
- 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
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<?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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user