🐯

devise-token-authを用いたログイン・ログアウト機能の作成①

2023/08/14に公開

今回は、devise-token-authを用いたログイン・ログアウト機能の実装方法を解説いたします。
これからdevise-token-authを用いてアプリを作成しようとしている方の参考になれば幸いです。

まず、今回実装する上で必要な情報を説明いたします。

 

「認証」と「認可」

「認証」は、相手が誰であるのか確認・特定することで、「認可」は、特定条件下において対象物を利用可能にする権限を与えることです。(詳細はこちらこちらを参照。)

 

トークンベース認証

トークンベース認証は、トークンをチェックすることで同一性を確認することができプロセスです(参照)。

トークンベース認証には、Webトークンを用いた認証方法があります。

Webトークンを用いた認証は、ユーザーがユーザー名とパスワードを使って毎回ログインする代わりに、一度ログインした後、システムが発行する「トークン(認証に成功したという情報)」と呼ばれる特別なコードを使用してアクセスを制御します。サーバ側で発行・認証されたトークンはクライアント側で管理します。

クライアント側が持つこのトークンは認証に成功したという情報を持つので、そのトークンの有効期限内であれば、ログインしたセッションを有効な状態を保つことができます。

トークンベース認証とよく対比されるのが、Cookie(Session)認証であり、こちらの認証方法と相違点を理解しておくと良いと思います。(こちらを参照)

Cookie(Session)認証は、クライアントが一度webサーバにアクセスするとサーバ側でCookieファイルが作成され、クライアント側に保存されます。

次回以降、サーバにアクセス(リクエスト)した時、そのCookie(Session情報)を用いてサーバ側にあるSession情報と合致しているか検証し、合致していたら認証成功となります。

これが、Cookie(Session)認証の仕組みです。

つまり、Cookie(Session)認証とトークンベース認証の違いは、サーバ側にsession情報(認証のための情報)があるかないかということです。(トークンベース認証の場合、トークンが返却されたということは認証成功したということなので、認証するためのSessionなどの情報は不要ということです。)

図に書き起こすと下記の通りです。

https://www.okta.com/jp/identity-101/what-is-token-based-authentication/
https://www.cloudflare.com/ja-jp/learning/access-management/token-based-authentication/
https://zenn.dev/tanaka_takeru/articles/3fe82159a045f7

 
また、トークンベース認証のトークン生成から検証までの仕組みは下記の通りです。

  1. ユーザーがログイン情報(アドレスやパスワード)を送信すると、サーバーはユーザーの認証情報を検証します。
  2. ログインに成功すると、サーバーはトークン(認証に成功したという情報)を生成し、ユーザーに返します。
  3. ログイン後、ユーザーはトークン(認証に成功したという情報)を使用してサーバーにアクセスします。このとき、HTTPヘッダーのAuthorizationフィールドにトークンを含めます。
  4. サーバーは受信したトークンを検証し、トークンが有効であり、そのユーザーが正当なアクセス権を持っていることを確認します。

(devise-token-authの場合、認証成功後にrails APIを叩くことができるといったイメージです。)

 

devise-token-auth

railsの認証ライブラリであるdeviseを拡張し、APIのエンドポイント認証をトークンベース認証(サーバから生成されたトークンによりそのユーザーが誰であるのか確認・特定する認証方式)で検証することができるgemライブラリです。

ドキュメントとその翻訳
https://devise-token-auth.gitbook.io/devise-token-auth/
https://sainu.hatenablog.jp/entry/2018/08/11/194319

新規登録やログイン時のcurlコマンドを用いた動作確認の方法
https://qiita.com/tomokazu0112/items/5fdd6a51a84c520c45b5

react,rails の実装方法
https://qiita.com/kazama1209/items/caa387bb857194759dc5#deviseをインストール
https://zenn.dev/shogo_matsumoto/articles/c6485b39c5f621

 

ログイン機能の作成

上記のトークンベース認証の仕組みと添付させていただいた記事を参考にログイン機能を実装してみました。

実装する上での方針として、こちらの記事を参考に表側でログイン用のアドレスとパスワードを入力するための入力フォームを作成し、裏側でそれらの情報をもとに認証処理を実装することができれば良いと思います。

なので、フロントのReact側では、アドレスとパスワードを入力させるためのフォームを作成し、そこに各ユーザーのアドレスとパスワードを入力させるような仕組みにしました。

また、バックエンドの rails側 ではReact側から送信されたアドレスとパスワードからユーザーを判別し、rails APIを実行するために必要な認証用のトークン(access-token, client, uid )を発行させるようにしました。

実装結果は下記の通りです。

frontend/src/components/sign_in/SignIn.jsx
import React, { useEffect } from 'react'
import { useDispatch ,useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { Field, reduxForm } from 'redux-form';
import { Button } from '@mui/material';

// components
import { renderTextField } from '../modules/renderTextField';
import { verifyUserData } from '../../apis/signIn';

const SignIn = (props) => {

  const { handleSubmit } = props;
  const dispatch = useDispatch()
  const form = useSelector(state => state.form);
  const values = form && form.signInForm && form.signInForm.values;
  const pageFlag = useSelector(state => state.pageFlag)
  const navigate = useNavigate();

  // ユーザー情報(アドレスとパスワード)の送信
  const submitLoginUserData = (e) => {
    e.preventDefault();

    const params = {
      email: values.email,
      password: values.password
    }

    verifyUserData(params, dispatch)
  }

  // ログイン後、トップページ遷移
  useEffect(() => {
    if (pageFlag.flag) {
      navigate('/');
    }
  }, [pageFlag.flag, navigate]);

  return (
    <>
      <br></br>
      <div>
        ログインページです。
      </div>
      <br></br>
      <form onSubmit={handleSubmit}>
        <br></br>
        <Field
          name="email"
          component={renderTextField}
          label="メールアドレス"
          placeholder="メールアドレスを入力してください。"
          style={{ width: 280 }}
        />
        <br></br>
        <br></br>
        <Field
          name="password"
          component={renderTextField}
          label="パスワード"
          placeholder="パスワードを入力してください。"
          style={{ width: 280 }}
        />
        <br></br>
        <br></br>
        <Button variant="outlined" onClick={submitLoginUserData}>ログイン</Button>
      </form>
      <br></br>
      <br></br>
      <br></br>
      <div>会員登録がまだの方はこちらへ</div>
      <br></br>
      <Button variant="outlined" onClick={() => navigate('/users/sign_up')}>新規会員登録(無料)</Button>
    </>
  )
}

export default reduxForm({
  form: 'signInForm',
})(SignIn);

上記のSignIn componentでは、ユーザーがログインするためのアドレスとパスワードの入力フォームをcomponentとして作成し、アドレスとパスワードの入力値をparamsというオブジェクトにまとめ、verifyUserData という関数 の引数として設定し、verifyUserData 関数を実行しました。

また、ログイン成功後、入力フォームからページ遷移するような仕組みにしました。

frontend/src/urls/index.js
const DEFAULT_API_LOCALHOST = 'http://localhost:3010/api/v1'

export const loginIndex = `${DEFAULT_API_LOCALHOST}/auth/sign_in`
frontend/src/apis/signIn.js
import axios from 'axios';
import { loginIndex } from '../urls/index'
import { dispatchUserData } from '../reducks/reducers/user';
import { pageTransitionFlag } from '../reducks/reducers/common';

// ログイン認証処理
export const verifyUserData = async(params, dispatch) => {
  await axios.post(loginIndex, params)
  .then(response => {
    dispatch(dispatchUserData(response.data));
    dispatch(pageTransitionFlag(true));

    // レスポンスの内容をCookieに保存
    if (navigator.cookieEnabled)
    {
        document.cookie = 'access-token=' + response.data.data.access_token;
        document.cookie = 'client=' + response.data.data.client;
        document.cookie = 'uid=' + response.data.data.uid;
    }

    alert('ログイン成功しました。')

  }).catch(error => {
    console.log(error);
    alert('ログイン失敗しました。')
  });
};

上記のsignIn.js の verifyUserData 関数は、react側から取得したparams というアドレスとパスワードの入力値をまとめたオブジェクトとHTTPライブラリの axios を用いて、rails API を POSTリクエストしています。

また、rails API から返却された HTTPレスポンス の中にある access-token, client, uid という認証結果を Cookie へ保存しています。

なぜこのようなことをするのかというと、devise-token-auth を使用する場合、rails API をたたくときに、ユーザー登録時やログイン時に 生成させた認証情報(access-tokenclientuid)が必要だからです。

なので、ユーザーがログインした際に生成される認証情報(access-tokenclientuid)をCookie に保存することで、ユーザーがAPIリクエストを行う際に自動的に認証情報を提供できるようになり、ユーザーは簡単にログイン状態を維持しながら rails API を利用できる仕組みを実現することができます。

config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      mount_devise_token_auth_for 'User', at: 'auth', controllers: {
        sessions: 'api/v1/auth/sessions'
      }
    end
  end
end

上記 の routes.rbでは、ログイン処理を実行するためのルーティングを設定しています。

app/controllers/api/v1/auth/sessions_controller.rb
class Api::V1::Auth::SessionsController < DeviseTokenAuth::SessionsController

  # ログイン処理
  def create
    user = User.find_by(email: params[:email])
    if user && user.valid_password?(params[:password])

      # トークンを生成
      token = user.create_new_auth_token

      # トークン情報をJSON形式で返す
      render json: {
        data: user.as_json.merge({
          access_token: token['access-token'],
          client: token['client'],
          uid: token['uid']
        })
      }, status: :ok
    else
      render json: { error: 'Invalid email or password' }, status: :unauthorized
    end
  end
end

上記の sessions_controller.rb では、createアクションを定義し、その中にreact側から送信されたアドレスとパスワードを用いたログイン処理を実装しました。

まず、アドレスからユーザー情報を取得し、送信されたパスワードがユーザーのものかどうか判断させ、そのパスワードがユーザーのものならば、access-token, client, uid などの情報を作成するようにしました。

次に、その結果を React側へ JSON形式で返却するようにしました。

以上が、devise-token-authを用いたログイン処理の実装結果です。

Discussion