【问题标题】:Add 2FA or OTP (via e-mail) to Laravel Fortify 2FA via Authenticator App通过 Authenticator App 将 2FA 或 OTP(通过电子邮件)添加到 Laravel Fortify 2FA
【发布时间】:2021-08-12 02:21:45
【问题描述】:

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

config/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 框架来做到这一点,方法如下:

views/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)。

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

routes/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' , 真);

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 保护的部分。

谢谢。

【问题讨论】:

标签: php laravel totp laravel-fortify


【解决方案1】:

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

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

use PragmaRX\Google2FA\Google2FA;

...

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

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

【讨论】:

  • 嘿,这很有趣。但如果我没记错的话,这种 OTP 很快就会过期——比如 30 秒?因此,如果您使用它发送 SMS,它甚至可能在用户收到它之前就已过期 - 您通常希望给 OTP 几分钟的有效期,对吧?
  • 是的,从技术上讲,它在 30 秒内是好的,但如果我没记错的话,算法本身仍然会认为最后一个数字在一定时间内也是有效的,所以在某种程度上它是好的一分钟。
  • 至于 OTP 的理想有效期,您必须记住事物的性质/目的;因此,理想情况下,有效期应尽可能短,并使用合理快速的交付方式。适应传输延迟(超过过期的宽限期)并不是这种安全实施的真正优先事项
猜你喜欢
  • 2022-06-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-15
  • 2018-03-12
  • 2020-07-17
  • 2022-01-19
  • 1970-01-01
相关资源
最近更新 更多