Skip to content

Commit

Permalink
feat(users): add two factor authentication (#80)
Browse files Browse the repository at this point in the history
* - Switch from laravel/ui to laravel/fortify for authentication
- Implement two-factor authentication

* Fix user model reference

* Adjust email verification resend url

* Partially override fortify routes to disable the default two-factor enable/disable routes

* feat(auth): reimplement invitation consumption

* fix: remove laravel/ui routes

* refactor: fix PHP styling

* feat(users): update fortify implementation
- make recaptcha toggleable in config
- implement laravel honeypot on fortify routes
- fix resend verification email route

* refactor: fix blade formatting

* fix(users): fix login form display
- remove redundant controller functions

* fix(users): put back confirm 2FA function

* refactor: fix PHP styling

* chore(deps-dev): update JS dependencies

* refactor: fix blade formatting

* fix(users): add notice to 2FA settings re alias login

* refactor: fix blade formatting

* refactor(users): move register validator to UserService

---------

Co-authored-by: itinerare <[email protected]>
  • Loading branch information
itinerare and itinerare authored Dec 11, 2023
1 parent f82a7f2 commit 4e07b2e
Show file tree
Hide file tree
Showing 32 changed files with 1,441 additions and 315 deletions.
38 changes: 38 additions & 0 deletions app/Actions/Fortify/CreateNewUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Actions\Fortify;

use App\Models\Invitation;
use App\Models\User\User;
use App\Services\InvitationService;
use App\Services\UserService;
use Illuminate\Support\Facades\Hash;
use Laravel\Fortify\Contracts\CreatesNewUsers;

class CreateNewUser implements CreatesNewUsers {
use PasswordValidationRules;

/**
* Validate and create a newly registered user.
*
* @return \App\Models\User
*/
public function create(array $input) {
(new UserService)->validator($input)->validate();

$user = User::create([
'name' => $input['name'],
'email' => $input['email'],
'password' => Hash::make($input['password']),
'rank_id' => 2,
]);

if (isset($input['code'])) {
if (!(new InvitationService)->useInvitation(Invitation::where('code', $input['code'])->whereNull('recipient_id')->first(), $user)) {
throw new \Exception('An error occurred while using the invitation code.');
}
}

return $user;
}
}
16 changes: 16 additions & 0 deletions app/Actions/Fortify/PasswordValidationRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Actions\Fortify;

use Laravel\Fortify\Rules\Password;

trait PasswordValidationRules {
/**
* Get the validation rules used to validate passwords.
*
* @return array
*/
protected function passwordRules() {
return ['required', 'string', new Password, 'confirmed'];
}
}
26 changes: 26 additions & 0 deletions app/Actions/Fortify/ResetUserPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\Actions\Fortify;

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;

class ResetUserPassword implements ResetsUserPasswords {
use PasswordValidationRules;

/**
* Validate and reset the user's forgotten password.
*
* @param mixed $user
*/
public function reset($user, array $input) {
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();

$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}
31 changes: 31 additions & 0 deletions app/Actions/Fortify/UpdateUserPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Actions\Fortify;

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;

class UpdateUserPassword implements UpdatesUserPasswords {
use PasswordValidationRules;

/**
* Validate and update the user's password.
*
* @param mixed $user
*/
public function update($user, array $input) {
Validator::make($input, [
'current_password' => ['required', 'string'],
'password' => $this->passwordRules(),
])->after(function ($validator) use ($user, $input) {
if (!isset($input['current_password']) || !Hash::check($input['current_password'], $user->password)) {
$validator->errors()->add('current_password', __('The provided password does not match your current password.'));
}
})->validateWithBag('updatePassword');

$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}
54 changes: 54 additions & 0 deletions app/Actions/Fortify/UpdateUserProfileInformation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace App\Actions\Fortify;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;

class UpdateUserProfileInformation implements UpdatesUserProfileInformation {
/**
* Validate and update the given user's profile information.
*
* @param mixed $user
*/
public function update($user, array $input) {
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],

'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');

if ($input['email'] !== $user->email &&
$user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
}

/**
* Update the given verified user's profile information.
*
* @param mixed $user
*/
protected function updateVerifiedUser($user, array $input) {
$user->forceFill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();

$user->sendEmailVerificationNotification();
}
}
16 changes: 0 additions & 16 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use App\Models\User\User;
use App\Models\User\UserAlias;
use App\Services\LinkService;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Laravel\Socialite\Facades\Socialite;
Expand All @@ -23,8 +22,6 @@ class LoginController extends Controller {
|
*/

use AuthenticatesUsers;

/**
* Where to redirect users after login.
*
Expand All @@ -39,19 +36,6 @@ public function __construct() {
$this->middleware('guest')->except('logout');
}

/**
* Show the application's login form.
*
* @return \Illuminate\Http\Response
*/
public function showLoginForm() {
$altLogins = array_filter(Config::get('lorekeeper.sites'), function ($item) {
return isset($item['login']) && $item['login'] === 1 && $item['display_name'] != 'tumblr';
});

return view('auth.login', ['userCount' => User::count(), 'altLogins' => $altLogins]);
}

/**
* Authenticate via Aliases.
*
Expand Down
59 changes: 1 addition & 58 deletions app/Http/Controllers/Auth/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@
use App\Services\InvitationService;
use App\Services\LinkService;
use App\Services\UserService;
use Carbon\Carbon;
use DB;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Validator;
use Laravel\Socialite\Facades\Socialite;
use Settings;

Expand All @@ -32,8 +28,6 @@ class RegisterController extends Controller {
|
*/

use RegistersUsers;

/**
* Where to redirect users after registration.
*
Expand Down Expand Up @@ -84,7 +78,7 @@ public function postRegisterWithDriver(LinkService $service, Request $request, $

$data = $request->all();

$this->validator($data, true)->validate();
(new UserService)->validator($data, true)->validate();
$user = $this->create($data);
if ($service->saveProvider($provider, $providerData, $user)) {
Auth::login($user);
Expand All @@ -99,57 +93,6 @@ public function postRegisterWithDriver(LinkService $service, Request $request, $
}
}

/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm() {
$altRegistrations = array_filter(Config::get('lorekeeper.sites'), function ($item) {
return isset($item['login']) && $item['login'] === 1 && $item['display_name'] != 'tumblr';
});

return view('auth.register', ['userCount' => User::count(), 'altRegistrations' => $altRegistrations]);
}

/**
* Get a validator for an incoming registration request.
*
* @param mixed $socialite
*
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data, $socialite = false) {
return Validator::make($data, [
'name' => ['required', 'string', 'min:3', 'max:25', 'alpha_dash', 'unique:users'],
'email' => ($socialite ? [] : ['required']) + ['string', 'email', 'max:255', 'unique:users'],
'agreement' => ['required', 'accepted'],
'password' => ($socialite ? [] : ['required']) + ['string', 'min:8', 'confirmed'],
'dob' => [
'required', function ($attribute, $value, $fail) {
$formatDate = Carbon::createFromFormat('Y-m-d', $value);
$now = Carbon::now();
if ($formatDate->diffInYears($now) < 13) {
$fail('You must be 13 or older to access this site.');
}
},
],
'code' => ['string', function ($attribute, $value, $fail) {
if (!Settings::get('is_registration_open')) {
if (!$value) {
$fail('An invitation code is required to register an account.');
}
$invitation = Invitation::where('code', $value)->whereNull('recipient_id')->first();
if (!$invitation) {
$fail('Invalid code entered.');
}
}
},
],
'g-recaptcha-response' => 'required|recaptchav3:register,0.5',
]);
}

/**
* Create a new user instance after a valid registration.
*
Expand Down
Loading

0 comments on commit 4e07b2e

Please sign in to comment.