#JWTとは
JSON Web Tokenの略称です。
詳細内容については以下のリンクを参考にしてください。
https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html
##JWT構成
ヘッダ、ペイロード、署名の3つのパートになっててそれぞれBase64でエンコードされている。
{
"typ":"JWT",
"alg":"HS256"
}
{
"sub": "1234567890",
"iss": "John Doe",
"aud": "audience",
"exp": 1353604926
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
3つのパートは . (ドット) で結合されている。
ヘッダ・ペイロード・証明を簡単に作ってみたいなら以下のリンクでできます。
https://jwt.io/#debugger
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM2ODA2In0.4fC4yLEmYTjiwaXk3R_AUUPEQSuI_ARmkoMqosWEJ-c
##JWTクレーム
JWT クレームセットは JSON オブジェクトであり, それぞれのメンバは JWT として送られるクレームである. JWT クレームセット内のクレーム名は一意でなければならない
今回は以下の4つのクレームを使います。
"iss" (Issuer) クレーム:JWTの発行者の識別子
"sub" (Subject) クレーム:JWTの主語となる主体の識別子
"aud" (Audience) クレーム:JWTを利用することが想定された主体の識別子一覧
"exp" (Expiration Time):JWTの有効期限
#今回やってみること
リクエストパラメータの改ざん検知のための署名検証の実装になります。
ペイロードにリクエストパラメーターを追加して、そのパラメーターと実際受け取ったパラメーターに改ざんがあるかを検証する実装をやってみます。
"params": "{\"email\":\"test@gmail.com\",\"password\":\"password\"}",
#APIプロジェクト生成
$ rails new jwt --api
##Gemfileにjwt追加
gem 'jwt'
##Jwt署名検証チェックモジュール作成
module Signature
extend ActiveSupport::Concern
def verify_signature
render status: 401, json: { message: '改ざんが見つかりました。'} if request.headers['jwt-signature'].blank?
request_params = JSON.parse(request.body.read)
@signature ||= JwtSignature.new(jwt: request.headers['jwt-signature'])
@signature.verify!(params: request_params)
rescue JwtSignature::InvalidSignature
render status: 401, json: { message: '改ざんが見つかりました。'}
end
end
##Jwt署名検証処理モデル作成
class JwtSignature
class InvalidSignature < StandardError; end
ALGORITHM = 'HS256'
ISSUER = 'user'
AUDIENCE = 'audience'
SUB = "test"
TOKEN_TYPE = 'JWT'
# SECRET_KEYは重要なので環境ごとに定義して安全に管理してください〜
SECRET_KEY = '1gCi6S9oaleH22KWaXyXZAQccBx4lUQi'
def initialize(jwt:)
@jwt = jwt
end
def verify!(params:)
raise InvalidSignature unless valid_payload? && valid_params?(params: params) && valid_header?
end
private
def valid_payload?
return false unless jwt_payload['iss'] == ISSUER
return false unless jwt_payload['sub'] == SUB
return false unless jwt_payload['aud'] == AUDIENCE
true
end
def valid_header?
return false unless jwt_header['alg'] == ALGORITHM
return false unless jwt_header['typ'] == TOKEN_TYPE
true
end
def valid_params?(params:)
JSON.parse(jwt_payload['params']) == params
end
def jwt_header
@jwt_header ||= decoded_jwt.second
end
def jwt_payload
@jwt_payload ||= decoded_jwt.first
end
def decoded_jwt
@decoded_jwt ||= JWT.decode(@jwt, SECRET_KEY, true, algorithm: ALGORITHM)
rescue JWT::DecodeError
raise InvalidSignature
end
end
JWT.decodeはHashのArrayを返すので
decoded_jwt.firstでPayload、decoded_jwt.secondでHeaderを取得
User Table作成
app/models/user.rbが生成される
$ rails g model User name:string email:string token:string expired_at:datetime
$ rails db:migrate
routes.rbにAPI追加
Rails.application.routes.draw do
post 'tokens/create'
end
Token Controller作成
class TokensController < ActionController::API
include Signature
# createの時だけ署名検証を確認する
before_action :verify_signature, only: %i(create)
def create
# 今回は署名検証処理だけ実装
user = User.find_by(email: params[:email], password: params[:password])
return render status: 400, json: { message: 'ユーザーが存在しません。' } unless user
# Tokenが存在しない場合は更新
if user.token.blank?
user.token = SecureRandom.uuid
user.save
end
render status: 200, json: { name: user.name, email: user.email, token: user.token }
end
end
テストユーザー登録
$rails c
irb(main):001:0> User.new(name: 'test', email: 'test@gmail.com', password: 'password')
(0.5ms) SELECT sqlite_version(*)
=> #<User id: nil, name: "test", email: "test@gmail.com", password: [FILTERED], token: nil, expired_at: nil, created_at: nil, updated_at: nil>
irb(main):002:0> User.new(name: 'test', email: 'test@gmail.com', password: 'password').save
(0.1ms) begin transaction
User Create (0.9ms) INSERT INTO "users" ("name", "email", "password", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["name", "test"], ["email", "test@gmail.com"], ["password", "password"], ["created_at", "2020-07-04 02:30:17.701626"], ["updated_at", "2020-07-04 02:30:17.701626"]]
(0.8ms) commit transaction
=> true
#JwtEncodedデータ生成
https://jwt.io/#debugger
上記のリンクからJwtEncodedデータを生成
Payload部分は自分が必要なJwtクレームとパラメータを追加
paramsにemail, passwordを追加してAPIのパラメーターのJsonとPayloadのJsonを比較している
VERIFY SIGNATUREのSecurityキーも自分のSecurityに変更(JwtSignatureモデルのSECRET_KEY使用)
満期時間を設定したい場合はexpにUnixtimeを設定
irb(main):040:0> (Time.now + 300).to_i
=> 1593838401
Postmanで実行してみる
Headerに「jwt-signature」を追加してJwtEncodedデータを追加
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM4NDAxIn0.dSNqdhHBJKUJHnJa_2sS_3Qr4oNNdr5MKFx5ufwqLv4
Bodyにjson形式でemail, password追加
{ "email": "test@gmail.com", "password": "password"}