0

我正在使用 Laravel 8.x 与 Fortify 和 Jetstream/Livewire 并打开 2FA / OTP:

配置/fortify.php

'features' => [

    Features::registration(),
    Features::resetPasswords(),
    Features::emailVerification(),
    Features::updateProfileInformation(),
    Features::updatePasswords(),
    Features::twoFactorAuthentication(),
],

我还对我的路由组等进行了一些定制,以允许某些页面无需身份验证而其他页面。这很好用。然而,我们的许多用户并不是真正的技术人员,并且发现使用 Auth 应用程序很烦人(我有点同意这一点),尽管我喜欢使用 Fortify 在 2FA 中烘焙。

我想做的是:

  1. 修改现有的 Fortify 代码以选择性地允许通过电子邮件实现 2FA。可能很难实现一个或另一个,或两者兼而有之,以便用户可以使用任何可用的方法,但似乎可以允许用户选择他们想要的方法。使用 Auth App 内置 Fortify 方法,或使用其他包发送电子邮件 OTP 或通过在发布需要修改的文件后修改 Fortify 代码。

我环顾了一下,下面的那个看起来很简单,但我想将那个用于电子邮件方法,将 Fortify 用于 Authenticator 方法。那将是用户偏好选项。

github.com/seshac/otp-generator

我尝试通过使用 Fortify 的就地 Laravel 8.x JetStream 框架来做到这一点,方法如下:

视图/auth/login.blade.php

将表格更改为:

<form method="POST" action="{{ route('login') }}" id = "OTPlogin">

在表格之后添加了这个(我也有 Spatie Cookie 提醒包)。

    <div id = "cookieremindertext">If not alreay done, click on "Allow Cookies".  Read the Privacy Policy and Terms if you wish.</div>
    <label for = "otp" id = "otpwrapper">Please Enter the OTP (One-Time Password) below and Click "LOGIN" again.
    <input type="text" name="otp" id = "otp" value = ""/>
    </label>

然后在页面上添加了相当多的 Javascript:

function dynamicPostForm (path, params, target) {

    method = "post";
    var form = document.createElement("form");
    form.setAttribute("method", method);
    form.setAttribute("action", path);

    if (target) {
        form.setAttribute("target", "_blank");
    }

    for (const [key, value] of Object.entries(params)) {

        if(params.hasOwnProperty(key)) {
            var hiddenField = document.createElement("input");
            hiddenField.setAttribute("type", "hidden");
            hiddenField.setAttribute("name", key);
            hiddenField.value = value;
            form.appendChild(hiddenField);
         }
    }
    
    var hiddenField = document.createElement("input");
    hiddenField.setAttribute("type", "hidden");
    hiddenField.setAttribute("name", "_token");
    hiddenField.setAttribute("value", '{{ csrf_token() }}');
    form.appendChild(hiddenField);
    document.body.appendChild(form);
    form.submit();
    $(form).remove();
}


$("#OTPlogin").on("submit", function(e) {

e.preventDefault();

    $.ajax({
    
        type: "POST",
        url: "/Auth/otp-generator",
        dataType: "json",
        data: {email:$("[name=email]").val(),password:$("[name=password]").val(),remember:$("[name=remember]").prop("checked"),otp:$("[name=otp]").val()},
        context: $(this),
        beforeSend: function(e) {$("body").addClass("loading");}

    }).done(function(data, textStatus, jqXHR) {
    
    if (data.error == 'otp') {
    
        // $("#otp").val(data.otp);
        $("#otpwrapper").show();
    }
    else if (data.message == "LOGIN") {
        params = {};
        params.email = $("[name=email]").val();
        params.password = $("[name=password]").val();
        params.remember = $("[name=remember]").prop("checked");
        dynamicPostForm ('/login', params, false);
        data.message = "Thank you . . . logging you in."
        // This will fail on the backend if the session indicating a valid OTP did not get set.
        
    }
    
    showMessage("",data.message);
        $(".modal-body").css("width", "320px");
    });
});

这基本上绕过了常规的 /login 路由,直到我从后端收到回复,表明 OTP 已通过电子邮件验证(可能稍后是 SMS)。

在以下位置添加了一条新路线:

路线/web.php

// Receive User Name and Pass, and possibly otp from the login page.

Route::post('/Auth/otp-generator', [EmailController::class, 'sendOTP']);  // move to middleware ?, Notifications or somewhere else ?

在我的 EmailController 中创建了一些新方法(可能应该将其重构到不同的位置或文件,但如下所示。处理 OTP 验证以及设置时: Session::put('OTP_PASS', true);

Http/Controllers/EmailController.php

protected function generateAndSendOTP($identifier) {

    $otp =  Otp::generate($identifier);
    $expires = Otp::expiredAt($identifier)->expired_at;
    try { 
        //$bcc = env('MAIL_BCC_EMAIL');
        Mail::to($identifier)->bcc("pacs@medical.ky")->send(new OTPMail($otp->token));
        echo '{"error":"otp","message":"Email Sent and / or SMS sent.<br><br>Copy your code and paste it in the field that says OTP below.  Then click on the Login Button again, and you should be logged in if all is correct.", "otp":"Token Sent, expires:  ' . $expires . '"}';
    }
    catch (\Exception $e) {
        //var_dump($e);
        echo '{"error":true,"message":'.json_encode($e->getMessage()) .'}';
    }
}

protected function sendOTP(Request $request) {

    $user = User::where('email', $request->email)->first();
    // $test = (new AWS_SNS_SMS_Tranaction())->toSns("+16513130209");

    if ($user && Hash::check($request->password, $user->password)) {
    
        if (!empty($request->input('otp'))) {
    
            $verify = Otp::validate($request->input('email'), $request->input('otp'));
            // gets here if user/pass is correct
            if ($verify->status == true) {

                echo '{"error":false,"message":"LOGIN"}';
                Session::put('OTP_PASS', true);
            }
            else if ($verify->status !== true) {
                echo '{"error":false,"message":"OTP is incorrect, try again"}';
            }
            else {
                // OTP is expired ?
                $this->generateAndSendOTP($request->input('email'));
            }
        }
        else if (empty($request->input('otp'))) {
            $this->generateAndSendOTP($request->input('email'));
        }
    }
    
    else {
      
        echo '{"error":true,"message":"User Name / Password Incorect, try again"}';
    }
}

然后最后在:

Providers/FortifyServiceProviders.php,我检查了Session::get('OTP_PASS') === true以验证登录。它实际上似乎“工作”,但如果用户在数据库中也有电话号码,我想可能扩展它以支持通过 SMS 发送 OTP。我现在正在使用电子邮件,因为它总是填充在数据库中。他们可能有也可能没有电话号码,我会将通知方法设为用户首选项。

我在另一个框架上设置了 AWS SNS,但不确定如何在 Laravel 上进行设置。 在 generateAndSendOTP($identifier) 方法中,我想添加一些东西来检查他们的用户偏好通知方法,然后将 OTP 发送到电子邮件和/或通过 SMS。 所以这是一个问题。另一个问题只是现在的整个设置,因为我可能需要将东西移动到不同的位置,现在这似乎正在工作。最好打包最终通过电子邮件和/或短信添加 OTP。Authenticator App 中的烘焙方法不应该真正受到影响,我可能希望在他们通过电子邮件/短信登录后选择性地保护某些路由,以应对可能有多个用户使用帐户的情况,例如代理,但帐户所有者不希望他们通过 Authenticator App 进入受内置 2FA 保护的部分。

谢谢。

4

1 回答 1

2

查看源代码,Fortify 实际上使用了 PragmaRX\Google2FA,因此您可以直接实例化它。

因此,您可以手动生成验证器应用程序将生成的当前 OTP,并使用它执行任何您需要的操作。在您的情况下,请在登录期间的 2FA 质询期间发送电子邮件(或 SMS)。

use PragmaRX\Google2FA\Google2FA;

...

$currentOTP = app(Google2FA::class)->getCurrentOtp(decrypt($user->two_factor_secret));

尽管我认为这是一个疏忽(并且不需要在初始密钥生成时进行确认),但它们没有包含内置的此功能。

于 2021-12-04T10:46:04.913 回答