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

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

今回はLaravel公式パッケージの[Laravel Passport](https://github.com/laravel/passport)と[Laravel Socialite](https://github.com/laravel/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](https://github.com/nuxt-community/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](https://github.com/laravel/socialite)で簡単に取得できます。

```php
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](https://github.com/barryvdh/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で簡単に取得できます。

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

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

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

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

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

```php
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](https://github.com/hareku/nuxtjs-laravel-twitter-login)を参照してください。

```php
$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側のコールバック処理を見てみましょう。

```javascript
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
  })
}
```