[Ruby] Implementation of signature verification for detecting alteration of request parameters using JWT (Rails)

3 minute read

What is #JWT Abbreviation for JSON Web Token.

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

JWT configuration

The header, payload, and signature are made up of three parts, 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 joined by a. (Dot).

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

JWT


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM2ODA2In0.4fC4yLEmYTjiwaXk3R_AUUPEQSuI_ARmkoMqosWEJ-c

JWT Claim

The JWT claim set is a JSON object and each member is a claim sent as a JWT. Claim names within 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 JWT “aud” (Audience) Claims: List of identifiers of the subjects who are supposed to use JWT “exp” (Expiration Time): JWT expiration date

What to do this time

Implementation of signature verification for detection of tampering with request parameters. Let’s try adding an request parameter to the payload and verifying that the parameter and the one actually received are tampered with.

Add 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 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 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

Since JWT.decode returns an Array of Hash Payload with decoded_jwt.first and Header with decoded_jwt.second Screenshots 2020-07-04 13.55.00.png

Create User Table

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

Token Controller creation

tokens_controller.rb


class TokensController <ActionController::API
  include Signature

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

  def create
# Only the signature verification process is implemented this time
    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 generationhttps://jwt.io/#debugger

Generate JwtEncoded data from the above link Payload part add Jwt claims and parameters that you need Adding email and password to params and comparing API parameter Json and Payload Json Security key of VERIFY SIGNATURE also changed to your own security (using SECRET_KEY of JwtSignature model) If you want to set the expiration time, set Unixtime to exp

unixtime (5 minutes after the current time)


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

![Screenshot 2020-07-04 13.49.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108475/272109d1-05c8-385e-1361-(a2813eeccae8.png)

Run with # Postman Add “jwt-signature” to Header and add JwtEncoded data

JwtEncoded data


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyIiwic3ViIjoidGVzdCIsImF1ZCI6ImF1ZGllbmNlIiwicGFyYW1zIjoie1wiZW1haWxcIjogXCJ0ZXN0QGdtYWlsLmNvbVwiLCBcInBhc3N3b3JkXCI6IFwicGFzc3dvcmRcIn0iLCJleHAiOiIxNTkzODM4NDAxIn0.dSNqdhHBJKUJHnJa_2sS_3Qr4oNNdr5MKFx5ufwqLv4

![Screenshot 2020-07-04 13.48.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108475/6c36ded2-abc3-bbbd-9a65-(c95718a92a9d.png)

Add email and password in json format to Body

json


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

Screenshot 2020-07-04 11.47.42.png

execute

I got the token safely after verifying the signature ~ ![Screenshot 2020-07-04 11.50.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108475/10686ae7-b182-dc25-8a5b-(ccd22ce35234.png)

Run after 5 minutes in the same state

Since exp is set to 5 minutes, signature verification fails after 5 minutes ![Screenshot 2020-07-06 19.55.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108475/38b7c50f-a955-b1d3-f7b3-(159cbd019986.png)