[Laravel] Google2FA for Laravel

[Laravel] Google2FA for Laravel updated_at: 2024-11-22 15:50

Google2FA for Laravel

오늘은 라라벨에서 google2fa (구글을 이용한 2단계 인증) 에 대해서 설명 드리겠습니다.
사용할 package는 아래와 같으므로 더 자세한 것이 필요하면 참조하세요.
여기서는 간단하게 구현되는 것 까지만 처리 하도록 하겠습니다.

github

Installing

먼저 google2fa-laravel 및 qr-code를 위해 bacon-qr-code 를 인스톨 하겠습니다.

composer require pragmarx/google2fa-laravel
composer require bacon/bacon-qr-code

Users table modify

users 테이블에 google2fa_secret 필드를 추가합니다.

$table->string('google2fa_secret')->nullable()

처리하기

google2fa 가 세팅되었다면 로그인시 정상적인 ID/pwd 로 접근하였을때 바로 세션을 생성하지 않고 2google2fa를 실행시킨 후 이 것까지 성공하면 세션을 활성화 시킨다.
아래에 전체 코드를 같이 넣어 놓았으므로 참조 바랍니다.

routes > web.php

// 2fa
// setting : 2fa 가 활성화 / 비활성화 링크 노출
Route::get('2fa/setting', 'Google2FAController@setting');
// 2fa 활성화 시키기
Route::get('2fa/enable', 'Google2FAController@enableTwoFactor');
// 2fa 비활성화 시키기
Route::get('2fa/disable', 'Google2FAController@disableTwoFactor');
// 2fa 입력폼
Route::get('/2fa/validate', 'Google2FAController@getValidateToken');
// 2fa 확인폼
Route::post('/2fa/validate', 'Google2FAController@postValidateToken');

로그인

로그인시 google2fa_secret 존재 여부 체크 후 존재하면 2fa 입력폼 으로 리다이렉트 시킵니다.

public function login(Request $request){
  ..........
  // 로그인 유효성 처리 완료후
  if ($user->google2fa_secret) { // google2fa_secret 가 세팅된 상태라면
    Auth::logout(); // 로그아웃 시킨다.
    $request->session()->put('2fa:user:id', $user->id); // 현재값은 세션에 넣어 둔다.
    return redirect('2fa/validate'); // 2fa를 입력하는 창으로 이동 시킨다. (아래 getValidateToken() 호출)
  }
}

Google2FAController

<?php

namespace App\Http\Controllers;

use Crypt;
use Google2FA;
use Illuminate\Http\Request;
use App\Http\Requests\ValidateSecretRequest; // ValidateSecretRequest 소스는 아래 참조
use App\Http\Controllers\Controller;

use Carbon\Carbon;

class Google2FAController extends Controller
{

  public function __construct(){}

  public function setting(Request $request)
  {
    return view('google2fa.welcome');
  }

  /**
   *
   * @param \Illuminate\Http\Request $request
   * @return \Illuminate\Http\Response
   */
  public function enableTwoFactor(Request $request)
  {
    $secret = $this->generateSecret();

    //get user
    $user = $request->user();

    //encrypt and then save secret
    $user->google2fa_secret = Crypt::encrypt($secret);
    $user->save();

    $imageDataUri = Google2FA::getQRCodeInline(
      $request->getHttpHost(),
      $user->email,
      $secret,
      200
    );

    return view('google2fa.enableTwoFactor', [
      'image' => $imageDataUri,
      'secret' => $secret
    ]);

  }

  public function disableTwoFactor(Request $request)
  {
    $user = $request->user();
    $user->google2fa_secret = null;
    $user->save();
    return view('google2fa.welcome');
  }

  /**
   * Generate a secret key in Base32 format
   *
   * @return string
   */
  private function generateSecret()
  {
    return Google2FA::generateSecretKey();
  }


  // 2fa 페이지 호출
  public function getValidateToken()
  {
    if (session('2fa:user:id')) {
      return view('google2fa.validate');
    }
    return redirect('login');
  }

public function postValidateToken(ValidateSecretRequest $request)
  {
    $userId = $request->session()->pull('2fa:user:id'); // 기존 세션값에서 userId 가져옮
    $key = $userId . ':' . $request->totp; // userId와 입력값(totp: google 2fa 값)을 이용하여 키를 만들어 준다.

    //use cache to store token to blacklist
    \Cache::add($key, true, 4); // 키값을 추가한다.

    $user = \Auth::loginUsingId($userId); // 로그인 처리

    return redirect()->intended('/');
  }
}


// 2fa 페이지 호출
public function getValidateToken()
{
  if (session('2fa:user:id')) {
    return view('auth/2fa-validate');
  }
  return redirect('login');
}

public function postValidateToken(ValidateSecretRequest $request)
  {
    $userId = $request->session()->pull('2fa:user:id'); // 기존 세션값에서 userId 가져옮
    $key = $userId . ':' . $request->totp; // userId와 입력값(totp: google 2fa 값)을 이용하여 키를 만들어 준다.

    //use cache to store token to blacklist
    Cache::add($key, true, 4); // 키값을 추가한다.

    //login and redirect user
    $user = Auth::loginUsingId($userId); // 로그인 처리

    return redirect()->intended('/');
  }

Requests

Http 하위에 Requests 디렉토리를 생성후 아래와 같이 각각 Request.php 및 ValidateSecretRequest.php를 저장한다.

  • Request.php
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
abstract class Request extends FormRequest
{}

  • ValidateSecretRequest.php
<?php

namespace App\Http\Requests;

use Cache;
use Crypt;
use Google2FA;
use App\Models\User;
use App\Http\Requests\Request;
use Illuminate\Validation\Factory as ValidatonFactory;

class ValidateSecretRequest extends Request
{
  /**
   *
   * @var \App\User
   */
  private $user;

  /**
   * Create a new FormRequest instance.
   *
   * @param \Illuminate\Validation\Factory $factory
   * @return void
   */
  public function __construct(ValidatonFactory $factory)
  {
    $factory->extend(
      'valid_token',
      function ($attribute, $value, $parameters, $validator) {
        $secret = Crypt::decrypt($this->user->google2fa_secret);
        return Google2FA::verifyKey($secret, $value);
      },
      'Not a valid token '
    );

    $factory->extend(
      'used_token',
      function ($attribute, $value, $parameters, $validator) {
        $key = $this->user->id . ':' . $value;
        return !Cache::has($key);
      },
      'Cannot reuse token'
    );
  }

  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
  public function authorize()
  {
    try {
      $this->user = User::findOrFail(
        session('2fa:user:id')
      );
    } catch (Exception $exc) {
      return false;
    }

    return true;
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'totp' => 'bail|required|digits:6|valid_token|used_token',
    ];
  }
}

blades

  • enableTwoFactor.blade.php

<p>구글 OTP 앱을 여신 후 아래 이미지를 스캔하세요</p>
<div>
{!! $image !!}
</div>
<p>구글 OTP 앱이 바코드를 인식하지 못하면 아래 숫자를 입력하세요</p>
enter in the following number: <code>{{ $secret }}</code>

  • validate.blade.php
<form method="POST" action="/2fa/validate" >
  @csrf
  <input type="number" name="totp">
  <button type="submit" class="btn btn-primary">
    Validate
  </button>
</form>
평점을 남겨주세요
평점 : 5.0
총 투표수 : 1

질문 및 답글