[RUBY] Signature verification implementation (Rails) for detecting tampering with request parameters using JWT

What is JWT

Abbreviation for JSON Web Token.

Please refer to the link below for details. https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html

JWT configuration

It has three parts, a header, a payload, and a signature, each encoded in Base64.

header


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

payload


{
  "sub": "1234567890",
  "iss": "John Doe",
  "aud": "audience",
  "exp": 1353604926
}

signature


HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

The three parts are connected by a. (Dot).

If you want to easily create a header, payload, and proof, you can do it with the link below. https://jwt.io/#debugger

JWT


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM2ODA2In0.4fC4yLEmYTjiwaXk3R_AUUPEQSuI_ARmkoMqosWEJ-c

JWT claim

A JWT claim set is a JSON object, each member of which is a claim sent as a JWT. The claim name in the JWT claim set must be unique. This time, I will use the following four claims.

"iss" (Issuer) Claim: JWT issuer identifier "sub" (Subject) Claim: Identifier of the subject that is the subject of the JWT "aud" (Audience) Claim: List of identifiers of entities that are supposed to use JWT "exp" (Expiration Time): JWT expiration date

Try this time

It is an implementation of signature verification for detecting tampering with request parameters. Let's try an implementation that adds a request parameter to the payload and verifies that the parameter and the parameter actually received have been tampered with.

Added params to payload


  "params": "{\"email\":\"[email protected]\",\"password\":\"password\"}",

API project generation

console


$ rails new jwt --api

Add jwt to Gemfile

gem 'jwt'

Jwt signature verification check module creation

app/controllers/concerns/signature.rb


module Signature
  extend ActiveSupport::Concern

  def verify_signature
    render status: 401, json: { message: 'Tampering was found.'} 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: 'Tampering was found.'}
  end
end

Jwt signature verification processing model creation

app/models/jwt_signature.rb


class JwtSignature
  class InvalidSignature < StandardError; end
  ALGORITHM = 'HS256'
  ISSUER = 'user'
  AUDIENCE = 'audience'
  SUB = "test"
  TOKEN_TYPE = 'JWT'
  # SECRET_KEY is important, so define it for each environment and manage it safely ~
  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

Because JWT.decode returns a Hash Array Get Payload with decoded_jwt.first and Header with decoded_jwt.second スクリーンショット 2020-07-04 13.55.00.png

User Table creation

app / models / user.rb is generated

$ rails g model User name:string email:string token:string expired_at:datetime
$ rails db:migrate

API added to routes.rb

routes.rb


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

Create Token Controller

tokens_controller.rb


class TokensController < ActionController::API
  include Signature

  #Check signature verification only when creating
  before_action :verify_signature, only: %i(create)

  def create
    #This time only the signature verification process is implemented
    user = User.find_by(email: params[:email], password: params[:password])

    return render status: 400, json: { message: 'User does not exist.' } unless user

    #Update if Token does not exist
    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

Test user registration

$rails c

irb(main):001:0> User.new(name: 'test', email: '[email protected]', password: 'password')
   (0.5ms)  SELECT sqlite_version(*)
=> #<User id: nil, name: "test", email: "[email protected]", password: [FILTERED], token: nil, expired_at: nil, created_at: nil, updated_at: nil>
irb(main):002:0> User.new(name: 'test', email: '[email protected]', 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", "[email protected]"], ["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 data generation

https://jwt.io/#debugger Generate JwtEncoded data from the link above Payload part adds Jwt claims and parameters you need Add email and password to params and compare API parameter Json with Payload Json Change the Security key of VERIFY SIGNATURE to your own Security (using SECRET_KEY of JwtSignature model) If you want to set the maturity time, set Unix to exp

unixtime(5 minutes after the current time)


irb(main):040:0> (Time.now + 300).to_i
=> 1593838401

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

Try running with Postman

Add "jwt-signature" to Header and add JwtEncoded data

JwtEncoded data


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM4NDAxIn0.dSNqdhHBJKUJHnJa_2sS_3Qr4oNNdr5MKFx5ufwqLv4

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

Add email and password to Body in json format

json


{ "email": "[email protected]", "password": "password"}

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

Run

After verifying the signature, I was able to get the token safely ~ スクリーンショット 2020-07-04 11.50.29.png

Run after 5 minutes in the same state

Since 5 minutes was set for exp, signature verification failed after 5 minutes. スクリーンショット 2020-07-06 19.55.22.png

Recommended Posts

Signature verification implementation (Rails) for detecting tampering with request parameters using JWT
Using Material Design for Bootstrap with Rails 5.2
Using PAY.JP API with Rails ~ Implementation Preparation ~ (payjp.js v2)
Try using view_component with rails
Japaneseize using i18n with Rails
Preparation for developing with Rails
Login function implementation with rails
[Rails] Implementation of coupon function (with automatic deletion function using batch processing)