mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-22 11:33:24 +00:00
df44c46a12
The page rendered the form without looking at the token, so somebody opening a link an hour late typed a password, typed it again to confirm, and was then told "this password reset token is invalid" — a word nobody outside the code knows, at the end rather than the start. Links last an hour and people open them late. That is ordinary, not an error to be scolded for. store() still validates and is still the rule; there is a test that a spent token is refused there whatever the page drew. This is only the screen being honest a minute earlier. An address that is missing, or belongs to nobody, is drawn as the form was before. Partly because an unanswerable question is not an expired link, but mostly because a page that said "expired" for a real address and something else for an unknown one would answer whether an account exists here to anybody typing guesses — the exact property /forgot-password protects by saying "a link will be sent if the account exists". Two tests pin that. Worth having now rather than later: the advisories publishing with this release will send more people than usual through this screen, in a hurry and some of them frightened. Found by the portal session's user, who opened a real link an hour and forty minutes after it was sent.
84 lines
3.1 KiB
PHP
84 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Password;
|
|
use Inertia\Testing\AssertableInertia;
|
|
|
|
/**
|
|
* An expired reset link should say so before asking for the work, not
|
|
* after it.
|
|
*
|
|
* The scaffolding renders the form without looking at the token, so
|
|
* somebody opening a link an hour late typed a password, typed it again to
|
|
* confirm, and was then told "this password reset token is invalid" — a
|
|
* word nobody outside the code knows, at the end rather than the start.
|
|
*
|
|
* store() still validates and is still the rule. This is only the screen
|
|
* being honest a minute earlier.
|
|
*/
|
|
beforeEach(function () {
|
|
$this->user = User::factory()->create(['email' => 'owner@example.com']);
|
|
});
|
|
|
|
test('a live link still shows the form', function () {
|
|
$token = Password::broker()->createToken($this->user);
|
|
|
|
$this->get("/reset-password/{$token}?email=owner@example.com")
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page
|
|
->component('auth/reset-password')
|
|
->where('expired', false));
|
|
});
|
|
|
|
test('a spent link says so instead of asking for a password', function () {
|
|
$this->get('/reset-password/not-a-real-token?email=owner@example.com')
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page->where('expired', true));
|
|
});
|
|
|
|
test('a link whose token has been used is spent', function () {
|
|
$token = Password::broker()->createToken($this->user);
|
|
Password::broker()->deleteToken($this->user);
|
|
|
|
$this->get("/reset-password/{$token}?email=owner@example.com")
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page->where('expired', true));
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| It must not answer whether an account exists
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| /forgot-password deliberately says "a link will be sent if the account
|
|
| exists". A screen that said "expired" for a real address and something
|
|
| else for an unknown one would give that away to anybody typing guesses.
|
|
*/
|
|
|
|
test('an address nobody has is not called expired', function () {
|
|
$this->get('/reset-password/not-a-real-token?email=nobody@example.com')
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page->where('expired', false));
|
|
});
|
|
|
|
test('a missing address is not called expired either', function () {
|
|
$this->get('/reset-password/not-a-real-token')
|
|
->assertOk()
|
|
->assertInertia(fn (AssertableInertia $page) => $page->where('expired', false));
|
|
});
|
|
|
|
test('the real check still happens on the write', function () {
|
|
// The screen is a courtesy; store() is the rule. A spent token is
|
|
// refused there whatever the page decided to draw.
|
|
$this->post('/reset-password', [
|
|
'token' => 'not-a-real-token',
|
|
'email' => 'owner@example.com',
|
|
'password' => 'a-brand-new-password',
|
|
'password_confirmation' => 'a-brand-new-password',
|
|
])->assertSessionHasErrors('email');
|
|
|
|
expect(Hash::check('a-brand-new-password', $this->user->fresh()->password))->toBeFalse();
|
|
});
|