Laravel6.0 の認証を Firebase のTwitter Oauth認証で行ったメモ

## 概要
モチベーションは以下。

* Laravel初心者なので、機能に色々触れてみたい。
* 最近流行のOAuthを使ってみたい。
* せっかくgcpを触っているのだから、firebaseも触ってみたい。
* パスワード入れるの面倒だからツイッターで入りたい

セキュリティホール見つけるなどしたら教えてください。

## 構成

プロジェクトフォルダは[Laravel6.0をGoogle Cloud PlatformのGoogle App EngineのPHP7.2環境で動かしたメモ](https://qiita.com/hibohiboo/items/57b31b3c14686fbea3b5) でのフォルダ構成と同じ。
今回触ったファイルを主にあげる。

```yaml
- .env
- secret.json
- app
  - User.php
  - Auth
    - MyEloquentUserProvider.php
  - Http
    - Controller
      - MyAuth
        - LoginController.php
    - Providers
      - AuthServiceProvider.php
- config
  - auth.php 
  - firebase.php
- databasae
  - migrations
    - 2019_10_12_100647_create_users_table.php
- resources
  - views
    - login.blade.js
- routes
  - web.php
```

## 認証の流れ

以下のイメージ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/44f6b100-2b9c-a48d-1788-af1034ae49a3.png)

## 事前準備

### ツイッターで認証APIを作成
[Twitter 開発者登録に使う英語例文集](https://note.mu/mogya/n/nbd9a720f8a5b)を参考に、[Twitter 社](https://developer.twitter.com/content/developer-twitter/ja.html)へ申請。

```
# The core use case, intent, or business purpose for your use of the Twitter APIs
I want to use Twitter's API to authenticate users in my application.
My application is the website to create charactersheet for Table Talk RPG.

# If you intend to analyze Tweets, Twitter users, or their content, share details about the analyses you plan to conduct and the methods or techniques
No, I have no plan to analyze Tweets at all.

# If your use involves Tweeting, Retweeting, or liking content, share how you will interact with Twitter users or their content
No. A User can tweet own charactersheet. However, My application doesn't use Twitter API to do this. I just add the "tweet" button on my site.


# If you’ll display Twitter content off of Twitter, explain how and where Tweets and Twitter content will be displayed to users of your product or service, including whether Tweets and Twitter content will be displayed at row level or aggregated
I have no plan to display Twitter data on my site or outside of twitter. I use Twitter API to authenticate users.
```

#### App の作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/d3f16414-faaf-03aa-87fe-9841e969d21e.png)

『i』のついているところが必須項目。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/9137f68d-a0e9-9a41-19e5-da0fb6b7bf5d.png)

アクセス権限を Read-only にする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/c181693b-4b8d-a2ac-3994-482931ad7e29.png)

### firebase の準備

#### firebaseプロジェクトの準備
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/e3b56ce5-660f-05b5-70d5-1560876df2da.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/2b183ec5-9d63-71cd-59be-02bd05d9b32e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/992eda4b-8386-24ee-6829-7f68ff5dae77.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/7f59ff56-2fc0-87b8-e36d-fe1c36d2ed7b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/078c05b3-41ac-b8d7-b637-2598225806d0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/86b46fe6-ecd9-63ac-fc4c-f34121628286.png)
#### 基本設定

リソースロケーションのリージョンは東京にしておく。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/bcea7da2-8436-9972-6d6d-e2c0a62559f0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/d79ed5af-1ff5-f856-22ae-2bf910f88b27.png)

#### Authentifactionの有効化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/e9c0d07f-2d68-f053-a72c-af0f393641fe.png)

『ログイン方法』のタブから、 Twitter を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/4b13c87e-e1cf-26c1-3efd-fc157824ce72.png)

ツイッターの開発アカウントでアプリ連携を確認して入力する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/0500a039-da6b-e3b7-7627-092ab4dc0071.png)

ツイッターのアプリ連携の『Callback URLs』に、Firebaseの画面に書かれているコールバック URL を記入する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/29d5b437-fdd0-2381-2eda-292bc51af514.png)

『ログイン方法』のタブの下のほうにある、承認済ドメインにファイルを配置するサーバのドメインを追加。ローカル開発の場合は、IP番号を追加でもいい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/07163ce9-20ce-bc89-3518-8fb3a59fd089.png)

#### 秘密鍵のダウンロード

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99876/ff57a4de-b211-1ecc-0785-2ff53f5f53f4.png)

jsonファイルはLaravelのディレクトリに置いておく。
ここではsecret.jsonの名前で保存したとしておく。

### Laravelでの作業
[Laravel + Nuxt.js + Firebase でいい感じにTwitterによるソーシャルログインを実現する](https://qiita.com/maguro_tuna/items/4b3ecd7502e218f103ca)を参考にして行った。

#### Firebase用のライブラリの導入

```
composer require kreait/laravel-firebase
```

#### Firebaseの秘密鍵の設定

秘密鍵のダウンロードで保存したjsonのパスを環境変数に記入する。

```config:.env
FIREBASE_CREDENTIALS=secret.json
```

firebaseのconfigファイルを追加する。

```php:config/firebase.php
<?php

return [
    'credentials' => [
        'file' => base_path(env('FIREBASE_CREDENTIALS')),
        'auto_discovery' => true,
    ],
];
```

#### サービスプロバイダの追加

追加したライブラリをサービスプロバイダに追加する。

```diff:config/app.config
        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
+        Kreait\Laravel\Firebase\ServiceProvider::class, // ←追加
    ],
```

#### 認証用のテーブル作成

```
php artisan make:migration create_users_table --create=users
```

```php:databasae/migrations/2019_10_12_100647_create_users_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('firebase_uid', 255)->unique();
            $table->string('name', 255);
            $table->string('twitter_screen_name', 255)->nullable();
            $table->string('twitter_profile_image_url_https', 1024)->nullable();
            $table->timestamps();
            $table->rememberToken();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}
```

あわせてユーザのモデルも修正。

```php:app/User.php
<?php

namespace App;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
    public $firebase_uid;
    public $twitter_screen_name;
    public $twitter_profile_image_url_https;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'twitter_screen_name','twitter_profile_image_url_https', 'firebase_uid'
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];
}
```

環境変数にデータベースの情報を追加する。

```:.env
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=<laradockで決めたもの>
DB_USERNAME=<laradockで決めたもの>
DB_PASSWORD=<laradockで決めたもの>
```

テーブルの作成

```
php artisan migration
```

#### ログイン画面の作成

```html:resources/views/login.blade.php
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="ログインテストページ" />
    <meta name="keywords" content="TRPG,開発,ツール" />
    <meta name="robots" content="index" />

    <title>ログインテストページ</title>
    <link href="https://www.gstatic.com/firebasejs/ui/4.2.0/firebase-ui-auth.css"rel="stylesheet"/>
    <script src="https://www.gstatic.com/firebasejs/6.3.5/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.3.5/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/ui/4.2.0/firebase-ui-auth__ja.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script type="text/javascript">
firebase.initializeApp({
  "apiKey": "YOUR_API_KEY",
  "projectId": "whitemaptools",
  "authDomain": "whitemaptools.firebaseapp.com"
});
      // FirebaseUI config.
      var uiConfig = {
        signInSuccessUrl: '/callback',
        callbacks: {
            signInSuccessWithAuthResult : (authResult,callbackUrl)=>{
                // Twitterでのサインインに成功したら、Laravelのログインを呼び出す
                var user = authResult.user;
                var twitterUser = authResult.additionalUserInfo;
                var twitter_screen_name = twitterUser.profile.screen_name;
                var twitter_profile_image_url_https = twitterUser.profile.profile_image_url_https;
                (async ()=>{
                    const idToken = await user.getIdToken(true);
                    document.getElementById('token').value = `${idToken}`;
                    document.getElementById('twitter_screen_name').value = `${twitter_screen_name}`;
                    document.getElementById('twitter_profile_image_url_https').value = `${twitter_profile_image_url_https}`;
                    document.getElementById('loginform').submit();
                    return;
                })();

                return false;
            },
        },

        signInOptions: [
          firebase.auth.TwitterAuthProvider.PROVIDER_ID,
        ],
        tosUrl: 'agreement',
        privacyPolicyUrl: function() {
          window.location.assign('privacy-policy');
        }
      };
      var ui = new firebaseui.auth.AuthUI(firebase.auth());
      ui.start('#firebaseui-auth-container', uiConfig);
    </script>
  </head>
  <body>
    <h1>ログイン</h1>
    <div id="firebaseui-auth-container"></div>
    <form id="loginform" action="/login" method="post" style="display:none">
        {{ csrf_field() }}
        <input id="token" name="token">
        <input id="twitter_screen_name" name="twitter_screen_name">
        <input id="twitter_profile_image_url_https" name="twitter_profile_image_url_https">
    </form>
  </body>
</html>
```

`#firebaseui-auth-container`のdiv要素を指定して、firebaseのソーシャルログイン用のボタンを表示するようにしている。

#### ログイン後画面の作成

```html:resources/views/home.blade.php
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Document</title>        
    </head>
    <body>
        こんにちは！ 
@if(Auth::check())
        {{\Auth::user()->name}} さん 
@else 
  ゲストさん <br />
  <a href="/login">ログイン</a>
@endif
        
    </body>
</html>
```

#### ルーティング設定
```php:routes/web.php
Route::get('/login', function () {
    return view('login');
});
Route::post('/login', 'MyAuth\LoginController@authenticate');
Route::get('/home', function () {
    return view('home');
});
```

#### ログイン用のコントローラ作成

```
php artisan make:controller MyAuth/LoginController --invokable
```

```php:app/Http/Controllers/MyAuth/LoginControler.php
<?php
namespace App\Http\Controllers\MyAuth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    /**
     * @var Firebase
     */
    private $auth;
    private $logger;

    /**
     * コンストラクタインジェクションで $firebase を用意
     * @param Firebase $firebase
     */
    public function __construct(\Kreait\Firebase\Auth $auth,  \Psr\LOg\LoggerInterface $logger)
    {
        $this->auth = $auth;
        $this->middleware('guest')->except('logout');
        $this->logger = $logger;
    }


    /**
     * 認証を処理する
     *
     * @param  \Illuminate\Http\Request $request
     *
     * @return Response
     */
    public function authenticate(Request $request)
    {
        $id_token = $request->input('token');

        try {
            $verifiedIdToken = $this->auth->verifyIdToken($id_token);
        } catch (InvalidToken $e) {
            $logger->error("invalidToken", $e->toString());
            return response()->json([
                'error' => $e->toString(),
            ]);
        }

        $uid = $verifiedIdToken->getClaim('sub');
        $credentials = ['firebase_uid'=> $uid];

        $firebase_user = $this->auth->getUser($uid);
        $user = \App\User::firstOrCreate(
            ['firebase_uid' => $uid],
            ['name' => $firebase_user->displayName, 
            'twitter_screen_name' => $request->input('twitter_screen_name'),
            'twitter_profile_image_url_https' => $request->input('twitter_profile_image_url_https')]
        );

        if (Auth::attempt($credentials)) {
            // 認証に成功した
            // return redirect()->intended('home');
            return redirect('/home');
        }else{
            abort(401, 'Unauthorixed');
        }
    }
}
```

#### パスワード検証の削除

```php:app/Auth/MyEloquentUserProvider.php
<?php
namespace App\Auth; 
 
use Illuminate\Auth\EloquentUserProvider; 
use Illuminate\Contracts\Hashing\Hasher as HasherContract; 
use Illuminate\Contracts\Auth\Authenticatable as UserContract; 
 
class MyEloquentUserProvider extends EloquentUserProvider 
{  
    public function validateCredentials(UserContract $user, array $credentials) 
    { 
        // パスワード認証しない
        return true;
        // ハッシュ値による認証
        //  return $this->hasher->check($plain, $user->getAuthPassword());
    }
}
```

```php:app/Providers/AuthServiceProvider.php
<?php

namespace App\Providers;
use App\Auth\MySessionGuard; 
use App\Auth\MyEloquentUserProvider; 
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Auth; 
use \Psr\LOg\LoggerInterface;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot(LoggerInterface $logger)
    {
        $this->registerPolicies();
 
        Auth::provider('my_eloquent', function($app, array $config) {
            return new MyEloquentUserProvider($app['hash'], $config['model']);
        });
    }
}
```

#### 不要な設定の削除
パスワードを使用しないので、パスワード関連の設定を削除

```diff:config/auth.php
<?php

return [
    'defaults' => [
        'guard' => 'web',
- 'passwords' => 'users',
    ],
    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'api' => [
            'driver' => 'token',
            'provider' => 'users',
            'hash' => false,
        ],
    ],
    'providers' => [
        'users' => [
-             'driver' => 'eloquent',
+            'driver' => 'my_eloquent',
            'model' => App\User::class,
        ],
    ],
- 'passwords' => [
-     'users' => [
-         'provider' => 'users',
-         'table' => 'password_resets',
-         'expire' => 60,
-     ],
- ],
];
```

これで、firebaseと連携したログインができるようになっている、はず。

[ここまでのソース](https://github.com/hibohiboo/whitemap/tree/a16318e94185eecfda1d96bcac5eb3e7d01ab4d2/laravel_docker/whitemap)

## 参考

[Twitter 開発者登録に使う英語例文集](https://note.mu/mogya/n/nbd9a720f8a5b)
[面倒なログイン機能の実装は Firebase Authentication に丸投げしよう](https://www.apps-gcp.com/firebase-authentication/)
[firebase ui](https://github.com/firebase/firebaseui-web)
[Guard](https://qiita.com/fagai/items/a70c937ab7cf72f19dc2)
[【PHP】新 TwitterOAuth でログイン機能を実装する](https://qiita.com/sofpyon/items/982fe3a9ccebd8702867)
[Firebase Admin SDK for PHP Authentication](https://firebase-php.readthedocs.io/en/latest/authentication.html)
[Laravel のログイン認証の基本(Authentication)を完全理解する](https://reffect.co.jp/laravel/laravel-authentication-understand)
[Laravel 5.7 でプレーンなパスワード認証を行う](http://spacetimebubble.net/blog/2018/12/11/laravel-5-7%E3%81%A7%E3%83%97%E3%83%AC%E3%83%BC%E3%83%B3%E3%81%AA%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E8%AA%8D%E8%A8%BC%E3%82%92%E8%A1%8C%E3%81%86/)
[既存の user マイグレーションファイルを書き換えてみた](http://raichel.hatenablog.com/entry/2015/02/15/170639)
[Laravel 公式](https://readouble.com/laravel/)
[Laravel 6.0 ログイン機能を実装する](https://qiita.com/ucan-lab/items/bd0d6f6449602072cb87)
[Laravel 6.0 ログイン機能を実装する](https://blog.capilano-fw.com/?p=4576)
[【Laravel + JavaScript】QR コードで自動ログインする機能をつくる（ダウンロード可）](https://blog.capilano-fw.com/?p=3969)
[Laravel + Nuxt.js + Firebase でいい感じに Twitter によるソーシャルログインを実現する](https://qiita.com/maguro_tuna/items/4b3ecd7502e218f103ca)
[Nuxt.js と Laravel を使って Twitter ログイン機能を実装する](https://qiita.com/hareku/items/ea09602bf40bf0a42040)
[皆見飽きただろう Nuxt.js with Firebase 入門 認証編](https://qiita.com/kiyc/items/322b354290f95dbe3276)
[Nuxt/Vuex で Firebase Authentication を使ってユーザー認証機能を作る](https://blog.ikedaosushi.com/entry/2019/04/17/201246)
[Firebase Auth を Nuxt + Rails の自前サービス に導入してみた](https://www.slideshare.net/TomoeTeshima/firebase-auth-nuxt-rails)
[モダンな技術を全く知らない SIer5 年目が Web サービスを作ることになったため 0 から勉強する ~Laravel jwt-auth + Nuxt 編~](https://qiita.com/gemetasu1103/items/2981f69f5c8c3fe3e742)
[LaravelAPI + Nuxt で MultiAuth を実装する](https://qiita.com/gemetasu1103/items/2a502fde072845962865)
[vue.js (Nuxt.js)と laravelPassport を使って SPA でのログイン機能を実装してみた](https://qiita.com/frostnday/items/2268c825923310571d93)
[Firebase と Laravel の連携検証してその先の DALI-KNX を目指す](https://digital-light.jp/2018/09/13/tried-to-connect-firebase-with-laravel-for-dali-knx/)
[[Laravel] Firebae の ID トークンをバックエンドで確認する](https://atuweb.net/201809_laravel_verify_firebase_token/)
[Laravel Firebase Auth](https://medium.com/@morrislaptop/laravel-firebase-auth-cff892b116a2) \*
[laravel-firebase](https://github.com/kreait/laravel-firebase)
[[Laravel] 6.0.4 がリリースされました](https://xzxzyzyz.com/2019/09/laravel-604-released/)
[ファサードと DI テストについて](https://qiita.com/yousan/items/18dab4dbac4a27ce3662)
[Laradock の Nginx を SSL 化する](https://qiita.com/osakana9114/items/48fb03e51e23dd02871c)
[コピペで OK！ローカル環境に HTTPS を導入する（nginx 編）](https://blog.capilano-fw.com/?p=2228)
[Firebase で作った Web サービスを 3 ヶ月運用してみて、ハマったこと・知っておきたかったこと](https://qiita.com/kira_puka/items/ef7dd47519403cd9bcf2)
[Rails+Firebase認証のサンプルアプリ](https://qiita.com/johnslith/items/6f8742b786b50f8dc0ac)
[firebase-ui web](https://github.com/firebase/firebaseui-web)
[Firebase Auth のユーザ認証機能を自前のデータベースと連携する](https://qiita.com/geerpm/items/165c31302edce1e52146)