テクメモ

備忘録

Laravel+JWTで認証付きAPIをつくる

JWTとは?

JWT = JSON Web Token

特徴としては、

  1. 電子署名がついているため改竄をチェックできる
  2. URL-safeになってる(URLに含むことができる文字のみで構成される)
  3. 実際の中身はJSON
  4. ステートレスであること

で、すごいざっくりいうと改竄ができないURLに埋め込むことができるTokenのこと

4について詳しく書くと トークンを使った認証はサーバー側で認証状態を管理しなく、送られてきたトークンを検証することしかしない。 サーバー側で認証状態をどう扱い、管理していくかという問題を考えずにすむことができるということ。

JWTの構成要素

  1. ヘッダー(Base64文字列)
  2. JSONの中身(Base64文字列)
  3. 電子署名

Base64とは、すべてのデータをアルファベット(a~z, A~z)と数字(0~9)、一部の記号(+,/)の64文字で表すエンコード方式のこと

3つの要素が.で繋がれた形になっている。

{ヘッダー}.{クレームデータ(JSONの中身)}.{電子署名}

ヘッダー

{
  'alg': 'HS256',
  'typ': 'JWT'
}

ヘッダ情報には、署名アルゴリズムの種類やメタ情報が記載されている。

alg:電子署名に使われているアルゴリズムを指定するキー
typ:トークンタイプ。JWT以外使うかは不明

algは、HS256やRS256などがありますが、RS256を使用すると脆弱性があるようです。

RS256 でTOKENを発行する仕組みになっている場合、攻撃者が HS256 のアルゴリズムで公開鍵(利用用途的に公開されているので誰でも取得可能)を用いて TOKEN を発行するとチェックする際に HS256 を用いてチェックされ KEY は同じ公開鍵を利用するためチェックが通ってしまうことになり、改ざんが可能になるということみたいです。

※ 参考 JWTライブラリの危機的な脆弱性について調べた - Qiita

クレームデータ

JSONの中身が入っている。デコードするだけで内容を確認できる。
※デコードするだけで中身が全部見れてしまうので、見えてはいけない情報を含めるのは回避すべき

予約済みのキー名が存在している。以下はその一覧

キー名 役割
exp 期限切れ日時
ndf 期間開始日時
iat JWT発行日時
iss 発行者(サーバ側)の識別子
aud 利用する側(クライアント側)の識別子

電子署名

電子署名部分はデータ改竄されているかどうかをチェックする際に使われている。

JWT認証の流れ

  1. クライアントが認証情報(user_idやpassword)を送信する。
  2. サーバが認証情報を取得してuser_idやexpを含む、JSON秘密鍵で暗号化してJWTとして返却する。
  3. クライアントはこのJWTを使って通常のAPI リクエストを行なう。
  4. サーバは、秘密鍵を使ってJWTを検証し、ログインIDをJSONから取り出し、処理を行なう。

クライアントから送信されるJWTはサーバで秘密鍵を使って改竄が行われたかを検証することができる為、user_idexpの改竄を検知する事ができる。

LaravelでのJWTの使い方

認証機能を導入

make:authコマンドでユーザー登録、ログイン、パスワードリセットのベースを構築することができる。

php artisan make:auth

上記コマンドでマイグレーションファイルも作ってくれるのでmigrationをする

php aritsan migrate

composerでjwt-authをインストール

まずはjwt-authを入れる。

composer require tymon/jwt-auth 1.0.0-rc3

※Laravel5.5以上は、1.0以上のjwt-authが必要なのでバージョンを指定する

次にjwt-authの初期設定を行う

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

これを行うことで、/config/配下にjwt.phpが作成されます。

php artisan vendor:publishを何故行うのか
JWTAuthが持つconfigファイルは、vendor配下に設置されている。直接このファイルを変更しても今後パッケージが更新されると変更内容が失われてしまう。
そのため、このようなconfigファイルはPublish(内容を変更してもいいディレクトリにコピー)して使用する。
php artisan vendor:publishを行うことで/config配下にjwt.phpという形で変更可能なconfigファイルが作成される。

jwt-authが使えるシークレットキーを生成する。

php artisan jwt:secret

.envに以下のように行が追加されたか確認する

JWT_SECRET=XXXXXXX

jwt-authの設定

以下のようにconfig\app.phpのproviders配列とaliases配列にjwt-authを登録する。

config/app.php

'providers' => [

...

  Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
],

'aliases' => [

...

  'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class,
  'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class,
],

Guardを修正

guardは認証を管理するドライバークラスです。 apiでjwt-authを使うのでdriver:jwtに変えます。

'guards' => [
...
        'api' => [
            'driver' => 'jwt',
            'provider' => 'users',
        ],
    ],

Userモデルを編集

Userモデルを以下のように編集する。

<?php

namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier() {
        return $this->getKey();
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

Laravelの認証機能を使うためにAuthenticatableを継承し、jwt-authのインタフェースをJWTSubjectを通じて実装する。 getJWTIdentifierはjwtのトークンを取得する処理。
getJWTCustomClaimsはクレームデータ(jwtの中身)に情報を追加したい時に使用する。

認証用コントローラの作成

認証用のコントローラAuthControllerを作成する。

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;

class AuthController extends Controller
{
    public function login(request $request) {
        $credentials = $request->only('email', 'password');
        try {
            if (! $token = JWTAuth::attempt($credentials)) {
                return response()->json(['error' => 'Unauthorized'], 401);
            }
        } catch (JWTException $e) {
            return response()->json(['error' => 'could_not_create_token'], 500);
        }

        return response()->json(compact('token'));
    }

    public function me()
    {
        $user = JWTAuth::parseToken()->authenticate();
        return response()->json(compact('user'));
    }
}

loginはリクエストからemailとpasswordをとってJWTAuth::attemptメソッドで認証し、 失敗した場合は401、成功した場合はトークンを発行して返す処理。 meはJWTを付与してリクエストすると、ユーザー情報を返す処理。

最後に、ルーティングを定義する。

$router = app('Illuminate\Routing\Router');

$router->group(['middleware' => ['guest:api']], function () use ($router) {
    $router->post('/login', 'AuthController@login')->name('auth-login');
});
$router->group(['middleware' => ['auth:api']], function () use ($router) {
    $router->get('/me', 'AuthController@me')->name('auth-me');
});

以上で、Laravel+JWTで認証付きAPIを実装することができる。

参考文献

qiita.com