認証-認可

Laravel Google AuthenticatorによるTOTPログイン【Google2FA for Laravel】

Laravel

利用しているライブラリ

 

 

覚書

実装のサンプルコードです。

省略しているので動作しませんが、ご参考までに。

 

ログイン周りのカスタマイズをする必要があります。

ログイン周りは一歩間違えると危険なので、

サーバ側(AWS ElastiCache Redisなど)に格納されているセッション情報の中身を随時確認しながら開発するようにしましょう🐱

 

 

ライブラリのインストール

 

# composer require pragmarx/google2fa-laravel
# php artisan vendor:publish --provider="PragmaRX\Google2FALaravel\ServiceProvider"
# php artisan config:clear

 

実装例

 

ライブラリのコンフィグ

 

config/google2fa.php

<?php

return [

・・・

    'login_period' => [
        'day' => [
            'ttl' => 86400, // 1日の秒数
            'key' => "Day"
        ],
        'month' => [
            'ttl' => 2592000, // 30日の秒数
            'key' => "Month",
        ],
    ],
];

 

Controllerで利用するRequest

 

 

LoginByGoogleAuthenticatorRequest

<?php

namespace App\Http\Requests\SAMPLECM;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class LoginByGoogleAuthenticatorRequest 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()
    {
        $department_id = $this->get('department_id');

        return [
            'code' => [
                'required',
                'string',
                'size:6',
            ],
            'administrator_uuid' => [
                'required',
                'string',
                Rule::exists('company_administrators', 'uuid')->where(function ($query) use ($department_id) {
                    return $query->where('department_id', $department_id)
                        ->where('enable_mfa', 1);
                }),
            ],
        ];
    }
}

 

Controller

LoginController

<?php

namespace App\Http\Controllers\SAMPLECM;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use App\Http\Requests\SAMPLECM\AutoLoginRequest;
use App\Services\SAMPLECM\LoginService;
use App\Http\Requests\SAMPLECM\LoginByGoogleAuthenticatorRequest;
use App\Entities\CompanyAdministrator;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */
    use AuthenticatesUsers;

    /**
     * ログインロック仕様
     *
     * 試行回数 20回
     * ロック時間 3分
     */
    protected $maxAttempts = 20;
    protected $decayMinutes = 3;

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct(
        LoginService $s_login
    ) {
        $this->s_login = $s_login;
    }

    protected function sendLockoutResponse(Request $request)
    {
        // 残りロック時間
        $seconds = $this->limiter()->availableIn(
            $this->throttleKey($request)
        );
        // エラーメッセージ
        $messages = Lang::get('auth.throttle', ['seconds' => $seconds]);
        throw new \App\Exceptions\LimitLoginAttemptException($messages);
    }

    /**
     * Get the login username to be used by the controller.
     *
     * @return string
     */
    public function username()
    {
        return 'mail_address';
    }

    protected function guard()
    {
        return Auth::guard('samplecm');
    }

    /**
     * Show the application's login form.
     *
     * @return \Illuminate\Http\Response
     */
    public function showLoginForm()
    {
        return view('SAMPLECM.login.index');
    }

    /**
     * @override
     */
    protected function sendLoginResponse(Request $request)
    {
        $request->session()->regenerate();

        $this->clearLoginAttempts($request);
        return $this->authenticated($request, $this->guard()->user());
    }

    /**
     * @override
     */
    protected function authenticated(Request $request, $user)
    {
        // MFAが有効の場合はidのみ返却する
        if ($this->s_login->isGoogleAuthenticatorLogin($user)) {
            // MFA認証があるので、ログイン成功セッションを無効化
            $request->session()->invalidate();
            return [
                'administrator_uuid' => $user->company_administrator_uuid,
            ];
        }
        return $this->s_login->updateLastAuthenticatedAndUnsetPassword($user);
    }

    /**
     * @override
     */
    public function logout(Request $request)
    {
        $this->guard()->logout();
        $request->session()->invalidate();

        return redirect('/company/login');
    }

    public function autoLogin(AutoLoginRequest $request)
    {
        if ($this->guard()->loginUsingId($request->id)) {
            return $this->sendLoginResponse($request);
        }
        return $this->sendFailedLoginResponse($request);
    }

    /**
     * Google Authenticator 認証コードの検証
     *
     * @param LoginByGoogleAuthenticatorRequest $request
     * @return CompanyAdministrator
     */
    protected function loginByGoogleAuthenticator(LoginByGoogleAuthenticatorRequest $request): CompanyAdministrator
    {
        return $this->s_login->loginByGoogleAuthenticator($request);
    }
}


 

Service

 

LoginService

<?php

namespace App\Services\SAMPLECM;

use App\Entities\CompanyAdministrator;
use App\Repositories\Company\CompanyAdministratorRepository;
use App\Util\Google2FaUtil;
use App\Exceptions\LimitLoginAttemptException;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Carbon;
use App\Http\Requests\SAMPLECM\LoginByGoogleAuthenticatorRequest;
use Illuminate\Support\Facades\Auth;

class LoginService
{
    const LIMIT_TOTP_LOGIN_ATTEMPT = 5;
    const ALLOW_TOTP_LOGIN_HOURS = 1; // 現在より1時間後

    public function __construct(
        CompanyAdministratorRepository $r_administrator,
        Google2FaUtil $util_google2fa
    ) {
        $this->r_administrator = $r_administrator;
        $this->util_google2fa = $util_google2fa;
    }

    protected function guard()
    {
        return Auth::guard('samplecm');
    }

    /**
     * idで管理者を返却
     *
     * @param string $uuid
     * @return ?CompanyAdministrator
     */
    public function getCompanyAdministratorById(string $uuid): ?CompanyAdministrator
    {
        return $this->r_administrator->findByUuid($uuid);
    }

    /**
     * Google Authenticatorによるログインか?
     *
     * @param CompanyAdministrator $user
     * @return bool
     */
    public function isGoogleAuthenticatorLogin(CompanyAdministrator $user): bool
    {
        return !is_null($this->r_administrator->findGoogleAuthenticatorLogin($user->id, $user->department_id));
    }

    /**
     * Google Authenticatorによるトークン検証
     *
     * @param string $code
     * @param string $uuid
     * @return bool
     */
    public function checkGoogleAuthenticatorCode(string $code, string $uuid): bool
    {
        return $this->util_google2fa->verifyCode($this->r_administrator, $uuid, $code);
    }

    /**
     * Google AuthenticatorによるTOTPログイン試行攻撃対策のカラムを初期化
     *
     * @param CompanyAdministrator $user
     * @return void
     */
    public function clearTotpLoginAttempts(CompanyAdministrator $user): void
    {
        $user->login_attempt = null;
        $user->login_allow_time = null;
        $user->save();
    }

    /**
     * TOTPログイン試行対策ブロックをされているか?
     *
     * @param CompanyAdministrator $user
     * @return bool
     */
    public function isBlockTotpLogin(CompanyAdministrator $user): bool
    {
        $now = Carbon::now();
        // 現在日時がログイン許可時間の期限を迎えている場合はログイン試行回数を初期化
        if (!is_null($user->login_allow_time) && $now > $user->login_allow_time) {
            $this->clearTotpLoginAttempts($user);
            $user->save();
            return false;
        }
        // ログイン試行回数がブロック回数以上の場合
        if ($user->login_attempt >= self::LIMIT_TOTP_LOGIN_ATTEMPT) {
            return true;
        }
        return false;
    }

    /**
     * TOTPログイン失敗時は試行回数をインクリメントさせる
     *
     * @param CompanyAdministrator $user
     * @return void
     */
    public function failTotpLoginAttempt(CompanyAdministrator $user): void
    {
        $user->login_attempt++;

        // ログイン試行回数が既定に達したら許可時間を設定
        if ($user->login_attempt === self::LIMIT_TOTP_LOGIN_ATTEMPT) {
            $now = Carbon::now();
            $user->login_allow_time = $now->addHours(self::ALLOW_TOTP_LOGIN_HOURS)->format('Y-m-d H:i:s');
        }
        $user->login_attempt_time = Carbon::now()->format('Y-m-d H:i:s');
        $user->save();
    }

    /**
     * 残りロック時間を秒数で取得
     *
     * @param CompanyAdministrator $user
     * @return int
     */
    public function getAllowLoginSeconds(CompanyAdministrator $user): int
    {
        $now = Carbon::now();
        return $now->diffInSeconds($user->login_allow_time);
    }

    /**
     * セッションのTTLを返却
     *
     * @param string $login_period
     * @return int
     */
    public function getSessionLifetime(string $login_period): int
    {
        $config_google2fa = config('google2fa');
        return $config_google2fa['login_period'][mb_strtolower($login_period)]['ttl'] ?? $config_google2fa['login_period']['day']['ttl'];
    }

    /**
     * Google Authenticator 認証コードの検証
     *
     * @param LoginByGoogleAuthenticatorRequest $request
     * @return CompanyAdministrator
     */
    public function loginByGoogleAuthenticator(LoginByGoogleAuthenticatorRequest $request): CompanyAdministrator
    {
        $user = $this->getCompanyAdministratorById($request->administrator_uuid);
        // TOTPログイン試行対策で処理ブロックされているかチェック
        if ($this->isBlockTotpLogin($user)) {
            // ロック時の処理
            $seconds = $this->getAllowLoginSeconds($user);
            $messages = [
                'code' => __('auth.throttle', ['seconds' => $seconds])
            ];
            throw new LimitLoginAttemptException($messages);
        }
        if ($this->checkGoogleAuthenticatorCode($request->code, $request->administrator_uuid)) {
            // 成功時の処理
            $request->session()->regenerate();
            // セッションにログイン成功情報を記録
            $this->guard()->loginUsingId($user->id);
            $this->clearTotpLoginAttempts($user);
            return $this->updateLastAuthenticatedAndUnsetPassword($user);
        }
        // 失敗時の処理
        $this->failTotpLoginAttempt($user);
        return $this->sendFailedTotpLoginResponse("code");
    }

    /**
     * ログインユーザのレスポンスからパスワードとGoogle Authenticatorの秘密鍵を削除して返却
     *
     * @param CompanyAdministrator $user
     * @return CompanyAdministrator
     */
    public function updateLastAuthenticatedAndUnsetPassword(CompanyAdministrator $user): CompanyAdministrator
    {
        // 最終ログイン日時更新
        $user->last_authenticated_at = Carbon::now();
        $user->save();
        // 不必要なプロパティ削除
        unset(
            $user['password'],
            $user['mfa_secret_key'],
            $user['login_allow_time'],
            $user['login_attempt_time'],
            $user['login_attempt'],
            $user['enable_mfa'],
            $user['created_at'],
            $user['updated_at']
        );
        return $user;
    }

    /**
     * 失敗時のレスポンスを返却
     *
     * @param string $key
     * @return ValidationException
     */
    protected function sendFailedTotpLoginResponse(string $key): ValidationException
    {
        throw ValidationException::withMessages([
            $key => [trans('auth.failed')],
        ]);
    }
}

 

 

ライブラリ利用のUtil

 

Google2FaUtil

<?php

namespace App\Util;

use App\Repositories\Interfaces\MfaLoginInterface;
use Throwable;
use Illuminate\Support\Facades\Log;

/**
 * Google Authenticatorによる2Factor Authentication
 */
class Google2FaUtil
{
    public function __construct()
    {
        $this->i_google2fa = app('pragmarx.google2fa');
    }

    /**
     * 秘密鍵を返却
     *
     * @return string
     */
    public function getSecretKey(): string
    {
        return $this->i_google2fa->generateSecretKey();
    }

    /**
     * コードの検証
     *
     * @param MfaLoginInterface $i_mfa_login
     * @param string $uuid
     * @param string $code
     * @return bool
     */
    public function verifyCode(MfaLoginInterface $i_mfa_login, string $uuid, string $code): bool
    {
        $secret = $i_mfa_login->getSecret($uuid);
        return $this->i_google2fa->verifyGoogle2FA($secret, $code);
    }

    /**
     * QRコード生成に必要な情報を返却
     *
     * @param string $mfa_secret_key
     * @param string $user_name
     * @param string $app_name
     * @return string
     */
    public function getQrcode(string $mfa_secret_key, string $user_name, string $hostname): string
    {
        return urlencode('otpauth://totp/'.$user_name.'?secret='.$mfa_secret_key.'&issuer='.$app_name);
    }
}

 

インターフェイス

MfaLoginInterface

<?php

namespace App\Repositories\Interfaces;

interface MfaLoginInterface
{
    public function getSecret(string $uuid): ?string;
}

 

リポジトリ

 

CompanyAdministratorRepository

<?php

namespace App\Repositories\Company;

use App\Entities\CompanyAdministrator;
use Illuminate\Support\Collection;
use App\Repositories\Interfaces\MfaLoginInterface;
use Illuminate\Support\Facades\DB;

use Illuminate\Support\Str;

class CompanyAdministratorRepository implements MfaLoginInterface
{
    /**
     * @param \App\Entities\CompanyAdministrator $resource
     */
    public function __construct(CompanyAdministrator $resource)
    {
        $this->resource = $resource;
    }

    /**
     * uuidでインスタンス返却
     *
     * @param string $uuid
     * @return ?CompanyAdministrator
     */
    public function findByUuid(string $uuid): ?CompanyAdministrator
    {
        return $this->findWhere(['company_administrator_uuid' => $uuid]);
    }

    /**
     * idとdepartment idを指定して返却
     *
     * @param int $department_id
     * @param int $company_administrators_id
     * @return ?CompanyAdministrator
     */
    public function findByIdAndDepartmentId(int $department_id, int $company_administrators_id): ?CompanyAdministrator
    {
        return $this->resource
            ->where('company_administrators.department_id', $department_id)
            ->where('company_administrators.id', $company_administrators_id)
            ->first();
    }

    /** 管理者をUUIDで検索する
     *
     * @param integer $department_id
     * @param array $uuids
     * @return Collection
     */
    public function getByUuids(int $department_id, array $uuids):Collection
    {
        return $this->resource
            ->where('department_id', $department_id)
            ->whereIn('company_administrator_uuid', $uuids)
            ->get();
    }

    /**
     * idとdepartment idを指定してGoogle Authenticatorが有効な1レコードを返却
     *
     * @param int $id
     * @param int $department_id
     * @return ?CompanyAdministrator
     */
    public function findGoogleAuthenticatorLogin(int $id, int $department_id): ?CompanyAdministrator
    {
        return $this->resource
            ->join('departments', 'departments.id', '=', 'company_administrators.department_id')
            ->where('departments.id', $department_id)
            ->where('company_administrators.id', $id)
            ->where('departments.mfa_type', 'GoogleAuthenticator')
            ->where('company_administrators.enable_mfa', '1')
            ->first();
    }

    /**
     * MFA 秘密鍵を返却
     *
     * @param string $uuid
     * @return string
     */
    public function getSecret(string $uuid): ?string
    {
        $instance = $this->findByUuid($uuid);
        if (is_null($instance)) {
            return null;
        }
        return $instance->mfa_secret_key;
    }
}

 

ミドルウェア

 

SetSessionLifetimeWhenEnableMfa

<?php

namespace App\Http\Middleware\SAMPLECM;

use Closure;
use App\Util\DateUtil;

class SetSessionLifetimeWhenEnableMfa
{
    /**
     * Google Authenticatorでの2段階認証時のセッションTTL制御
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $this->r_administrator = app()->make(\App\Repositories\Company\CompanyAdministratorRepository::class);
        $user = $this->r_administrator->findGoogleAuthenticatorLogin($request->administrator_uuid ?? 0, $request->department_id);
        if (is_null($user)) {
            return $next($request);
        }

        // 管理者毎にセッションTTLを上書き。設定する際の単位が分なので変換する
        $this->s_login = app()->make(\App\Services\SAMPLECM\LoginService::class);
        config(['session.lifetime' => $this->_convertSecondsToMinutes($this->s_login->getSessionLifetime($user->login_period))]);
        return $next($request);
    }

    /**
     * 秒を分に変換
     *
     * @param integer $seconds
     * @return integer
     */
    private function _convertSecondsToMinutes(int $seconds): int
    {
        return floor($seconds / 60);
    }
}

 

 

参考

 

Amazonおすすめ

iPad 9世代 2021年最新作

iPad 9世代出たから買い替え。安いぞ!🐱 初めてならiPad。Kindleを外で見るならiPad mini。ほとんどの人には通常のiPadをおすすめします><

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)