電話番号の認証を２段階認証とか勉強しつつ実装してみた


参考にしたのはこれ

- [GoogleやLINEみたいなSMS二要素認証をTwilioでサクッと実装する方法 - WPJ](https://www.webprofessional.jp/secure-laravel-apps-2fa-via-sms/)

ちょっとややこしいけど、有効期限を設定したワンオフの暗証番号を作成して、それと付き合わせることで認証をしているだけ、といえばだけでした。



## twilio導入でちょっと手間取った
twilioというものを使うとsmsが送れる。
という話だけ聞いて、とりあえずやってみました。

### お金かかるの？
気になったのは、SMSというのは安価な通信手段ではあれど無料ってことはないよねってこと。
電話料金とかかかるんですよね……？

**と思ったんですが、どうやらtwilioというのは無料アカウントというかトライアルアカウントみたいな状態が存在するみたいです。**

電話番号１つだけなら、11ドル分だけ無料で使えるっぽい。
ありがたや。

### やってみた
というわけでやってみました。

と言ってもアカウントを登録してSMS認証して、Get start的なことが書かれた赤いボタンを押して電話番号を取得しただけです。

間違えて日本のを取得しちゃったんですけどね。

日本の番号からだとSMSが送れないらしいです。
当然ながら、説明を読むとちゃんと書かれてますので落ち着いて登録しましょう（当たり前

で、日本がダメならどこって話なんですが、米国のが必要とのことです。
ダッシュボードにある`Programmable SMS`というウィンドウにある赤いボタンを押すと、米国設定の電話番号がデフォルトで選ばれてくれます。

ちなみに、間違えて取得しても破棄できるのでご安心を。
手順は以下。

DashboardのPhone Numbers > Manage Numbersで電話番号一覧を表示。
その後、電話番号（赤いところ）をクリック。
電話番号の情報ページに飛ぶので、画面下にあるRelease Numberというリンクをクリックすれば取得した番号を破棄できる。


# やりたいことを実装してみる
さて、では作ってみるとします。
実装したい部分の手順はこんな感じです。

1. 電話番号認証画面で電話番号を入力してSMS送信ボタンを押す
2. SMS送信処理をして認証ページに遷移する
3. 認証コード入力画面で認証コードを入力して認証ボタンを押す
4. 入力された認証コードとトークンを突き合わせ、合ってたら電話認証を1に更新する

記載してるコードはまるごとじゃないです。
適宜補完してください。

## マイグレーション
usersに追加します。

```php:create_users_table.php
$table->string('country_code', 4)->nullable();
$table->string('phone')->nullable();
$table->tinyInteger('verified_phone')->default(0);
```

## 電話認証画面
パスとファイル名は適当です。
このページには適当に遷移させてください。

たぶん数字のみとかヴァリデーション作らないといけない気がするけどスルーしてます。

```html:mypage/phone.blade.php
@php
// http://photos.google.com/?hl=ja
$country_list =
    [['93',	'アフガニスタン',	'Afghanistan'],
    〜〜〜なんかいろいろ続く〜〜〜
    ];
@endphp

〜〜〜中略〜〜〜

@section('content')
<h1 class="mt-3 mb-5">{{ $title }}</h1>
<form class="form-horizontal" role="form" method="POST" action="{{ route('registerPhone') }}">
    {{ csrf_field() }}
    <div class="input-group mb-3">
        <label for="phone">{{ __('Phone') }}</label>
        <div class="input-group-addon">
            <select name="country_code" style="width: 150px;">
                <option value="">{{ __('Country...') }}</option>
                @foreach($country_list as $c)
                <option value="+{{$c[0]}}" @if ("+".$c[0] == $country_code) selected @endif>(+{{$c[0]}}) {{$c[1]}} ({{$c[2]}})</option>
                @endforeach
            </select>
        </div>
        <input id="phone" type="text" class="form-control" name="phone" value="{{ old('phone', $phone) }}" required>
    </div>
    <button type="submit" class="btn btn-primary">{{ __('Send SMS') }}</button>
    @if ($verified_phone == 1)
    <label class="text-danger">{{ __('Phone Number is Authenticated.') }}</label>
    @endif
</form>
@endsection
```

## 認証コード入力画面
そのままな画面です。

```html:verify.blade.php
@section('content')
<h1>{{ $title }}</h1>
<form class="form-horizontal" role="form" method="POST" action="{{ route('verifyPhone') }}">
    {{ csrf_field() }}
    <div class="form-group">
        <label name="phone">{{ $postdata['country_code']." ".$postdata['phone'] }}</label>
    </div>
    <div class="form-group">
        <input id="verified_code" type="text" class="form-control" name="verified_code" value="" required>
    </div>
    <button type="submit" class="btn btn-primary">{{ __('Submit') }}</button>
</form>
@endsection
```

## ルーティング
追加します。

```php:web.php
Route::get('mypage/phone', 'Auth\MypageController@getRegisterPhone');
Route::post('mypage/phone', 'Auth\MypageController@postRegisterPhone')->name('registerPhone');
Route::get('mypage/phone/verify', 'Auth\MypageController@getVerifyPhone');
Route::post('mypage/phone/verify', 'Auth\MypageController@postVerifyPhone')->name('verifyPhone');
```

### コントローラー
MypageControllerを追加します。

```bash:command
php artisan make:controller MypageController
```

作成されたコントローラーに追加します。

```php:MypageController.php
use Auth;
use Session;
use App\Token;

〜〜〜中略〜〜〜

    /**
     * mypage/phoneを開く
     *
     * @return $this
     */
    protected function getRegisterPhone()
    {
        $user = Auth::user();

        return view('mypage.phone')
            ->with([
                'country_code' => $user->country_code,
                'phone' => $user->phone,
                'verified_phone' => $user->verified_phone
            ]);
    }

    /**
     * mypage/phoneからのpostを受け取る
     *
     * @return $this|\Illuminate\Http\RedirectResponse
     */
    protected function postRegisterPhone()
    {
        $user = Auth::user();

        $user->country_code = $_POST['country_code'];
        $user->phone = $_POST['phone'];
        $user->verified_phone = 0; //認証結果は一度リセットする
        $user->save();
        Auth::login($user); //ログインしているユーザー情報を更新

        $token = Token::create([
            'user_id' => $user->id
        ]);

        if ($token->sendCode())
        {
            Session::put('token_id', $token->id);

            return redirect('mypage/phone/verify')->withInput();
        }

        $token->delete(); // delete token because it can't be sent
        return redirect('mypage/phone')->with('status', __('Send SMS is failed.'));
    }

    /**
     * mypage/phone/verifyを開く
     *
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    protected function getVerifyPhone()
    {
        // post内容を取得
        $postdata = Session::get('_old_input');

        return view('mypage.verify', compact('postdata'));
    }

    /**
     * mypage/phone/verifyからpostを受け取る
     *
     * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
     */
    protected function postVerifyPhone()
    {
        $user = Auth::user();
        $token = Token::whereId(session('token_id'))->first();

        $v_code = $_POST['verified_code'];

        if ($v_code == $token->code)
        {
            $user->verified_phone = 1;
            $user->save();

            $token->used = 1;
            $token->save();

            return redirect('mypage')->with('status', __('Phone Number Authentication is complete.'));
        }

        return redirect('mypage/phone/verify');
    }

```

### モデル
Token.phpを追加します。

内容は参考資料に記載されているものと同じです。


## 手順ごとの詳細
### 1. 電話番号認証画面で電話番号を入力してSMS送信ボタンを押す

電話番号認証画面を開くとき、データを渡すためにAuth:user()を使っています。
事前にAuth::login($user)を使ってログインしておく必要があります。


### 2. SMS送信処理をして認証ページに遷移する
ここはほぼ参考資料の通りです。

前ページのpostを受け取って、userを更新してからgetにリダイレクトしてます。


### 3. 認証コード入力画面で認証コードを入力して認証ボタンを押す
ここも参考資料の通りです。


### 4. 入力された認証コードとトークンを突き合わせ、合ってたら電話認証を1に更新する
これでいいのかなと思いつつ、セッションから取り出したtokenと入力されたコードを比較します。
一致したら、userのフラグとtokenのusedを1に更新します。


# だいたいそんな感じ
作ってしまえばなんてことない処理でしたね。

あーでもないこーでもないとやってたせいでとんでもない時間がかかりましたが。

参考になる部分があれば幸いです。