29
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LaravelAdvent Calendar 2020

Day 4

Laravel(6.x)でのセキュアそうなAPI認証を実装する

Last updated at Posted at 2020-12-04

こんにちは。M2です。
Laravel Advent Calendar 2020の4日目を担当させていただきます。

概要

Laravelのtokenを使ったAPI認証でなるべくセキュアそうな認証方法を実装してみたので、備忘録がてらに共有します。

Laravelパスポートを使いたいところですが、使わない方向です。

環境

  • Laravel 6.x
  • フロント側は(Vue.js 3)だけでど何でも良い

方向性

  • DBにapi_tokenを用意して、それをユーザ認証用のtokenとする
  • DBに保存しているapi_tokenはHash化して保存する
  • ブラウザ側ではapi_tokenを暗号化してCookieに保存する
  • JavaScript側でapi_tokenにアクセスできないように、Laravel側でhttponlyに設定する
  • ずっと同じapi_tokenも嫌なので、ログイン毎に更新する

api_tokenカラムの導入

$ php artisan make:migration add_api_token_to_users_table --table=users
database/migrations/${date}_add_api_token_to_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddApiTokenToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('api_token', 80)->after('password')
                ->unique()
                ->nullable()
                ->default(null);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropColumn('api_token');
        });
    }
}
$ php artisan migrate

DBに保存しているapi_tokenはHash化して保存する

config/auth.php
<?php

return [

~~ 省略 ~~ 

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => true, // ここをtrueにする
        ],
    ],

~~ 省略 ~~ 

ブラウザ側ではapi_tokenを暗号化してCookieに保存する

app/Http/Controllers/Auth/RegisterController.php
  /**
   * Create a new user instance after a valid registration.
   *
   * @param  array  $data
   * @return \App\User
   */
  protected function create(array $data)
  {
    $token = $this->generateApiTokenAndSetCookie();

    return User::create([
.
.
.
      'api_token' => hash('sha256', $token),
    ]);
  }
}
//$this->generateApiTokenAndSetCookie();の中身

use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Cookie;

.
.
.

  public function generateApiTokenAndSetCookie()
  {
    $token = Str::random(80);

    $encryptToken = Crypt::encrypt($token);
    Cookie::queue(Cookie::forget('api_token'));
    Cookie::queue('api_token', $encryptToken, '10000000');

    return $token;
  }

api_tokenの暗号化は手動で設定しておく

app/Http/Middleware/EncryptCookies.php
<?php

namespace App\Http\Middleware;

use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;

class EncryptCookies extends Middleware
{
    /**
     * The names of the cookies that should not be encrypted.
     *
     * @var array
     */
    protected $except = [
        'api_token'
    ];
}

JavaScript側でapi_tokenにアクセスできないように、Laravel側でhttponlyに設定する

# .env
.
.
.

SESSION_SECURE_COOKIE=true

ずっと同じapi_tokenも嫌なので、ログイン毎に更新する

app/Http/Controllers/Auth/LoginController.php
.
.
.
  public function login(Request $request)
  {
.
.
.


    if ($this->attemptLogin($request)) {
      // Cookieのapi_tokenの更新して、DBのapi_tokenも更新する
      $token = $this->generateApiTokenAndSetCookie();
      User::where('email', $request->email)
        ->update(['api_token' => hash('sha256', $token)]);
     
      return $this->sendLoginResponse($request);
    }
.
.
.

  }

いざAPI認証

HogeService.php
// Serviceクラスでも何でも好きなところで...
<?php

namespace App\Services;

use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Cookie;

.
.
.

  public function getUser(string $url): ?string
  {
    $client = new \GuzzleHttp\Client();
    if (Cookie::get('api_token') === null) {
      return null;
    }
    $decryptToken = Crypt::decrypt(Cookie::get('api_token'), true);
    $response = $client->request(
      'GET',
      $url, 
      ['verify' => true] 
    );
    $response = $response->getBody()->getContents();
    $result = json_decode($response);
    if (json_last_error() === JSON_ERROR_NONE) {
      return json_encode($result);
    } else {
      return null;
    }
  }
}

Controllerから上記のAPIにアクセスする

HogeGetUserController.php
<?php
.
.
.
.
use App\Services\AuthApi;

/**
 * @group UserInfo API
 *
 * APIs 会員登録系
 */
class HogeGetUserController extends Controller
{

  protected $authApi;

  public function __construct(AuthApi $authApi)
  {
    $this->authApi = $authApi;
  }

.
.
.
  public function userInfo(Request $request)
  {
    $json = $this->authApi->getUser(route(('api.requestUser')));
    return $json;
  }

  public function requestUser(Request $request)
  {
    return $request->user();
  }

ルーティングの設定

routes/api.php
.
.
.

  Route::get('userInfo', 'HogeGetUserController@userInfo');

  Route::group(['middleware' => 'auth:api'], function () {
    Route::get('requestUser', 'HogeGetUserController@requestUser')->name('api.requestUser');
  });

これで、ログイン後に/api/userInfoにアクセスすれば、Userの情報が取得できる。

Vue.jsから呼び出す

hoge.vue
<template>
.
.
.
</template>

<script lang="ts">
axios.get('/api/userInfo')

</script>

テストする

tests/Feature/UserTest.php
<?php

namespace Tests\Feature;

use App\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use Tests\TestCase;

class UserTest extends TestCase
{

    use DatabaseTransactions;

    /**
     *
     * @return void
     */
    public function test_getユーザー取得()
    {
        $token = Str::random(80);

        $user = factory(User::class)->create([
            'api_token' => hash('sha256', $token),
        ]);

        $response = $this->actingAs($user)->json('GET', "/api/requestUser?api_token={$token}");
        $json = json_decode($response->getContent());

        $this->assertObjectHasAttribute('id', $json);
        $this->assertObjectHasAttribute('name', $json);
        $this->assertObjectHasAttribute('email', $json);
    }

問題点とか

  • JavaScriptからCookieにアクセスできなくても、正しいapi_tokenを叩けば、誰でも認証情報にアクセスできちゃう...
  • ログイン毎にapi_tokenが更新されるとはいえ、誰か良い方法を教えてクレメンス

参考

公式:Laravel 6.x API認証
Laravel6.x + Vue.js2(TypeScript)のSPAでシンプルなAPI認証を実装する

29
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?