false, 'auth-ui.features.two_factor_required' => false, ]); }); it('returns the same password-reset response for existing and unknown accounts', function () { Notification::fake(); $user = User::factory()->create(['email' => 'known@example.com']); $expectedMessage = 'If an account exists for that email address, a password reset link has been sent.'; $this->post('/forgot-password', ['email' => 'known@example.com']) ->assertSessionHas('status', $expectedMessage) ->assertSessionDoesntHaveErrors(); $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.2']) ->post('/forgot-password', ['email' => 'unknown@example.com']) ->assertSessionHas('status', $expectedMessage) ->assertSessionDoesntHaveErrors(); Notification::assertSentTo($user, QueuedResetPasswordNotification::class); }); it('invalidates existing sessions and notifies the user after password reset', function () { Notification::fake(); config(['session.driver' => 'database']); $user = User::factory()->create(['email' => 'reset@example.com']); DB::table('sessions')->insert([ [ 'id' => 'existing-session-one', 'user_id' => $user->id, 'ip_address' => '192.0.2.10', 'user_agent' => 'Test', 'payload' => 'payload', 'last_activity' => now()->timestamp, ], [ 'id' => 'existing-session-two', 'user_id' => $user->id, 'ip_address' => '192.0.2.11', 'user_agent' => 'Test', 'payload' => 'payload', 'last_activity' => now()->timestamp, ], ]); $token = Password::createToken($user); $this->post('/reset-password', [ 'token' => $token, 'email' => $user->email, 'password' => 'a completely new secure password', 'password_confirmation' => 'a completely new secure password', ])->assertRedirect('/login'); $user->refresh(); expect(DB::table('sessions')->where('user_id', $user->id)->count())->toBe(0) ->and(Hash::check('a completely new secure password', $user->password))->toBeTrue() ->and($user->auth_session_version)->toBe(1); Notification::assertSentTo($user, PasswordChangedNotification::class); }); it('stores new passwords with Argon2id', function () { $this->post('/register', [ 'username' => 'argon-user', 'first_name' => 'Argon', 'last_name' => 'User', 'email' => 'argon@example.com', 'password' => 'correct horse battery staple', 'password_confirmation' => 'correct horse battery staple', ])->assertRedirect('/dashboard'); $user = User::where('email', 'argon@example.com')->firstOrFail(); expect(password_get_info($user->password)['algoName'])->toBe('argon2id'); }); it('rejects new passwords shorter than fifteen characters', function () { $this->post('/register', [ 'username' => 'short-password', 'first_name' => 'Short', 'last_name' => 'Password', 'email' => 'short@example.com', 'password' => 'too-short', 'password_confirmation' => 'too-short', ])->assertSessionHasErrors('password'); $this->assertDatabaseMissing('users', ['email' => 'short@example.com']); }); it('rejects password hashes created with an unexpected algorithm', function () { $user = User::factory()->create(['email' => 'unexpected-hash@example.com']); DB::table('users')->where('id', $user->id)->update([ 'password' => password_hash('correct-password', PASSWORD_BCRYPT, ['cost' => 4]), ]); $this->post('/login', [ 'login' => 'unexpected-hash@example.com', 'password' => 'correct-password', ])->assertSessionHasErrors('login'); $this->assertGuest(); expect(password_get_info($user->fresh()->password)['algoName'])->toBe('bcrypt'); }); it('enforces account-aware login throttling across different IP addresses', function () { User::factory()->create([ 'email' => 'limited@example.com', 'password' => 'correct-password', ]); foreach (range(1, 10) as $attempt) { $this->withServerVariables(['REMOTE_ADDR' => "192.0.2.{$attempt}"]) ->post('/login', [ 'login' => 'limited@example.com', 'password' => 'wrong-password', ]) ->assertSessionHasErrors('login'); } $this->withServerVariables(['REMOTE_ADDR' => '192.0.2.11']) ->post('/login', [ 'login' => 'limited@example.com', 'password' => 'wrong-password', ]) ->assertTooManyRequests(); }); it('adds browser security headers and prevents caching secret-bearing pages', function () { $this->get('/') ->assertHeader('X-Content-Type-Options', 'nosniff') ->assertHeader('X-Frame-Options', 'DENY') ->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin') ->assertHeader('Permissions-Policy'); $response = $this->get('/login') ->assertHeader('Cache-Control') ->assertHeader('Pragma', 'no-cache'); expect($response->headers->get('Cache-Control'))->toContain('no-store'); }); it('adds CSP and HSTS to secure production responses', function () { $previousEnvironment = $this->app->environment(); $this->app->instance('env', 'production'); try { $this->withServerVariables([ 'HTTPS' => 'on', 'REQUEST_SCHEME' => 'https', 'SERVER_PORT' => 443, ]) ->get('https://localhost/') ->assertHeader('Content-Security-Policy') ->assertHeader( 'Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload' ); } finally { $this->app->instance('env', $previousEnvironment); } }); it('ends an authenticated session at its absolute expiry', function () { $user = User::factory()->create(); $this->actingAs($user) ->withSession(['auth.absolute_expires_at' => now()->subSecond()->timestamp]) ->get('/dashboard') ->assertRedirect('/login') ->assertSessionHas('error', 'Your session has expired. Please sign in again.'); $this->assertGuest(); }); it('records the session version and absolute expiry after login', function () { $user = User::factory()->create([ 'email' => 'session-version@example.com', 'password' => 'correct horse battery staple', 'auth_session_version' => 3, ]); $response = $this->post('/login', [ 'login' => $user->email, 'password' => 'correct horse battery staple', ])->assertRedirect('/dashboard') ->assertSessionHas('auth.session_version', 3); expect($response->getSession()->get('auth.absolute_expires_at')) ->toBeGreaterThan(now()->timestamp); }); it('revokes an authenticated session when its user session version is stale', function () { $user = User::factory()->create(['auth_session_version' => 2]); $this->actingAs($user) ->withSession([ 'auth.session_version' => 1, 'auth.absolute_expires_at' => now()->addHour()->timestamp, ]) ->get('/dashboard') ->assertRedirect('/login') ->assertSessionHas( 'error', 'Your session is no longer valid. Please sign in again.' ); $this->assertGuest(); });