
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、前回の記事Twilioでブラウザから電話を掛ける・受けるという記事ではブラウザから電話の発信&着信をしてみました。
そして、この機能を開発したあとで、Twilio
を使えばより面白い機能を開発することができる(実際は、最低金額を課金したので貧乏性な私はできるだけ減価償却したいという気持ちも強かった)ので、今回は前回に引き続き
Twilio
の記事をお届けしたいと思います。
そして、その内容はというと・・・
通話を使った自動ログインです
です。
これは、例えばログインするメールアドレスやパスワードを忘れてしまったときに登録済みの電話番号から通話をすることで自動ログインすることができるという機能です。(もしくは強力な認証にするために、通話のみでログインする形でもいいかもしれませんし、2段階認証として使ってもいいでしょう)
※ ちなみに今回の機能を使うとこんなカンジの通話になります。
↓↓↓
ぜひ皆さんのお役に立てると嬉しいです
開発環境: Laravel 5.8
目次 [非表示]
やりたいこと
通話を使った2段階認証は次の流れで実装します。
- ブラウザからアクセス
- 登録されている自分の電話番号を送信
- 4桁の数字をランダムに作成して表示
- Twilioで購入した電話番号にスマホなどから発信
- 4桁の数字を入力して本人確認
- ブラウザ側で自動ログインしてリダイレクト
では実際にやってみましょう!
前提として
Laravel
にログイン機能がインストールされていることが前提となっています。もしインストールがまだの方は以下を参考してください。
- Laravel 6.x 以上 ・・・ Laravel6.0でログイン機能を使う方法
- それ未満 ・・・ 【Laravel5.6】インストール直後にやること3点
Twilioに登録する
もちろん今回の機能を実装するにはTwilio
に登録しておき、電話番号を購入しておかないといけません。やり方については前回記事、Twilioでブラウザから電話を掛ける・受けるで紹介していますので、先にそちらで作業を行ってください。
Twilioのパッケージをインストールする
Twilio
はPHP
のヘルパーライブラリを用意してくれています。
通話時のプログラム実行で使いますので、以下のコマンドでインストールしておきましょう。
composer require twilio/sdk
2段階認証のフィールドをusersテーブルに追加する
今回の機能を実現するために、通常のusers
テーブルに以下4つのフィールドを追加します。
- phone_number ・・・ 電話番号
- tfa_phone_token ・・・ 2段階認証トークン(電話用)
- tfa_browser_token ・・・ 2段階認証トークン(ブラウザ用)
- tfa_succeeded ・・・ 2段階認証の成功
では以下のコマンドでマイグレーションを作成してください。
php artisan make:migration add_two_factor_auth_fields_to_users
すると、database/migrations/****_**_**_******_add_two_factor_auth_fields_to_users.php
というファイルが作成されるので中身を以下のように変更します。(太字が変更したところです)
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddTwoFactorAuthFieldsToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
Schema::table('users', function (Blueprint $table) {
$table->string('phone_number')
->nullable()
->after('remember_token')
->comment('電話番号');
$table->string('tfa_phone_token')
->nullable()
->after('phone_number')
->comment('2段階認証トークン(電話用)');
$table->string('tfa_browser_token')
->nullable()
->after('tfa_phone_token')
->comment('2段階認証トークン(ブラウザ用)');
$table->boolean('tfa_succeeded')
->default(false)
->after('tfa_browser_token')
->comment('2段階認証の成功');
});
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone_number');
$table->dropColumn('tfa_phone_token');
$table->dropColumn('tfa_browser_token');
$table->dropColumn('tfa_succeeded');
});
}
}
変更が完了したら、以下のコマンドでマイグレーションを実行してください。
php artisan migrate
実行が完了するとテーブルは以下のようになります。
ルートをつくる
続いてルートです。
routes/web.php
に以下のコードを追加してください。
// ブラウザ側
Route::get('twilio/two_factor_auth/form', 'TwilioController@form');
Route::post('/ajax/twilio/set_token', 'TwilioController@setToken');
Route::post('/ajax/twilio/auth_check', 'TwilioController@checkAuth');
// 通話側(Twilioがアクセスする部分)
Route::post('twilio/phone_input', 'TwilioController@phoneInput')->name('twilio.phone_input');
Route::post('twilio/phone_submit/{user}', 'TwilioController@phoneSubmit')->name('twilio.phone_submit');
なお、Twilio
からのアクセスはCSRF
トークンを含んでいませんので、app/Http/Middleware/VerifyCsrfToken.php
の$except
に通話側のURL
を登録してミドルウェアを解除しておきましょう。
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'twilio/phone_input',
'twilio/phone_submit/*'
];
}
コントローラーをつくる
php artisan make:controller TwilioController
app/Http/Controllers/TwilioController.php
というファイルが作成されるので、中身を以下のようにしてください。
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Twilio\TwiML\VoiceResponse;
class TwilioController extends Controller
{
private $language = ['language' => 'ja-jp'];
// ブラウザ側
public function form() {
return view('twilio.two_factor_auth')
->with([
'twilio_phone_number' => 'xxx-xxxx-xxxx', // Twilioで購入した電話番号
'browser_token' => Str::random()
]);
}
public function setToken(Request $request) {
$result = false;
$user = null;
$phone_token = '';
if($request->filled('phone_number')) {
$user = \App\User::where('phone_number', $request->phone_number)->first();
if(!is_null($user)) {
for($i = 0 ; $i < 4 ; $i++) {
$phone_token .= rand(0, 9);
}
$user->tfa_phone_token = $phone_token;
$user->tfa_browser_token = $request->browser_token;
$result = $user->save();
}
}
return [
'result' => $result,
'phone_token' => $phone_token
];
}
public function checkAuth(Request $request) {
$result = false;
if($request->filled('phone_number')) {
$user = \App\User::where('phone_number', $request->phone_number)
->where('tfa_phone_token', $request->phone_token)
->where('tfa_browser_token', $request->browser_token)
->first();
if(!is_null($user) && $user->tfa_succeeded) {
\Auth::login($user);
$user->tfa_phone_token = null;
$user->tfa_browser_token = null;
$user->tfa_succeeded = false;
$result = $user->save();
}
}
return ['result' => $result];
}
// 通話側(Twilioがアクセスする部分)
public function phoneInput(Request $request) {
$incoming_phone_number = preg_replace('|^\+81|', '0', $request->From); // 世界的な番号から日本番号へ変換
$user = \App\User::where('phone_number', $incoming_phone_number)->first();
$response = new VoiceResponse();
if(!is_null($user)) {
$gather = $response->gather([
'action' => route('twilio.phone_submit', $user->id), // phoneSubmit()へデータ送信
'method' => 'POST',
'numDigits' => '4' // 4桁入力する
]);
$gather->say('現在ブラウザに表示されている4桁の数字をプッシュ入力してください。', $this->language);
} else {
$response->say('該当する電話番号が見つかりませんでした。登録された電話番号から発信してください。', $this->language);
$response->leave();
}
return $this->twimlResponse($response);
}
public function phoneSubmit(User $user, Request $request) {
$result = false;
$response = new VoiceResponse();
if($request->filled('Digits')) {
$digits = $request->Digits; // 入力された番号
if(!empty($digits) && $user->tfa_phone_token === $digits) {
$user->tfa_succeeded = true;
$result = $user->save();
}
}
if($result) {
$response->say('認証に成功しました。ブラウザ側で自動的にログインされます。', $this->language);
} else {
$response->say('入力された番号が正しくありません。', $this->language);
}
$response->leave();
return $this->twimlResponse($response);
}
private function twimlResponse($twiml_response) {
return response($twiml_response, 200)->header('Content-Type', 'text/xml');
}
}
少しコードが長いので、ブラウザ側と通話側で分けて説明をします。
ブラウザ側
form()
ブラウザで直接アクセスするメソッドです。
このページから電話番号を送信して通話時の認証用トークン(tfa_phone_token
, tfa_browser_token
)を保存することになります。
なお、twilio_phone_number
の部分はTwilio
で購入した番号へ変更してください。
setToken()
form()
ページからAjax
で電話番号を受け取るメソッドです。
ここでは、通話時に入力してもらう4桁のランダムな数字を作ってtfa_phone_token
に保存します。
また、同時にform()
で作成されたトークンをtfa_browser_token
として保存していますが、これはなりすましログインを防ぐために「電話番号が送信されたブラウザだけ」が自動ログインできるようにしています。
※ 正直なところ、複雑になってしまうのでtfa_browser_token
をつけるかどうか悩みましたが私の記事のせいで不正アクセス発生なんてことになるのは嫌なのであえてつけました。
checkAuth()
こちらもAjax
でアクセスされるメソッドですが、これは1秒間に1回何度もアクセスされることになります。
そして、通話を使ったログインが成功した段階でログインをし、form()
ページは自動でリダイレクトされることになります。
通話側
phoneInput()
Twilio
に発信すると一番先にアクセスされるメソッドです。
この中では発信者の電話番号を取得し、「その電話番号を登録しているユーザーが存在しているか」をチェックします。そして、もし存在してるなら4桁の数字の入力を求めるという内容になっています。
phoneSubmit()
phoneInput()
で入力された4桁の数字が送信されてくるメソッドです。
この数字と、すでに登録されているtfa_phone_token
が同じ値の場合は認証が成功したということでtfa_succeeded
をtrue
で保存してます。
tfa_succeeded
は、checkAuth()
メソッドの中で利用されることになります。
ビューをつくる
ブラウザからアクセスするフォーム用のビューを作成します。resources/views/twilio/two_factor_auth.blade.php
というファイルをつくって中身を以下のようにしてください。
<html>
<head>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container p-5" id="app">
<div class="row" v-if="!hasPhoneToken">
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<div class="form-group">
<label>登録されている電話番号</label>
<input class="form-control" type="text" v-model="phoneNumber">
</div>
<button class="btn btn-primary" type="button" @click="onSubmit">送信する</button>
</div>
</div>
</div>
</div>
<div class="row" v-if="hasPhoneToken">
<div class="col-md-6">
<div class="card bg-light">
<div class="card-body">
<div class="form-group">
<strong>「{{ $twilio_phone_number }}」</strong>に発信して以下の数字を入力してください。
<h1 class="text-center mt-4" v-text="phoneToken"></h1>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data: {
phoneNumber: '',
phoneToken: '',
browserToken: '{{ $browser_token }}'
},
methods: {
onSubmit() {
const url = '/ajax/twilio/set_token';
const params = {
phone_number: this.phoneNumber,
browser_token: this.browserToken
};
axios.post(url, params)
.then(response => {
if(response.data.result) {
this.phoneToken = response.data.phone_token;
} else {
alert('該当する電話番号が存在しません。');
}
});
}
},
computed: {
hasPhoneToken() {
return (this.phoneToken);
}
},
mounted() {
// 1秒ごとに電話からの認証が完了したかをチェック
const timer = setInterval(() => {
if(this.hasPhoneToken) {
const url = '/ajax/twilio/auth_check';
const params = {
phone_number: this.phoneNumber,
phone_token: this.phoneToken,
browser_token: this.browserToken
};
axios.post(url, params)
.then(response => {
if(response.data.result) {
clearInterval(timer);
console.log('ログイン成功');
// ここでリダイレクト
location.href = '/home';
}
});
}
}, 1000);
}
});
</script>
</body>
</html>
こちらもコードが少し長いですがやっていることは主に以下の2つだけです。
- 電話番号をAjaxで送信する
- 通話側の認証が完了しているかを1秒毎にAjaxで確認する
実際にブラウザで表示すると次のようになります。
(電話番号を入力するフォーム)
(通話をうながす表示)
Twilioでウェブフックを設定する
では、ここまでで作成したURLにTwilio
がアクセスできるようにウェブフックを設定します。
ウェブフックは購入した電話番号の詳細ページの「Voice」から設定できます。
入力したら「Save」ボタンをクリックして保存してください。
これで作業は終了です。
お疲れ様でした
テストしてみる
では実際に今回の機能を試してみましょう。
テストとしてusers
テーブルに自分の携帯番号を保存し、ブラウザからその番号を送信(&4桁の数字を表示)した状態で通話を開始しています。
どうでしょう。
イントネーションが少し微妙なところもありますが、自分がつくった文章をここまで会話に近い音声にしてくれています。
また、ブラウザ側では認証が完了した直後に自動的にリダイレクトされることも確認しました。
成功です
ちなみに
そもそもの話になるのですが、通話を使ったログインは事前に電話番号が登録されていることが大前提です。
また、電話番号をusers
テーブルに登録する際もショートメッセージなどでパスワードを送信し、その数字を入力することで認証するなどの本人確認も必要になってくるかと思います。
また、今回作成した電話番号のトークンも本来は有効期限をつけるべきですが、複雑になりすぎるのでこの部分は割愛しています。
おわりに
ということで今回はTwilio
を使って通話での認証機能を実装してみました。
最近では、LINE
などのメッセージアプリを使っているのでプライベートでの通話は減ってきた感はありますが、ビジネスの世界ではまだまだ電話が使われていますので、Twilio
を使うことでもっともっと面白い機能を実現することができると思います。
また、ちょっとコードが古い(古いパッケージが使われている)のでそのまま使うことはできないようですが、Twilio
を使った以下のような実例をKDDI
が公開してくれています。
ぜひこちらも参考にしてみてはいかがでしょうか。
ではでは〜!