Skip to content
This repository has been archived by the owner on Oct 1, 2021. It is now read-only.

Commit

Permalink
feat: 2auth reset password workflow (#133)
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonsobries authored May 4, 2021
1 parent 5167331 commit 18d5797
Show file tree
Hide file tree
Showing 11 changed files with 486 additions and 20 deletions.
3 changes: 2 additions & 1 deletion config/fortify.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@
*/

'routes' => [
'feedback_thank_you' => env('ROUTE_FEEDBACK_THANK_YOU', '/feedback/thank-you'),
'feedback_thank_you' => env('ROUTE_FEEDBACK_THANK_YOU', '/feedback/thank-you'),
'two_factor_reset_password' => env('ROUTE_TWO_RESET_PASSWORD', '/two-factor/reset-password/{token}'),
],

];
5 changes: 3 additions & 2 deletions resources/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
declare(strict_types=1);

return [
'password_doesnt_match' => 'The provided password does not match your current password.',
'password_doesnt_match_records' => 'This password does not match our records.',
'password_doesnt_match' => 'The provided password does not match your current password.',
'password_doesnt_match_records' => 'This password does not match our records.',
'password_reset_link_invalid' => 'Your password reset link expired or is invalid.',

'messages' => [
'one_time_password' => 'We were not able to enable two-factor authentication with this one-time password.',
Expand Down
2 changes: 1 addition & 1 deletion resources/views/auth/reset-password.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
<x-data-bag key="fortify-content" resolver="name" view="ark-fortify::components.component-heading" />

<div class="max-w-xl py-8 mx-auto">
<livewire:auth.reset-password-form />
<livewire:auth.reset-password-form :token="request()->route('token')" :email="old('email', request()->email)" />
</div>
@endsection
16 changes: 13 additions & 3 deletions resources/views/auth/two-factor-challenge.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@
x-cloak
class="max-w-xl p-8 mx-auto"
>


<form
x-show="!recovery"
method="POST"
action="{{ route('two-factor.login') }}"
class="flex flex-col py-8 px-4 sm:px-8 mx-4 border-2 rounded-xl border-theme-secondary-200"
action="{{ isset($resetPassword) ? route('two-factor.reset-password-store', ['token' => $token]) : route('two-factor.login') }}"
class="flex flex-col px-4 py-8 mx-4 border-2 sm:px-8 rounded-xl border-theme-secondary-200"
>
@csrf

<div class="mb-8">
@isset($resetPassword)
<input type="hidden" name="email" value="{{ $email }}" />
@endisset

<div class="flex flex-1">
<x-ark-input
type="text"
Expand Down Expand Up @@ -63,12 +69,16 @@ class="w-full"
<form
x-show="recovery"
method="POST"
action="{{ route('two-factor.login') }}"
action="{{ isset($resetPassword) ? route('two-factor.reset-password-store', ['token' => $token]) : route('two-factor.login') }}"
class="flex flex-col p-8 mx-4 border-2 rounded-xl border-theme-secondary-200"
>
@csrf

<div class="mb-8" >
@isset($resetPassword)
<input type="hidden" name="email" value="{{ $email }}" />
@endisset

<div class="flex flex-1">
<x-ark-input
type="password"
Expand Down
16 changes: 13 additions & 3 deletions src/Components/ResetPasswordForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace ARKEcosystem\Fortify\Components;

use ARKEcosystem\Fortify\Components\Concerns\ValidatesPassword;
use ARKEcosystem\Fortify\Models;
use Livewire\Component;

class ResetPasswordForm extends Component
Expand All @@ -13,16 +14,25 @@ class ResetPasswordForm extends Component

public $token;

public ?string $twoFactorSecret;

public array $state = [
'email' => '',
'password' => '',
'password_confirmation' => '',
];

public function mount()
public function mount(?string $token = null, ?string $email = null)
{
$this->token = request()->route('token');
$this->state['email'] = old('email', request()->email);
$this->token = $token;

if ($email !== null) {
$this->state['email'] = $email;

$user = Models::user()::where('email', $email)->firstOrFail();

$this->twoFactorSecret = $user->two_factor_secret;
}
}

/**
Expand Down
20 changes: 10 additions & 10 deletions src/FortifyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use ARKEcosystem\Fortify\Components\UpdateProfilePhotoForm;
use ARKEcosystem\Fortify\Components\UpdateTimezoneForm;
use ARKEcosystem\Fortify\Components\VerifyEmail;
use ARKEcosystem\Fortify\Http\Controllers\TwoFactorAuthenticatedPasswordResetController;
use ARKEcosystem\Fortify\Http\Responses\FailedPasswordResetLinkRequestResponse as FortifyFailedPasswordResetLinkRequestResponse;
use ARKEcosystem\Fortify\Http\Responses\SuccessfulPasswordResetLinkRequestResponse as FortifySuccessfulPasswordResetLinkRequestResponse;
use ARKEcosystem\Fortify\Responses\FailedTwoFactorLoginResponse;
Expand Down Expand Up @@ -180,16 +181,7 @@ private function registerViews(): void
$user = Models::user()::where('email', $request->get('email'))->firstOrFail();

if ($user->two_factor_secret) {
if (! $request->session()->get('errors')) {
$request->session()->put([
'login.idFailure' => $user->getKey(),
'login.id' => $user->getKey(),
'login.remember' => true,
'url.intended' => route('account.settings.password'),
]);

return redirect()->route('two-factor.login');
}
return redirect()->route('two-factor.reset-password', ['token' => $request->token, 'email' => $user->email]);
}

return view('ark-fortify::auth.reset-password', ['request' => $request]);
Expand Down Expand Up @@ -255,6 +247,14 @@ public function registerRoutes(): void
Route::view(config('fortify.routes.feedback_thank_you'), 'ark-fortify::profile.feedback-thank-you')
->name('profile.feedback.thank-you')
->middleware('signed');

Route::get(config('fortify.routes.two_factor_reset_password'), [TwoFactorAuthenticatedPasswordResetController::class, 'create'])
->name('two-factor.reset-password')
->middleware('guest');

Route::post(config('fortify.routes.two_factor_reset_password'), [TwoFactorAuthenticatedPasswordResetController::class, 'store'])
->name('two-factor.reset-password-store')
->middleware('guest');
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace ARKEcosystem\Fortify\Http\Controllers;

use ARKEcosystem\Fortify\Http\Requests\TwoFactorResetPasswordRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Routing\Controller;
use Laravel\Fortify\Contracts\FailedTwoFactorLoginResponse;

class TwoFactorAuthenticatedPasswordResetController extends Controller
{
/**
* Show the two factor authentication challenge view.
*
* @param TwoFactorResetPasswordRequest $request
* @param string $token
*
* @return mixed
*/
public function create(TwoFactorResetPasswordRequest $request, string $token)
{
if (! $request->hasChallengedUser()) {
throw new HttpResponseException(redirect()->route('login'));
}

if (! $request->hasValidToken()) {
throw new HttpResponseException(redirect()->route('login')->withErrors(['email' => trans('fortify::validation.password_reset_link_invalid')]));
}

return view('ark-fortify::auth.two-factor-challenge', [
'token' => $token,
'resetPassword' => true,
'email' => $request->challengedUser()->email,
]);
}

/**
* Validates the 2fa code and shows the reset password form.
*
* @param TwoFactorResetPasswordRequest $request
*
* @return mixed
*/
public function store(TwoFactorResetPasswordRequest $request)
{
$user = $request->challengedUser();

if (! $request->hasValidToken()) {
throw new HttpResponseException(redirect()->route('login')->withErrors(['email' => trans('fortify::validation.password_reset_link_invalid')]));
}

if ($code = $request->validRecoveryCode()) {
$user->replaceRecoveryCode($code);
} elseif (! $request->hasValidCode()) {
return app(FailedTwoFactorLoginResponse::class);
}

return view('ark-fortify::auth.reset-password');
}
}
62 changes: 62 additions & 0 deletions src/Http/Requests/TwoFactorResetPasswordRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace ARKEcosystem\Fortify\Http\Requests;

use Illuminate\Contracts\Auth\PasswordBroker;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Http\Exceptions\HttpResponseException;
use Laravel\Fortify\Contracts\FailedTwoFactorLoginResponse;
use Laravel\Fortify\Http\Requests\TwoFactorLoginRequest;

class TwoFactorResetPasswordRequest extends TwoFactorLoginRequest
{
/**
* Determine if the reset token is valid.
*
* @return bool
*/
public function hasValidToken()
{
$user = $this->challengedUser();

return $user && app(PasswordBroker::class)->tokenExists($user, $this->route('token'));
}

/**
* Determine if there is a challenged user in the current session.
*
* @return bool
*/
public function hasChallengedUser()
{
$model = app(StatefulGuard::class)->getProvider()->getModel();

return $this->has('email') &&
$model::whereEmail($this->get('email'))->exists();
}

/**
* Get the user that is attempting the two factor challenge.
*
* @return mixed
*/
public function challengedUser()
{
if ($this->challengedUser) {
return $this->challengedUser;
}

$model = app(StatefulGuard::class)->getProvider()->getModel();

if (! $this->has('email') ||
! $user = $model::whereEmail($this->get('email'))->first()) {
throw new HttpResponseException(
app(FailedTwoFactorLoginResponse::class)->toResponse($this)
);
}

return $this->challengedUser = $user;
}
}
20 changes: 20 additions & 0 deletions tests/Components/ResetPasswordFormTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Tests\Components;

use ARKEcosystem\Fortify\Components\ResetPasswordForm;
use Illuminate\Support\Facades\Config;
use Livewire\Livewire;
use function Tests\createUserModel;

Expand All @@ -20,3 +21,22 @@
])
->assertViewIs('ark-fortify::auth.reset-password-form');
});

it('gets the two factor code and the email', function () {
Config::set('fortify.models.user', \ARKEcosystem\Fortify\Models\User::class);

$user = createUserModel();

$user->two_factor_secret = 'secret';
$user->save();

Livewire::actingAs($user)
->test(ResetPasswordForm::class, ['email' => $user->email])
->assertSet('state', [
'email' => $user->email,
'password' => '',
'password_confirmation' => '',
])
->assertSet('twoFactorSecret', 'secret')
->assertViewIs('ark-fortify::auth.reset-password-form');
});
Loading

0 comments on commit 18d5797

Please sign in to comment.