1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby on Railsの習得のためタスクリスト的なの作ってみる⑤〜認証・トークン発行編〜

Posted at

ここまでの記事はこちら
Ruby on Railsの習得のためタスクリスト的なの作ってみる①〜構成、設計編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる②〜Rails7.0+React18.0+ MySQL8.0 Docker開発環境構築編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる③〜API開発・呼び出し編〜
Ruby on Railsの習得のためタスクリスト的なの作ってみる④〜API開発・MySQL接続とモデル実装編〜

今回は、認証周りを実装します。

構成

ログインAPIでは、アクセストークンとリフレッシュトークンをJWTで発行します。
アクセストークンはレスポンスボディで返却、リフレッシュトークンはCookieに追加します。

トークン更新APIでは、Cookieから取得したリフレッシュトークンで認証し、アクセストークンとリフレッシュトークンを再発行します。

JWTとは

こちらがわかりやすいかと思います。

簡単に説明すると、HTTPヘッダに情報を付加するためのJSONの形式
であり、一般に
・改竄されにくく、
・改竄を検知できる
ように運用することでログイン情報を保持する用法で使われることの多い形式です。

モデル準備

リフレッシュトークンの発行をイベントとしてテーブルに保存することでユーザーとの紐付けを簡単にし、かつ、同一リフレッシュトークンによるトークン再発行を一度限りとすることを実現します。

/db/migrate/******_create_token_issue_events.rb
class CreateTokenIssueEvents < ActiveRecord::Migration[7.0]
  def change
    create_table :token_issue_events do |t|
      t.references :user, foreign_key: true, null: false
      t.text :refresh_token, null: false
      t.datetime :event_datetime, null: false
    end
  end
end

ユーザーテーブルに関しては、password_digestカラムを追加しておきます。
これにより、ActiveModelのauthenticateメソッドが使えるようになります。
詳しくはこちらのドキュメントに書いてあります。
また、Userモデルクラスにhas_secure_passwordメソッドを追加しておく必要があります。

/db/migrate/******_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :password_digest, null: false
      t.string :name, null: false
      t.string :mail_address, null: false
    end

    add_index :users, [:mail_address], unique: true
  end
end

ログイン認証

authenticateメソッドは、一件取得したUserインスタンスに対して呼び出すと、以下のように、パスワードを引数として渡すだけで認証ができてしまいます。

/app/controllers/api/v1/auth_controller.rb
module Api
  module V1
    class AuthController < ApplicationController
      def login
        mail_address = params[:mail_address]
        password = params[:password]

        user = User.find_by(mail_address:)
        if user.nil?
          # ユーザーが存在しない場合の処理
        end

        authed_user = user.authenticate(password)
        unless authed_user
          # パスワードによる認証に失敗した場合の処理
        end
        # 正常な場合の処理 ここでトークンの発行を行う(次章)
      end
    end
  end
end

トークンの発行

上記リンクのReadmeに従ってgemをインストールすると、JWT.encodeJWT.decodeを主に使ってJWTのエンコード、デコードができるようになります。

トークンの生成のコード例は以下のようになります。アクセストークンもリフレッシュトークンも同様に生成できます。
また、この例ではRS256アルゴリズムを使っていますが、他の暗号アルゴリズムを使いたい場合は上記リンクのReadmeを参照して下さい。

def generate_access_token(subject)
  rsa_private = File.open('file_name_of_rsa', 'r').read
  private_key = OpenSSL::PKey::RSA.new(rsa_private)
  exipire_access = 60.minute # 有効期限

  JWT.encode(
    { iss: config.jwt.issuer,
      sub: subject,
      exp: Time.current.since(exipire_access).to_i },
    private_key,
    'RS256'
  )
end

トークンの認証

トークン認証のコード例は以下のようになります。

def decode_token(token)
  rsa_public = File.open('file_name_of_rsa.pub', 'r').read
  public_key = OpenSSL::PKey::RSA.new(rsa_public)

  begin
    decoded_token = JWT.decode(token, public_key, true, { algorithm: 'RS256' })
  rescue JWT::VerificationError => e
    # 認証エラー時の処理
  rescue JWT::ExpiredSignature => e
    # 有効期限切れ時の処理
  end

  decoded_token
end

以上のトークン生成・認証をいい感じにControllerから呼び出すことでログインAPIやトークン更新APIを実装することができます。

例えば、ログイン認証で認証した後に、以下のように
・リフレッシュトークンをCookieにセット
・トークン更新イベントテーブルにレコード保存
・アクセストークンを返却
するといい感じになります!

access_token = TokenHandler.generate_access_token(authed_user.id)
refresh_token = TokenHandler.generate_refresh_token(authed_user.id)
cookies[:jwt] = refresh_token

token_issue_event = TokenIssueEvent.new(
  { user_id: authed_user.id, refresh_token:, event_datetime: Time.current }
)

token_issue_event.save

render json: { access_token: }

次回予告

これで一応バックエンドのAPIが動くようになったので、次回はReactでフロントを作ります。サクッといきたい...!

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?