Web備忘録

雑多に書いています

SPAのTwitterログイン認証(OAuth)

SPA(RailsAPI + Vue.js)でのTwiiterログインを実装したので、自分の実装方法をまとめておきます。

概要図(ステップ)

f:id:fujiten3:20190801111818p:plain

実装の概要

まずはじめに、 実装を完走した感想ですがSPAで外部のAPIを利用したログインは基本的に辛みが深いように感じました。

自分の場合は、クライアント側(Vue.js) → バックエンド側(RailsAPI) → 外部API(TwitterAPI) というリクエストの遷移を基本にして実装したわけですが、「バックエンド → 外部API」の結果手に入れた認証情報をクライアント側に返す処理をどのように行うかというところで参考になるサイトがとても少なかったことでなかなか苦戦を強いられたからです。

調べたところクッキーを利用する以外にズバッと解決できそうなものを見つけられなかったので、今回はクッキーを使っています。

つまり、「バックエンド側(RailsAPI) → 外部API(TwitterAPI)」のやり取りの結果の一部をクッキーでクライアント側に返し、クライアント側でそれを受け取ったあと、そのクッキー情報に基づいてクライアントが再びRailsAPIを叩き、完全なログインを実現するという流れです。

ベストプラクティスかどうかはわかりませんが、XSS対策への対応は出来ています。

ログイン認証の大本は、以下の記事通りに作成しています。

fujiten3.hatenablog.com

ステップ1

(簡略化してポイントだけ書いてます。)

VueからRailsAPIを叩き、ステップ5を行うためのコールバックURLを貰う。

signinWithTwitter () {
  this.$http.secured.get('/authenticate')
    .then(response => location.href = response.data.oauth_url )
    .catch(error => this.signinFailed(error))

location.href = response.data.oauth_urlで、認可のためのコールバックURLを貰って、Twitterの承認画面に切り替えるという挙動になります。

ステップ2〜5

コールバックURLを返すための処理です。

また、クライアントサイドに「リクエストークン」をクッキーで返しています。(アクセストークンの取得に必要)

  def authenticate

    consumer ||= OAuth::Consumer.new(
      ENV['TWITTER_CONSUMER_KEY'],
      ENV['TWITTER_CONSUMER_KEY_SECRET'],
      { :site => "https://api.twitter.com" }
    )

    request_token = consumer.get_request_token(
      oauth_callback: ENV['TWITTER_CALLBACK_URL']
    )

    response.set_cookie("request_token",
      value: request_token.token,
      httponly: true,
      secure: Rails.env.production?)

    response.set_cookie("request_token_secret",
      value: request_token.secret,
      httponly: true,
      secure: Rails.env.production?)

    rtn = {}
    rtn["status"] = 'return_callback_url'
    rtn["oauth_url"] = request_token.authorize_url

    render json: rtn
  end

ステップ6〜7

認証に成功した場合、アクセストークンをクッキーで返答します。

def twitter

  request_token = OAuth::RequestToken.new(
    consumer,
    request.cookies['request_token'],
    request.cookies['request_token_secret']
  )

  access_token = request_token.get_access_token(
    {},
    :oauth_token => params[:oauth_token],
    :oauth_verifier => params[:oauth_verifier]
  )

  twitter_response = consumer.request(
    :get,
    '/1.1/account/verify_credentials.json',
    access_token, { :scheme => :query_string }
  )

  case twitter_response
  when Net::HTTPSuccess
    user_info = JSON.parse(twitter_response.body)

    if user_info["screen_name"]

      user = User.find_or_create(user_info, 'twitter')

      if user.persisted?

        payload = { user_id: user.id }
        session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
        tokens = session.login
        cookie_key_value_pairs = {

          'oauth_token2' => access_token.params["oauth_token"],
          'oauth_token_secret' => access_token.params["oauth_token_secret"],
          'csrf' => tokens[:csrf],
          'ac_token' => tokens[:access]
        }

        set_cookies_at_once(response, cookie_key_value_pairs)

        response.set_cookie('signedIn',
        value: true,
        domain: ENV["BASE_DOMAIN"],
        path: "/",
        secure: Rails.env.production?)

        set_cookie_at_token_validness(true)

        redirect_to ENV['CLIANT_SIDE_TOP_PAGE']

      else

        redirect_to ENV['CLIANT_SIDE_TOP_PAGE']

      end

    else
      # 認証失敗
    end

  else
    # OAuthを通してユーザ情報取得失敗
  end

end

ステップ8

クライアントにリダイレクトした後、実行されるメソッドです。

クッキーのsignedInに値が入っていた場合、RailsAPIを叩きます。

    checkSignedInWithCookie () {
      const cookies = document.cookie.split('; ')
      let obj = {}
      cookies.forEach(cookie => {
        let arr = cookie.split('=')
        obj[arr[0]] = arr[1]
      })
      if (obj.signedIn) {
        this.$http.secured.get(`/api/v1/users/show_me`)
          .then(response => {
            localStorage.signedIn = true
            localStorage.csrf = response.data.csrf

            this.$router.replace('/')
          })
          .catch(error => {
            this.setError(error, 'ログインエラー: なにかがおかしいです。')
          })
      }

ステップ9

tokenをAuthorizionヘッダーに格納したのち、 authorize_access_request!メソッドで、ステップ6〜7で発行したアクセストークンかどうかの認証を行っています。

認証に成功すれば、必要なユーザー情報やCSRFトークンを返答します。

def show_me
  ac_token = request.cookies['ac_token']
  request.headers['Authorization'] = "Bearer #{ac_token}"
  authorize_access_request!

  response.set_cookie('ac_token',
  value: nil,
  httponly: true,
  domain: ENV["BASE_DOMAIN"],
  path: "/",
  secure: Rails.env.production?)

  response.set_cookie('signedIn',
  value: nil,
  domain: ENV["BASE_DOMAIN"],
  path: "/",
  secure: Rails.env.production?)

  response.set_cookie(JWTSessions.access_cookie,
  value: request.cookies['ac_token'],
  httponly: true,
  secure: Rails.env.production?) 

  render json: { csrf: request.cookies['csrf'],
                  my_avatar: current_user.avatar.encode(:icon), 
                  uid: current_user.id  }
end

感想

ほとんど全ての記述をコントローラに書いてしまっているので、できればモデル層にコードを移したいなと思っています。

認証のためのクラスを作って、そこに処理を任せる形が一番きれいにリファクタリングできそうかな…。