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']; } }