LoginSignup
9
8

More than 3 years have passed since last update.

JWTを用いたリクエストパラメーターの改ざん検知のための署名検証実装(Rails)

Last updated at Posted at 2020-07-04

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

JWT
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の有効期限

今回やってみること

リクエストパラメータの改ざん検知のための署名検証の実装になります。
ペイロードにリクエストパラメーターを追加して、そのパラメーターと実際受け取ったパラメーターに改ざんがあるかを検証する実装をやってみます。

payloadにparams追加
  "params": "{\"email\":\"test@gmail.com\",\"password\":\"password\"}",

APIプロジェクト生成

console
$ rails new jwt --api

Gemfileにjwt追加

gem 'jwt'

Jwt署名検証チェックモジュール作成

app/controllers/concerns/signature.rb
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署名検証処理モデル作成

app/models/jwt_signature.rb
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を取得
スクリーンショット 2020-07-04 13.55.00.png

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追加

routes.rb
Rails.application.routes.draw do
  post 'tokens/create'
end

Token Controller作成

tokens_controller.rb
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を設定

unixtime(現在時間から5分後)
irb(main):040:0> (Time.now + 300).to_i
=> 1593838401

スクリーンショット 2020-07-04 13.49.22.png

Postmanで実行してみる

Headerに「jwt-signature」を追加してJwtEncodedデータを追加

JwtEncodedデータ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM4NDAxIn0.dSNqdhHBJKUJHnJa_2sS_3Qr4oNNdr5MKFx5ufwqLv4

スクリーンショット 2020-07-04 13.48.47.png

Bodyにjson形式でemail, password追加

json
{ "email": "test@gmail.com", "password": "password"}

スクリーンショット 2020-07-04 11.47.42.png

実行

署名検証後無事にtokenが取得できました〜
スクリーンショット 2020-07-04 11.50.29.png

同じ状態で5分後実行

expに5分を設定したので5分後には署名検証失敗
スクリーンショット 2020-07-06 19.55.22.png

9
8
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
9
8