mirror of
https://github.com/flarum/framework.git
synced 2025-02-20 17:52:45 +08:00
feat: clear password & email tokens when appropriate (#3567)
* test: password tokens are generated and deleted on password change * chore: delete all password tokens when the password is changed * test: email tokens are generated and deleted on email change * test: email tokens are deleted after password reset * chore: delete email tokens after password change * test: password tokens are deleted after email change * chore: delete password tokens after email change * chore: syntactic sugar * chore: unify event listening
This commit is contained in:
parent
f96f914576
commit
2b31b185e4
@ -89,6 +89,7 @@ class SavePasswordController implements RequestHandlerInterface
|
||||
} catch (ValidationException $e) {
|
||||
$request->getAttribute('session')->put('errors', new MessageBag($e->errors()));
|
||||
|
||||
// @todo: must return a 422 instead, look into renderable exceptions.
|
||||
return new RedirectResponse($this->url->to('forum')->route('resetPassword', ['token' => $token->token]));
|
||||
}
|
||||
|
||||
@ -97,8 +98,6 @@ class SavePasswordController implements RequestHandlerInterface
|
||||
|
||||
$this->dispatchEventsFor($token->user);
|
||||
|
||||
$token->delete();
|
||||
|
||||
$session = $request->getAttribute('session');
|
||||
$accessToken = SessionAccessToken::generate($token->user->id);
|
||||
$this->authenticator->logIn($session, $accessToken);
|
||||
|
39
framework/core/src/User/TokensClearer.php
Normal file
39
framework/core/src/User/TokensClearer.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User;
|
||||
|
||||
use Flarum\User\Event\EmailChanged;
|
||||
use Flarum\User\Event\PasswordChanged;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
|
||||
class TokensClearer
|
||||
{
|
||||
public function subscribe(Dispatcher $events): void
|
||||
{
|
||||
$events->listen([PasswordChanged::class, EmailChanged::class], [$this, 'clearPasswordTokens']);
|
||||
$events->listen(PasswordChanged::class, [$this, 'clearEmailTokens']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PasswordChanged|EmailChanged $event
|
||||
*/
|
||||
public function clearPasswordTokens($event): void
|
||||
{
|
||||
$event->user->passwordTokens()->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PasswordChanged $event
|
||||
*/
|
||||
public function clearEmailTokens($event): void
|
||||
{
|
||||
$event->user->emailTokens()->delete();
|
||||
}
|
||||
}
|
@ -721,6 +721,16 @@ class User extends AbstractModel
|
||||
return $this->hasMany(EmailToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the user's email tokens.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function passwordTokens()
|
||||
{
|
||||
return $this->hasMany(PasswordToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the permissions of all of the groups that
|
||||
* the user is in.
|
||||
|
@ -119,6 +119,7 @@ class UserServiceProvider extends AbstractServiceProvider
|
||||
$events->listen(EmailChangeRequested::class, EmailConfirmationMailer::class);
|
||||
|
||||
$events->subscribe(UserMetadataUpdater::class);
|
||||
$events->subscribe(TokensClearer::class);
|
||||
|
||||
User::registerPreference('discloseOnline', 'boolval', true);
|
||||
User::registerPreference('indexProfile', 'boolval', true);
|
||||
|
@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\api\users;
|
||||
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\EmailToken;
|
||||
use Flarum\User\PasswordToken;
|
||||
|
||||
class PasswordEmailTokensTest extends TestCase
|
||||
{
|
||||
use RetrievesAuthorizedUsers;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
$this->normalUser(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function actor_has_no_tokens_by_default()
|
||||
{
|
||||
$this->app();
|
||||
|
||||
$this->assertEquals(0, PasswordToken::query()->where('user_id', 2)->count());
|
||||
$this->assertEquals(0, EmailToken::query()->where('user_id', 2)->count());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function password_tokens_are_generated_when_requesting_password_reset()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/forgot', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'email' => 'normal@machine.local'
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
$this->assertEquals(1, PasswordToken::query()->where('user_id', 2)->count());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function password_tokens_are_deleted_after_password_reset()
|
||||
{
|
||||
$this->app();
|
||||
|
||||
// Request password change to generate a token.
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/forgot', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'email' => 'normal@machine.local'
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
// Additional Tokens
|
||||
PasswordToken::generate(2)->save();
|
||||
PasswordToken::generate(2)->save();
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
$this->assertEquals(3, PasswordToken::query()->where('user_id', 2)->count());
|
||||
|
||||
// Use a token to reset password
|
||||
$response = $this->send(
|
||||
$request = $this->requestWithCsrfToken(
|
||||
$this->request('POST', '/reset', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withParsedBody([
|
||||
'passwordToken' => PasswordToken::query()->latest()->first()->token,
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(0, PasswordToken::query()->where('user_id', 2)->count());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function email_tokens_are_generated_when_requesting_email_change()
|
||||
{
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/users/2', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'email' => 'new-normal@machine.local'
|
||||
]
|
||||
],
|
||||
'meta' => [
|
||||
'password' => 'too-obscure'
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(1, EmailToken::query()->where('user_id', 2)->count());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function email_tokens_are_deleted_when_confirming_email()
|
||||
{
|
||||
$this->app();
|
||||
|
||||
EmailToken::generate('new-normal2@machine.local', 2)->save();
|
||||
EmailToken::generate('new-normal3@machine.local', 2)->save();
|
||||
$token = EmailToken::generate('new-normal@machine.local', 2);
|
||||
$token->save();
|
||||
|
||||
$response = $this->send(
|
||||
$this->requestWithCsrfToken(
|
||||
$this->request('POST', '/confirm/'.$token->token, [
|
||||
'authenticatedAs' => 2
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(0, EmailToken::query()->where('user_id', 2)->count());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function email_tokens_are_deleted_after_password_reset()
|
||||
{
|
||||
$this->app();
|
||||
|
||||
// Request password change to generate a token.
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/forgot', [
|
||||
'authenticatedAs' => 2,
|
||||
'json' => [
|
||||
'email' => 'normal@machine.local'
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
// Additional Tokens
|
||||
EmailToken::generate('new-normal@machine.local', 2)->save();
|
||||
EmailToken::generate('new-normal@machine.local', 2)->save();
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
$this->assertEquals(2, EmailToken::query()->where('user_id', 2)->count());
|
||||
|
||||
// Use a token to reset password
|
||||
$response = $this->send(
|
||||
$request = $this->requestWithCsrfToken(
|
||||
$this->request('POST', '/reset', [
|
||||
'authenticatedAs' => 2,
|
||||
])->withParsedBody([
|
||||
'passwordToken' => PasswordToken::query()->latest()->first()->token,
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(0, EmailToken::query()->where('user_id', 2)->count());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function password_tokens_are_deleted_when_confirming_email()
|
||||
{
|
||||
$this->app();
|
||||
|
||||
PasswordToken::generate(2)->save();
|
||||
PasswordToken::generate(2)->save();
|
||||
|
||||
$token = EmailToken::generate('new-normal@machine.local', 2);
|
||||
$token->save();
|
||||
|
||||
$response = $this->send(
|
||||
$this->requestWithCsrfToken(
|
||||
$this->request('POST', '/confirm/'.$token->token, [
|
||||
'authenticatedAs' => 2
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertEquals(302, $response->getStatusCode());
|
||||
$this->assertEquals(0, PasswordToken::query()->where('user_id', 2)->count());
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ use Dflydev\FigCookies\SetCookie;
|
||||
use Illuminate\Support\Str;
|
||||
use Laminas\Diactoros\CallbackStream;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
/**
|
||||
@ -66,4 +67,15 @@ trait BuildsHttpRequests
|
||||
|
||||
return $req->withCookieParams($cookies);
|
||||
}
|
||||
|
||||
protected function requestWithCsrfToken(ServerRequestInterface $request): ServerRequestInterface
|
||||
{
|
||||
$initial = $this->send(
|
||||
$this->request('GET', '/')
|
||||
);
|
||||
|
||||
$token = $initial->getHeaderLine('X-CSRF-Token');
|
||||
|
||||
return $this->requestWithCookiesFrom($request->withHeader('X-CSRF-Token', $token), $initial);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user