Laragear / TwoFactor

Two-Factor Authentication for all your users out-of-the-box.
https://github.com/sponsors/DarkGhostHunter
MIT License
261 stars 20 forks source link

[1.0] Multiple inputs for 2fa code #53

Closed poseso closed 6 months ago

poseso commented 1 year ago

Please check these requirements

Description

Is it possible to use 6 inputs for entering the 2fa_code ? I tried adding

public function prepareForValidation() { $this->merge([ '2fa_code' => $this->code1 . $this->code2 . $this->code3 . $this->code4 . $this->code5 . $this->code6 ]); }

and I do receive the 2fa_code
// 123456

but it doesn't work or gets validated, I would like to know if its possible? Thank you

Code sample

<!--begin::Section-->
                                <div class="mb-10">
                                    <!--begin::Label-->
                                    <div class="fw-bold text-start text-dark fs-6 mb-1 ms-1" data-kt-translate="two-step-label">Type your 6 digit security code</div>
                                    <!--end::Label-->
                                    <!--begin::Input group-->
                                    <div class="d-flex flex-wrap flex-stack">
                                        <input type="text" name="code_1" data-inputmask="'mask': '9', 'placeholder': ''" maxlength="1" class="form-control form-control-solid h-60px w-60px fs-2qx text-center border-primary border-hover mx-1 my-2" value="" />
                                        <input type="text" name="code_2" data-inputmask="'mask': '9', 'placeholder': ''" maxlength="1" class="form-control form-control-solid h-60px w-60px fs-2qx text-center border-primary border-hover mx-1 my-2" value="" />
                                        <input type="text" name="code_3" data-inputmask="'mask': '9', 'placeholder': ''" maxlength="1" class="form-control form-control-solid h-60px w-60px fs-2qx text-center border-primary border-hover mx-1 my-2" value="" />
                                        <input type="text" name="code_4" data-inputmask="'mask': '9', 'placeholder': ''" maxlength="1" class="form-control form-control-solid h-60px w-60px fs-2qx text-center border-primary border-hover mx-1 my-2" value="" />
                                        <input type="text" name="code_5" data-inputmask="'mask': '9', 'placeholder': ''" maxlength="1" class="form-control form-control-solid h-60px w-60px fs-2qx text-center border-primary border-hover mx-1 my-2" value="" />
                                        <input type="text" name="code_6" data-inputmask="'mask': '9', 'placeholder': ''" maxlength="1" class="form-control form-control-solid h-60px w-60px fs-2qx text-center border-primary border-hover mx-1 my-2" value="" />
                                    </div>
                                    <!--begin::Input group-->
                                </div>
                                <!--end::Section-->
DarkGhostHunter commented 1 year ago

This seems useful. I'll make some tests to see how it can be possible.

poseso commented 1 year ago

Screen Shot 2023-07-04 at 10 11 40 AM

poseso commented 1 year ago

That's what I'm trying to achieve

DarkGhostHunter commented 1 year ago

I understand. You can fix on the frontend by capturing the input and doing a fetch POST with cookies with each input concatenated into one string, and redirect on success.

poseso commented 1 year ago

I have like this on my LoginController:

    public function login(LoginRequest $request)
    {
        $attempt = Auth2FA::attempt($request->only('email', 'password'), $request->filled('remember'));

        if ($attempt) {
            $user = auth()->user();

            if (! $user->isActive()) {
                auth()->logout();

                return redirect()->route('frontend.auth.login')->withFlashDanger(__('Su cuenta ha sido desactivada.'));
            }

            event(new UserLoggedIn($user));

            if (config('boilerplate.access.user.single_login')) {
                auth()->logoutOtherDevices($request->password);
            }

            return redirect()->intended($this->redirectPath());
        }

        return back()->withFlashDanger(__('No hay ningún usuario que coincida con estas credenciales.'));
    }

and this is my LoginRequest:

<?php

namespace App\Http\Requests\Auth;

use App\Rules\Auth\Captcha;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Laragear\TwoFactor\TwoFactor;

class LoginRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        if ($this->isNotFilled('2fa_code')) {
            return [
                'email' => ['required', 'string', 'email'],
                'password' => ['required', 'string'],
                'g-recaptcha-response' => ['required_if:captcha_status,true', new Captcha],
            ];
        }

        return [
            '2fa_code' => ['required']
        ];
    }

    /**
     * Attempt to authenticate the request's credentials.
     *
     * @return void
     *
     * @throws ValidationException
     */
    public function authenticate()
    {
        $this->ensureIsNotRateLimited();

        $attempt = Auth::attemptWhen(
            $this->only('email', 'password'),
            TwoFactor::hasCodeOrFails(),
            $this->boolean('remember')
        );

        if (! $attempt) {
            RateLimiter::hit($this->throttleKey());

            throw ValidationException::withMessages([
                'email' => trans('auth.failed'),
            ]);
        }

        RateLimiter::clear($this->throttleKey());
    }

    /**
     * Ensure the login request is not rate limited.
     *
     * @return void
     *
     * @throws ValidationException
     */
    public function ensureIsNotRateLimited()
    {
        if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
            return;
        }

        event(new Lockout($this));

        $seconds = RateLimiter::availableIn($this->throttleKey());

        throw ValidationException::withMessages([
            'email' => trans('auth.throttle', [
                'seconds' => $seconds,
                'minutes' => ceil($seconds / 60),
            ]),
        ]);
    }

    /**
     * Get the rate limiting throttle key for the request.
     *
     * @return string
     */
    public function throttleKey()
    {
        return Str::lower($this->input('email')).'|'.$this->ip();
    }

    /**
     * @return array
     */
    public function messages()
    {
        return [
            'email.required' => __('Es obligatorio ingresar su dirección de correo.'),
            'password.required' => __('Es obligatorio ingresar su contraseña.'),
            'g-recaptcha-response.required_if' => __('El campo :attribute es obligatorio.', ['attribute' => 'CAPTCHA']),
        ];
    }
}

How should be the approach you mention?

Thank you for your quick response!

DarkGhostHunter commented 1 year ago

On the frontend, use JavaScript to capture the form submit and prevent it. From there, create a function that concatenates all the form inputs in one string, and send it to the backend.

It uses fetch(). If the response fails, I just un hide a div with a generic error. If it succeeded, I redirect the user to the dashboard.

That's how I do it on an private app. I don't need to do that on the backend.

poseso commented 1 year ago

Okay, I'll try and get back to you with a feedback, Thanks