Firebase謹製のPHP-JWTライブラリを利用した案件が多いからまとめる。
JWTライブラリにも色々種類があるのだ。
もくじ
payload
@see Wikipedia
コード | 名称 | 説明 |
iss | issuer | トークンの発行者 |
sub | Subject | トークンの主題 |
aud | Audience | トークンが意図している受信者の識別子 |
exp | Expiration Time | 有効期限。JWTが失効する日時 |
nbf | Not Before | トークンが有効になる日時 |
iat | issued at | トークンの発行日時 |
jti | JWT ID | 発行者ごとトークンごとに一意な識別子 |
インストール
$ composer require firebase/php-jwt
// docker-composeの場合はこっち
$ docker-compose exec php-fpm composer require firebase/php-jwt
# mkdir jwt_keys # cd jwt_keys
秘密鍵作成
# openssl genrsa -out private.pem 2048
公開鍵作成
# openssl rsa -in private.pem -outform PEM -pubout -out public.pem writing RSA key
# chmod 600 private.pem public.pem
Http/Kernel.php
protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.api' => \App\Http\Middleware\ApiTokenChecker::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ];
アクセストークンの設計
設計例
● Header { "typ": "JWT", // 固定値 "alg": "HS256" // 署名アルゴリズム HS256で良い "kid": "hohhofdohofeh" // key ID } ● Payload { "sub" : “12", // ユーザid "iat": 1356999524, "exp": 1360819380 } ● 有効期限 ・AccessToken = n分後で有効期限 ・RefreshToken = m週間で有効期限
JWTはピリオド2つで繋がったデータ構造になっています。
<Base64エンコードしたHeader>.<Base64エンコードしたClaims>.<Base64エンコードしたSignature>
- ヘッダー(header)
- 属性情報(claim)
- 署名(alg)
備考
- JWTは暗号化されているわけではない。
- JWTの文字列にURLに利用できない文字列を利用してはいけない
Header
{ "alg": "HS256", "typ": "JWT", "kid": "kjfjs0543939acf73be64604d49a097189a" }
kidを含めるとメンテナンス性が向上する。
kidがないと秘密鍵の交換の際に全員がログアウトすることになる。
Payload(ユーザ情報など)
{ “user_id”: “12”, // ユーザid "iat": 1356999524, // 発行日時 "exp": 1360819380 // 有効期限 "kid": "fakjo09jlksfjkslna.nb" // kid(Optional) }
絶対にペイロードにパスワード等の機密情報を含めてはいけない
- idは含める
- kidは秘密鍵が漏洩した際に差し変える為に必要
Signature
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
data = base64urlEncode( header ) + ‘.’ + base64urlEncode( payload )
signature = Hash( data, secret )
data + signature
JWTの中身を見たい場合(デバッガ)
下記のサービスで簡単に確認ができます。
Sample
$user_id = $_GET['user_id']; $password = $_GET['password']; // 認証 if(!is_invalid($user_id, $password)) { return json_encode(array( 'message' => 'Invalid User.' )); } function createAccessToken(int $minutes = 15, string $user_id): array { $current_time = time(); $expire = $current_time + ($minutes * 60); // 15分 $claims = array ( 'user_id' => $user_id, 'iat' => $current_time, 'exp' => $expire, ); /* 秘密鍵の取得 */ $private_key = file_get_contents(__DIR__ . '/keys/jwt.key'); /* エンコード */ $jwt = JWT::encode($claims, $private_key, 'HS256'); return json_encode(array( 'message' => 'Success.', 'jwt' => $jwt )); } function createRefreshToken(int $minutes = 40320, string $user_id): array { $current_time = time(); $expire = $current_time + ($minutes * 60); // デフォルト4週間 $claims = array ( 'user_id' => $user_id, 'iat' => $current_time, 'exp' => $expire, ); /* 秘密鍵の取得 */ $private_key = file_get_contents(__DIR__ . '/keys/jwt.key'); /* エンコード */ $jwt = JWT::encode($claims, $private_key, 'HS256'); return json_encode(array( 'message' => 'Success.', 'jwt' => $jwt )); }
セキュリティでの注意
- alg = noneにして検証を回避できる脆弱性が存在する
1. alg = none
2. ペイロードを改ざん
3. 署名を削除
対策
- algをホワイトリスト形式で制限する
- RS256(公開鍵認証方式)のみ扱うようにする
@see
- laravel firebase/php-jwt token验证
// firebase/php-jwtでの実装が見れる - JWT 入門
- JSON Web Token(JWT)のClaimについて
// Payload設計を見た。 - JSON Web Token (JWT) とは何?
- JWT(JSON Web Token)の「仕組み」と「注意点」
// セキュリティでの注意点の参考 - 🌟WebCrypto APIでJSON Web Tokenの検証を試してみる
- https://jwt.io/introduction/