Laravel - session data survives log-out/log-in, even for different users

前端 未结 2 1062
情话喂你
情话喂你 2021-01-02 03:54

Today I noticed something disturbing while inspecting session files in storage/framework/sessions folder created by Laravel 5.

Here is what happened:

2条回答
  •  生来不讨喜
    2021-01-02 04:33

    I believe this is the correct answer to this question/problem:

    When making multiple requests in one test, the state of your laravel application is not reset between the requests. The Auth manager is a singleton in the laravel container, and it keeps a local cache of the resolved auth guards. The resolved auth guards keep a local cache of the authed user.

    So, your first request to your api/logout endpoint resolves the auth manager, which resolves the api guard, which stores a references to the authed user whose token you will be revoking.

    Now, when you make your second request to /api/user, the already resolved auth manager is pulled from the container, the already resolved api guard is pulled from it's local cache, and the same already resolved user is pulled from the guard's local cache. This is why the second request passes authentication instead of failing it.

    When testing auth related stuff with multiple requests in the same test, you need to reset the resolved instances between tests. Also, you can't just unset the resolved auth manager instance, because when it is resolved again, it won't have the extended passport driver defined.

    So, the easiest way I've found is to use reflection to unset the protected guards property on the resolved auth manager. You also need to call the logout method on the resolved session guards.

    Source: Method Illuminate\Auth\RequestGuard::logout does not exist Laravel Passport

    To use that, add this to:

    TestCase.php

    protected function resetAuth(array $guards = null) : void
    {
        $guards = $guards ?: array_keys(config('auth.guards'));
    
        foreach ($guards as $guard) {
            $guard = $this->app['auth']->guard($guard);
    
            if ($guard instanceof SessionGuard) {
                $guard->logout();
            }
        }
    
        $protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
        $protectedProperty->setAccessible(true);
        $protectedProperty->setValue($this->app['auth'], []);
    }
    

    Then, use it like this:

    LoginTest.php

    class LoginTest extends TestCase
    {
        use DatabaseTransactions, ThrottlesLogins;
    
        protected $auth_guard = 'web';
    
        /** @test */
        public function it_can_login()
        {
            $user = $this->user();
    
            $this->postJson(route('login'), ['email' => $user->email, 'password' => TestCase::AUTH_PASSWORD])
                ->assertStatus(200)
                ->assertJsonStructure([
                    'user' => [
                        'id' ,
                        'status',
                        'name',
                        'email',
                        'email_verified_at',
                        'created_at',
                        'updated_at',
                        'photo_url',
                        'roles_list',
                        'roles',
                    ],
                ]);
    
            $this->assertEquals(Auth::check(), true);
            $this->assertEquals(Auth::user()->email, $user->email);
            $this->assertAuthenticated($this->auth_guard);
            $this->assertAuthenticatedAs($user, $this->auth_guard);
    
            $this->resetAuth();
        }
    
        /** @test */
        public function it_can_logout()
        {
            $this->actingAs($this->user())
                ->postJson(route('logout'))
                ->assertStatus(204);
    
            $this->assertGuest($this->auth_guard);
    
            $this->resetAuth();
        }
    
        /** @test */
        public function it_should_get_two_cookies_upon_login_without_remember_me()
        {
            $user = $this->user();
    
            $response = $this->postJson(route('login'), [
                'email' => $user->email,
                'password' => TestCase::AUTH_PASSWORD,
            ]);
    
            $response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
            $response->assertCookieNotExpired('XSRF-TOKEN');
            $this->assertEquals(config('session.http_only'), true);
    
            $this->resetAuth();
        }
    
        /** @test */
        public function it_should_get_three_cookies_upon_login_with_remember_me()
        {
            $user = $this->user();
    
            $response = $this->postJson(route('login'), [
                'email' => $user->email,
                'password' => TestCase::AUTH_PASSWORD,
                'remember' => true,
            ]);
    
            $response->assertCookieNotExpired(Str::slug(config('app.name'), '_').'_session');
            $response->assertCookieNotExpired('XSRF-TOKEN');
            $response->assertCookieNotExpired(Auth::getRecallerName());
    
            $this->resetAuth();
        }
    
        /** @test */
        public function it_should_throw_error_422_on_login_attempt_without_email()
        {
            $this->postJson(route('login'), ['email' => '', 'password' => TestCase::AUTH_PASSWORD])
                ->assertStatus(422)
                ->assertJsonStructure(['message', 'errors' => ['email']]);
    
            $this->assertGuest($this->auth_guard);
    
            $this->resetAuth();
        }
    
        /** @test */
        public function it_should_throw_error_422_on_login_attempt_without_password()
        {
            $this->postJson(route('login'), ['email' => $this->adminUser()->email, 'password' => ''])
                ->assertStatus(422)
                ->assertJsonStructure(['message', 'errors' => ['password']]);
    
            $this->assertGuest($this->auth_guard);
    
            $this->resetAuth();
        }
    
        /** @test */
        public function it_should_throw_error_422_on_login_attempt_with_empty_form()
        {
            $this->postJson(route('login'), ['email' => '', 'password' => ''])
                ->assertStatus(422)
                ->assertJsonStructure(['message', 'errors' => ['email', 'password']]);
    
            $this->assertGuest($this->auth_guard);
    
            $this->resetAuth();
        }
    
        /** @test */
        public function it_should_throw_error_401_as_guest_on_protected_routes()
        {
            $this->assertGuest($this->auth_guard);
    
            $this->getJson(route('me'))
                ->assertStatus(401)
                ->assertJson(['message' => 'Unauthenticated.']);
        }
    
        /** @test */
        public function it_should_throw_error_429_when_login_attempt_is_throttled()
        {
            $this->resetAuth();
    
            $throttledUser = factory(User::class, 1)->create()->first();
    
            foreach (range(0, 9) as $attempt) {
                $this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => "{TestCase::AUTH_PASSWORD}_{$attempt}"]);
            }
    
            $this->postJson(route('login'), ['email' => $throttledUser->email, 'password' => TestCase::AUTH_PASSWORD . 'k'])
                ->assertStatus(429)
                ->assertJson(['message' => 'Too Many Attempts.']);
    
            $this->resetAuth();
        }
    
    }
    

    A note about the throttle one. It took me several days to figure out how to ensure that 429 behaviour. Earlier unit tests will increase the number of 'attempts' leading up to the throttling, so you need to resetAuth before the throttle test or the throttle will be triggered at the wrong time and screw the test.

    Given the above unit test code, I am using this:

    Route::group(['middleware' => ['guest', 'throttle:10,5']], function () { /**/ });
    

    You can observe it working by changing any of those numbers, like 10,5 to 9,5 or 11,5 and watch how it affects the throttle unit test. You can also uncomment the resetAuth method and watch how it also screws the test.

    For unit testing anything related to auth, the resetAuth utility method is extremely useful, must-have. Also the knowledge of auth caching in AuthManager is a must-know to make sense of observed behaviour.

提交回复
热议问题