JWT with Rails

user authorization

채민균 / 18.11.14(wed)

정보 출처 : [https://velopert.com/2389, https://jwt.io, https://github.com/jwt/ruby-jwt]

JWT 란?

JSON Web Token(https://jwt.io/)

  • 웹표준
    • RFC 7519 를 따른다.
  • 자가수용적이다.
    • 필요한 모든 정보를 자체적으로 지닌다.
  • 수많은 프로그래밍 언어들이 지원한다.

언제쓸까?

  • 회원인증
    • 유저가 서버에 요청을 할 때마다 jwt 와 함께 요청을 하면, 해당 토큰이 유효한지를 판단하여 작업을 처리할 수 있다.
  • 정보교류
    • jwt에 정보를 담아서 두 개체간에 교류하면 정보를 보낸이나 정보의 내용이 바뀌지는 않았는지 검증할 수 있어서 안전하다.

어떻게 생겼을까?

  • jwt 는 헤더(header), 내용(payload), 서명(signature) 3가지로 이루어져 있다. 
  • aaa.bbb.ccc 의 형태를 띈다.

aaa(header)

  • 타입과 해싱 알고리즘 두 가지 정보를 갖고 있다.
{
  "typ": "JWT",
  "alg": "HS256"
}
  • 여기에 담는 정보의 한 '조각'을 claim 이라고 부른다. 클레임의 종류는 등록된 클레임, 공개 클레임, 비공개 클레임 3가지 종류가 있다.
  • 등록된 클레임 종류

bbb(payload)

iss: 토큰 발급자 (issuer)

sub: 토큰 제목 (subject)

aud: 토큰 대상자 (audience)

exp: 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1530839567771) 언제나 현재 시간보다 이후로 설정되어있어야한다.

nbf: Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.

iat: 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있다.

jti: JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용됩니다. 일회용 토큰에 사용하면 유용하다.

  • 여기에 담는 정보의 한 '조각'을 claim 이라고 부른다. 클레임의 종류는 등록된 클레임, 공개 클레임, 비공개 클레임 3가지 종류가 있다.
  • 공개 클레임들은 충돌이 방지된 이름을 갖고 있어야한다. 충돌을 방지하기 위해서 URI형식으로 짓는다.

bbb(payload)

{
    "https://velopert.com/jwt_claims/is_admin": true
}
  • 여기에 담는 정보의 한 '조각'을 claim 이라고 부른다. 클레임의 종류는 등록된 클레임, 공개 클레임, 비공개 클레임 3가지 종류가 있다.
  • 비공개 클레임은 서버와 클라이언트 간에 협의하에 사용되는 정보이다. 공개 클레임과는 달리 이름이 중복될 수 있다는 점에 주의한다.

bbb(payload)

{
    "user_id": 91247
}
  • 예제 payload

bbb(payload)

{
    "iss": "nbt.com",
    "exp": "1485270000",
    "https://velopert.com/jwt_claims/is_admin": true,
    "user_id": "117102",
    "username": "mingyun"
}
  • 헤더의 인코딩값과, 정보의 인코딩값을 합친후 주어진 비밀키로 해쉬를 하여 생성한다.

ccc(signature)

Rails 에 jwt 활용하기

회원인증에 필요한 것.

  • 토큰(인증용)
  • 레프레쉬 토큰(토큰 재발급용)
  • 발급자(등록된 클레임 iss)
  • 만료시간(등록된 클레임 exp)

Rails 에 jwt 활용하기

  • gemfile 에 다음을 추가.
# gem for jwt token
gem 'jwt'

Rails 에 jwt 활용하기

class TokenManager < ApplicationRecord
  # User model 참고. 
  #
  # class User < ApplicationRecord
  #  has_one :token_manager
  # end

  belongs_to :user

  def self.new_token(user_id)
    hmac_secret = 'my$ecretK3y'
    payload = {exp: (Time.now + 30.minutes).to_i,
               iss: 'mingyun.com',
               type: 'token',
               user_id: user_id}
    JWT.encode(payload, hmac_secret, 'HS256')
  end

end

# result : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDE5MzAyMDksImlhdCI6MTU0MTkyODQwOSwiaXNzIjoibWluZ3l1bi5jb20iLCJ0eXBlIjoidG9rZW4iLCJ1c2VyX2lkIjoxM30.ppENQL4g8vAoZa-2skqeSH5TDo-5Ie4yf8oNdIyFqI8"
$ rails g model TokenManager

1.토큰 생성하기

Rails 에 jwt 활용하기

class TokenManager < ApplicationRecord
  belongs_to :user

  HMAC_SECRET = 'my$ecretK3y'
  ISS = 'mingyun.com'
  REFRESH_TOKEN = 'refresh_token'
  TOKEN = 'token'
  ALGORITHM = 'HS256'

  # TokenManager migration file 참고.
  #
  # class CreateTokenManagers < ActiveRecord::Migration[5.2]
  #   def change
  #     create_table :token_managers do |t|
  #       t.text :refresh_token, comment: 'user의 token을 재발급받기 위한 토큰'
  #       t.belongs_to :user, index: true, comment: '외래키'
  #       t.timestamps
  #     end
  #   end
  # end

  def self.new_refresh_token(user_id)
    payload = {exp: exp(REFRESH_TOKEN),
               iss: ISS,
               type: REFRESH_TOKEN,
               user_id: user_id}
    refresh_token = JWT.encode payload, HMAC_SECRET, ALGORITHM

    token_manager = find_or_initialize_by(user_id: user_id)
    token_manager.refresh_token = refresh_token
    token_manager.save

    refresh_token
  end

  def self.new_token(user_id)
    payload = {exp: exp(TOKEN),
               iss: ISS,
               type: TOKEN,
               user_id: user_id}
    JWT.encode(payload, HMAC_SECRET, ALGORITHM)
  end

  private

  def self.exp(type)
    if type == REFRESH_TOKEN
      (Time.now + 1.year).to_i
    elsif type == TOKEN
      (Time.now + 30.minutes).to_i
    end
  end

end

2.리프레쉬 토큰 생성하기

Rails 에 jwt 활용하기

class TokenManager < ApplicationRecord
  belongs_to :user

  HMAC_SECRET = 'my$ecretK3y'
  ISS = 'mingyun.com'
  REFRESH_TOKEN = 'refresh_token'
  TOKEN = 'token'
  ALGORITHM = 'HS256'

  def self.usable?(token)
    return false if token.blank?

    begin
      JWT.decode(token, HMAC_SECRET, true, {iss: ISS, verify_iss: true, algorithm: ALGORITHM})
      true
    rescue JWT::InvalidIssuerError, JWT::ExpiredSignature, JWT::InvalidIatError
      false
    end
  end
  
  def self.new_token(user_id)
    payload = {exp: exp(TOKEN),
               iss: ISS,
               type: TOKEN,
               user_id: user_id}
    JWT.encode(payload, HMAC_SECRET, ALGORITHM)
  end

  def self.new_refresh_token
    payload = {exp: exp(REFRESH_TOKEN),
               iat: iat,
               iss: ISS,
               type: REFRESH_TOKEN,
               user_id: user_id}
    refresh_token = JWT.encode payload, Rails.application.config.hmac_secret, ALGORITHM

    token_manager = find_or_initialize_by(user_id: user_id)
    token_manager.refresh_token = refresh_token
    token_manager.save

    refresh_token
  end

  private

  def self.exp(type)
    if type == REFRESH_TOKEN
      (Time.now + 1.year).to_i
    elsif type == TOKEN
      (Time.now + 30.minutes).to_i
    end
  end

end

3. 토큰인증

Rails 에 jwt 활용하기

class TokenManager < ApplicationRecord
  belongs_to :user

  HMAC_SECRET = 'my$ecretK3y'
  ISS = 'mingyun.com'
  REFRESH_TOKEN = 'refresh_token'
  TOKEN = 'token'
  ALGORITHM = 'HS256'

  # decode result
   # [
   # [0] {
   #          "exp" => 1541932110,
   #          "iss" => "mingyun.com",
   #          "type" => "token",
   #          "user_id" => 15
   #     },
   # [1] {
   #          "typ" => "JWT",
   #          "alg" => "HS256"
   #     }
   # ] 

  def self.user_id(token)
    return nil if token.blank?

    if usable? token
      decoded_token = JWT.decode(token, HMAC_SECRET, true, {algorithm: ALGORITHM})
      decoded_token[0]['user_id']
    else
      nil
    end
  end

  def self.usable?(token)
    return false if token.blank?

    begin
      JWT.decode(token, HMAC_SECRET, true, {iss: ISS, verify_iss: true, algorithm: ALGORITHM})
      true
    rescue JWT::InvalidIssuerError, JWT::ExpiredSignature
      false
    end
  end
  
  def self.new_token(user_id)
    payload = {exp: exp(TOKEN),
               iss: ISS,
               type: TOKEN,
               user_id: user_id}
    JWT.encode(payload, HMAC_SECRET, ALGORITHM)
  end

  def self.new_refresh_token
    payload = {exp: exp(REFRESH_TOKEN),
               iat: iat,
               iss: ISS,
               type: REFRESH_TOKEN,
               user_id: user_id}
    refresh_token = JWT.encode payload, HMAC_SECRET, ALGORITHM

    token_manager = find_or_initialize_by(user_id: user_id)
    token_manager.refresh_token = refresh_token
    token_manager.save

    refresh_token
  end

  private

  def self.exp(type)
    if type == REFRESH_TOKEN
      (Time.now + 1.year).to_i
    elsif type == TOKEN
      (Time.now + 30.minutes).to_i
    end
  end

end

4. token 으로 부터 payload 뽑아내기

Rails 에 jwt 활용하기

이렇게 처리해두면, 유저가 jwt를 가지고 서버에 요청을 했을 때, 우리가 발행한 토큰이면서 만료되지 않았으면 요청받은 작업을 처리하면 된다.

 

토큰이 만료되었다면 리프레쉬 토큰으로 토큰을 재발급 받으면 된다.

 

리프레쉬 토큰이 만료되었다면 유저가 다시 로그인 flow 를 타야한다.

 

로그아웃은 서버에서 따로 처리할 필요가 없다.

JWT with rails, user authorization

By mingyun chae

JWT with rails, user authorization

루비온레일즈에서 jwt를 가지고 회원인증하는 방법을 설명합니다.

  • 648