JWT認証のお勉強
[秋葉原] ゆるふわLaravel勉強会 (認証/JWT)
JWT担当資料
JWT認証の特徴
- APIアプリケーションでよく使われる
- OAuthクライアントでの認証・認可などに
- トークンそのものを検証することで、フロントエンド側で認証可否を判断できる
- Laravel標準のトークン認証よりもセキュア
- なぜ?
- 中身は平文
- 署名により、「改ざんされていない」ことが担保されるだけ (完全性)
- base64urlエンコードに暗号強度はない
- javascriptの
atob
関数とかで普通に復号して覗ける
中身を覗ける例
- 手元にあったJWTを採用しているWebアプリケーションで認証したときの返却トークン
{"message":"You are successfully logged in!","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3QvYXBpL2F1dGgvbG9naW4iLCJpYXQiOjE1NTQwOTkzMzgsImV4cCI6MTU1NDEwMjkzOCwibmJmIjoxNTU0MDk5MzM4LCJqdGkiOiJvSnEyZmxPa01WRTRPcnFCIn0.2r1klvbpZx0oe_n0mZzxDJh6R00Dt4S5ivKvf0FKfOs"}
.
区切りで分割
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 eyJzdWIiOjEsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3QvYXBpL2F1dGgvbG9naW4iLCJpYXQiOjE1NTQwOTkzMzgsImV4cCI6MTU1NDEwMjkzOCwibmJmIjoxNTU0MDk5MzM4LCJqdGkiOiJvSnEyZmxPa01WRTRPcnFCIn0 2r1klvbpZx0oe_n0mZzxDJh6R00Dt4S5ivKvf0FKfOs
- それぞれjavascriptの
atob
関数でbase64デコード
{"typ":"JWT","alg":"HS256"} {"sub":1,"iss":"http://localhost/api/auth/login","iat":1554099338,"exp":1554102938,"nbf":1554099338,"jti":"oJq2flOkMVE4OrqB"} (署名バイナリ)
- 「このトークンはいつ有効期限が切れる(expire)」
とかがフロントエンドのJavaScriptだけでわかる
JWTの仕様をちょっと追ってみる
JWTヘッダ
JWTに適用される暗号化オペレーションについて記述しているJSONテキストオブジェクト. JWTがデジタル署名もしくはMACされている場合, JWTヘッダはJWSヘッダである. JWTが暗号化されている場合, JWTヘッダはJWEヘッダである.
- Signature -> JWS
- Encryption -> JWE かな?
例
{"typ":"JWT", "alg":"HS256"}
エンコード済JWTヘッダ
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
- JWTヘッダをBase64Urlエンコードしたもの
- もちろんこれ自体に暗号強度はない
JWTクレームセット
- JSONテキストオブジェクト
- 含クレーム
クレーム
- ある主体に関するひとまとまりの情報
- 名前-値ペアで表現される
クレーム名
- 任意の文字列値
- ただし、予約されているものあり
クレーム値
- 任意のJSON値
例
{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}
- 上のまるごとがJWTクレームセット
"iss":"joe"
とかがクレームiss
がクレーム名joe
がクレーム値
JWTペイロード
- JWTクレームセットのUTF-8表現でのオクテット配列
const str = `{"iss":"joe", "exp":1300819380, "http://example.com/is_root":true}` Array.from(str).map(ch => ch.charCodeAt(0))
[123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125]
エンコード済JWTペイロード
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
エンコード済JWT署名
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
完全なJWT
LaravelでJWTを利用する
tymon/jwt-authのすすめ
- Laravel 5.5 LTS時点で、JWT認証は標準で用意されていないので、ドライバを用意する必要がある
- Tymonさんのをつかうと良い
導入
composer require tymon/jwt-auth 1.0.0-rc2
- もっと新しいのあるかも
設定
- 設定ファイルをconfig/配下に追加
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
- 秘密鍵の生成
- 署名の発行に使う
php artisan jwt:secret
.env
にJWT_SECRET
が追記される
JWT_SECRET=vxMaKy1o...
Userクラス側のJWT認証対応
EloquentモデルのApp\User
でも、自前のクラスでも必要
Tymon\JWTAuth\Contracts\JWTSubject
インタフェースをimplementする- 古いバージョンだと無かったりする
"tymon/jwt-auth": "^0.5.12"
では無かった
getJWTIdentifier()
、getJWTCustomClaims()
を実装する
- 古いバージョンだと無かったりする
vendor/tymon/jwt-auth/src/Contracts
<?php /* * This file is part of jwt-auth. * * (c) Sean Tymon <tymon148@gmail.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Tymon\JWTAuth\Contracts; interface JWTSubject { /** * Get the identifier that will be stored in the subject claim of the JWT. * * @return mixed */ public function getJWTIdentifier(); /** * Return a key value array, containing any custom claims to be added to the JWT. * * @return array */ public function getJWTCustomClaims(); }
JWTSubject@getJWTIdentifier(): mixed
- ユーザーを特定できる一意の値を返却するように実装する
- RDB上の主キーを返すとか
- JWTクレームセットの
sub
(Subject)クレームになるっぽい
JWTSubject@getJWTCustomClaims(): array
JWT認証のガードの設定
config/auth.php
'defaults' => [ - 'guard' => 'web', + 'guard' => 'api', 'passwords' => 'users', ], 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'api' => [ - 'driver' => 'token', + 'driver' => 'jwt', 'provider' => 'users', ], ],
- JWT認証はAPI認証の一種
\Auth::guard('api')->attempt()
というように使いたいので、api
guardでjwt
ドライバを使うようにする- さらに
\Auth::attempt()
というように、デフォルトでapi
guardを使うにはdefaults.guard
をapi
にする
トークンを返す処理
- 下記、時間なく動作未検証
<?php Route::group(['middleware' => 'api'], function () { Route::post('/login', function (Request $request) { /** @var JWTGuard */ $guard = \Auth::guard('api'); /** @var false|string */ $token = $guard->attempt([ 'email' => $request->get('email'), 'password' => $request->get('password'), ]); return response()->json([ 'message' => 'successfully logged in!', 'token' => $token ]); }); Route::get('/users', function (Request $request) { return \Auth::user(); })->middleware('auth:api'); });
【コードリーディング】jwt
ドライバの登録はどこで行われている?
こういうのが気になって夜眠れなくなるのです(うそ)
- tymon/jwt-authのサービスプロバイダがある
- vendor/tymon/jwt-auth/src/Providers/LaravelServiceProvider.php
<?php namespace Tymon\JWTAuth\Providers; class LaravelServiceProvider extends AbstractServiceProvider { /** * {@inheritdoc} */ public function boot() { $path = realpath(__DIR__.'/../../config/config.php'); $this->publishes([$path => config_path('jwt.php')], 'config'); $this->mergeConfigFrom($path, 'jwt'); $this->aliasMiddleware(); $this->extendAuthGuard(); }
Tymon\JWTAuth\Providers\AbstractServiceProvider@extendAuthGuard()
を呼んでいる
<?php /** * Extend Laravel's Auth. * * @return void */ protected function extendAuthGuard() { $this->app['auth']->extend('jwt', function ($app, $name, array $config) { $guard = new JwtGuard( $app['tymon.jwt'], $app['auth']->createUserProvider($config['provider']), $app['request'] ); $app->refresh('request', $guard, 'setRequest'); return $guard; }); }
Illuminate\Auth\AuthManager@extend
を呼び、jwt
ドライバを登録している
<?php /** * Register a custom driver creator Closure. * * @param string $driver * @param \Closure $callback * @return $this */ public function extend($driver, Closure $callback) { $this->customCreators[$driver] = $callback; return $this; }
- ここで登録されたカスタムドライバがいつ呼ばれるかというと、
Illuminate\Auth\AuthManager@guard
が呼ばれたとき - つまり、
\Auth::guard('api')
とか呼んだとき \Auth::attempt()
とかを呼んで、暗黙裡に\Auth::guard()
(デフォルトguard使用)が呼ばれたときもそう
<?php /** * Attempt to get the guard from the local cache. * * @param string $name * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard */ public function guard($name = null) { $name = $name ?: $this->getDefaultDriver(); return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name); } /** * Resolve the given guard. * * @param string $name * @return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard * * @throws \InvalidArgumentException */ protected function resolve($name) { $config = $this->getConfig($name); if (is_null($config)) { throw new InvalidArgumentException("Auth guard [{$name}] is not defined."); } if (isset($this->customCreators[$config['driver']])) { return $this->callCustomCreator($name, $config); } $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; if (method_exists($this, $driverMethod)) { return $this->{$driverMethod}($name, $config); } throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined."); }
フロントエンド側(JavaScript)
Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3QvYXBpL2F1dGgvbG9naW4iLCJpYXQiOjE1NTQwOTkzMzgsImV4cCI6MTU1NDEwMjkzOCwibmJmIjoxNTU0MDk5MzM4LCJqdGkiOiJvSnEyZmxPa01WRTRPcnFCIn0.2r1klvbpZx0oe_n0mZzxDJh6R00Dt4S5ivKvf0FKfOs