Help us understand the problem. What is going on with this article?

Nuxt.jsとLaravelを使ってTwitterログイン機能を実装する

Nuxt.jsとLaravelを使用して、Twitterでログインする機能の実装方法を紹介します。

今回はLaravel公式パッケージのLaravel PassportLaravel Socialiteを利用し、実装したコードはGitHubに上げています。
https://github.com/hareku/nuxtjs-laravel-twitter-login

おおまかなTwitterログインの流れ

  1. ユーザーが「Twitterでログイン」ボタンを押す
  2. TwitterへのリダイレクトURLをLaravelから取得する
  3. 受け取ったリダイレクトURLにユーザーを遷移させる
  4. ユーザーがTwitterで認証を行い、Nuxt.jsのCallback URLへ戻ってくる
  5. (4)で受け取ったoauth_verifierなどのクエリをLaravelへ送信
  6. Laravel側でTwitterユーザー情報を取得し、Laravelのユーザーを取得(or 作成)
  7. ユーザーを元にPassportのAccessTokenを作成して返す
  8. 以降、Nuxt.jsではAuthorizationヘッダーにAccessTokenを付加してAPIリクエストを行う

上記の流れを元に解説していきます。
またNuxt.jsにはAxios moduleがありますので、HTTPクライアントにはそちらを使用します。

実装方法の解説

Nuxt.jsのリダイレクトページを作成

ユーザーが「Twitterでログイン」ボタンを押した時に遷移するページを作成します。
APIの/oauth/twitter/redirectからリダイレクトURLを取得しています。

pages/oauth/twitter/redirect.vue
<template>
  <p>Twitterへリダイレクトしています</p>
</template>

<script>
export default {
  middleware: 'guest',

  asyncData ({ app, error }) {
    return app.$axios.$get('/oauth/twitter/redirect')
      .then(data => {
        return { twitterAuthUrl: data.redirect_url }
      })
      .catch(e => error({ message: e.message, statusCode: e.statusCode }))
  },

  mounted () {
    window.location.href = this.twitterAuthUrl
  }
}
</script>

mountedメソッドは必ずクライアント側で呼ばれるライフサイクルメソッドです。(SSR時には呼ばれません)

リダイレクトURLを取得するAPIを作成

先ほどNuxt.jsで呼び出した/oauth/twitter/redirectをLaravelで実装します。
TwitterへのリダイレクトURLはLaravel Socialiteで簡単に取得できます。

use Laravel\Socialite\Facades\Socialite;

// SocialiteからリダイレクトURLを取得
Socialite::driver('twitter')->redirect()->getTargetUrl();

注意点としてSocialiteではセッション(Session)を利用します。
APIでSessionを利用するためには「CORS(Cross-Origin Resource Sharing)」の概念が必要になります。今回は説明を省略しますが、API通信ではセキュリティにおいても重要なので把握しておきましょう。

CORSの設定

Laravelにはlaravel-corsという便利なCORSパッケージがあるのでそちらを利用します。
supportsCredentialsをtrueに、そしてallowedOriginsにNuxt.jsのURLを指定しておきます。

config\cors.php
return [
    'supportsCredentials' => true,
    'allowedOrigins' => [
        env('WEB_URL', 'https://my-nuxt-app.com')
    ],
    'allowedHeaders' => ['*'],
    'allowedMethods' => ['*'],
    'exposedHeaders' => [],
    'maxAge' => 0,

];

Nuxt.jsのAxios側でもcredentialsを有効にします。

nuxt.config.js
axios: {
  credentials: true
}

Kernel.phpのmiddlewareGroupsに新たなsessionグループを追加します。

App\Http\Kernel.php
    protected $middlewareGroups = [
        // sessionグループを追加
        'session' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
        ],
    ];

これで完了です。
Controllerのミドルウェアに「session」を指定すれば、セッションを利用できるようになります。

Twitter認証後のCallbackページを実装

さて、ユーザーが先ほど取得したリダイレクトURLに遷移してアプリへ戻ってきました。
戻ってきたURLは以下のようになります。
https://my-nuxt-app.me/oauth/twitter/callback?oauth_token=xxxx&oauth_verifier=xxxx

そのままURLのクエリをthis.$router.queryで取得し、APIで送信してLaravel側で処理してもらいます。

pages/oauth/twitter/callback.vue
<template>
  <div>
    <p v-if="attempting">Twitterでログインしています。</p>

    <template v-else>
      <p v-else>Twitterでのログインに失敗しました。</p>
      <p>{{ failedMessage }}</p>
    </template>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'

export default {
  middleware: 'guest',

  data () {
    return {
      failedMessage: null
    }
  },

  computed: {
    attempting () {
      return !this.failedMessage
    }
  },

  methods: mapMutations([
    'setToken',
    'setUser'
  ]),

  async mounted () {
    try {
      const callbackData = await this.$axios.$get('/oauth/twitter/callback', { params: this.$route.query })

      this.setToken({ token: callbackData.access_token })
      this.setUser({ user: callbackData.user })

      this.$router.replace('/')
    } catch (error) {
      this.failedMessage = error.message
    }
  }
}
</script>

/oauth/twitter/callbackからAcessTokenとユーザー情報を取得し、Vuexへと保存しています。

Laravel側でユーザー情報を取得しAccessTokenと共に返す

先の/oauth/twitter/callbackを実装します。
Twitterのユーザー情報自体はSocialiteで簡単に取得できます。

$twitterUser = Socialite::driver('twitter')->user();

$user->getId();
$user->getNickname();
$user->getName();
$user->getEmail();
$user->getAvatar();

このユーザー情報を元に、アプリ側のユーザーを取得もしくは作成します。
この辺はアプリの仕様によって大きく異なりますが、私の場合は以下のように実装しています。

ユーザーのソーシャルアカウントModelを作成する(筆者の場合)

providerカラムに「twitter」、account_idカラムにはTwitterのid_strを格納します。

Schema::create('user_social_accounts', function (Blueprint $table) {
    $table->unsignedInteger('user_id');
    $table->string('provider');
    $table->string('account_id');

    $table->unique(['provider', 'account_id']);

    $table->foreign('user_id')
        ->references('id')
        ->on('users')
        ->onDelete('cascade');
});

ログイン時にこのデータベースに存在していなければ、新たにユーザーと共に作成します。
以下のコードは簡略化していますが、実際にはEメールアドレスの重複チェックなどが必要でしょう。完全なコードはGitHubを参照してください。

$twitterUser = Socialite::driver('twitter')->user();

$socialAccount = SocialAccount::firstOrNew([
    'provider'   => 'twitter',
    'account_id' => $twitterUser->getId(),
]);

if ($socialAccount->exists) {
    $user = User::find($socialAccount->getAttribute('user_id'));
} else {
    $user = User::create([
        'name'         => $twitterUser->getName(),
        'email'        => $twitterUser->getEmail(),
        'password'     => null,
        'twitter_id'   => $twitterUser->getNickName(),
    ]);

    $socialAccount->setAttribute('user_id', $createdUser->id);
    $socialAccount->save();
}

return [
    'user'         => $user,
    'access_token' => $user->createToken(null, ['*'])->accessToken,
];

返ってきたユーザー情報をVuexへ、AccessTokenをCookieへ保存

もう一度Nuxt.js側のコールバック処理を見てみましょう。

const callbackData = await this.$axios.$get('/oauth/twitter/callback', { params: this.$route.query })

this.setToken({ token: callbackData.access_token })
this.setUser({ user: callbackData.user })

取得したAccessTokenとユーザー情報を、それぞれVuexへ保存しています。
vuexファイルは以下のように実装します。

store/index.js
const inBrowser = typeof window !== 'undefined'

export const state = () => {
  return {
    user: null,
    loggedIn: false,
    token: null
  }
}

export const getters = {}

export const mutations = {
  setUser (state, { user }) {
    state.user = user
    state.loggedIn = Boolean(user)
  },

  setToken (state, { token }) {
    state.token = token

    // Store token in cookies
    if (inBrowser) {
      if (token) {
        this.$cookies.set('token', token, { expires: 30 })
      } else {
        this.$cookies.remove('token')
      }
    }
  }
}

export const actions = {
  // ユーザーの訪問時(SSR)で呼ばれるメソッド
  nuxtServerInit ({ dispatch, state, commit }, { error }) {
    const token = this.$cookies.cookies.token

    if (!token) {
      return Promise.resolve()
    }

    return dispatch('fetchUserByAccessToken', { token }).catch(e => {
      return dispatch('logout').catch(e => {
        error({ message: e.message, statusCode: e.statusCode })
      })
    })
  },

  fetchUserByAccessToken ({ commit, dispatch }, { token }) {
    commit('setToken', { token })

    return this.$axios.$get('/users/@me').then(user => {
      commit('setUser', { user })
    })
  },

  logout ({ commit }) {
    commit('setUser', { user: null })

    // Revoke access token
    return this.$axios.delete('/oauth/token/destroy').then(() => {
      commit('setToken', { token: null })
    }).catch(e => {
      commit('setToken', { token: null })
    })
  }
}

これによりSSR+SPAに対応した、Twitterログイン機能が実装できました。
またAxiosのrequestInterceptorを設定すれば、自動でAuthorizationヘッダーを付加してくれます。

plugins/axios.js
export default function ({ $axios, store }) {
  $axios.onRequest(config => {
    if (store.state.token) {
      config.headers.common['Authorization'] = `Bearer ${store.state.token}`
    }
    return config
  })
}
hareku
AWS / Go / Laravel / Vue.js / React
https://mycode.rip/
increments
「エンジニアを最高に幸せにする」ために Qiita、Qiita Team、Qiita Jobs を開発・運営しています。
https://increments.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした