Node.jsとGoogle Authenticatorを使って二段階認証を行う

## はじめに

2段階認証と言うと筆者の狭い見識では仮想通貨関連のアプリケーションでよく使われているという印象だったんですが、最近では様々なアプリケーションに使用されている（Qiitaさんとか、Gmailとか）なと思ったのと、直近で触ってるプロジェクトで導入することになったのでメモがてらその流れを記しておこうと思います。

結論から言うと**導入自体は非常に簡単**でビックリしました。

## 前提

- [Google Authenticator](https://itunes.apple.com/jp/app/google-authenticator/id388497605?mt=8)がスマホにインストールされている事

## 動作確認した環境

- OS: Ubuntu16.04
- Node.js: v8.9.4
- Yarn: v1.5.1

## yarn install

```terminal
$ yarn install speakeasy qrcode
```

## 1. speakeasyでSecret Keyの生成

speakeasyとは、2段階認証を行う際のハッシュ値の生成や認証コードが一致するかのチェックなどの手間となる部分をよしなにやってくれるモジュールです。

まずはSecret Keyを生成します。

```js
const speakeasy = require('speakeasy');
const secret = speakeasy.generateSecret({ length: 20 });

console.log(secret.base32);

// FBDXWRCNIBFUA5JKGNKF2SBRGVYUOKJQ
```

生成すると上記のような値が取得できるので、その値とユーザーとの関連性を持たせる為にユーザー作成時にそのSecret KeyをDBに入れます。（あくまでイメージですが、以下のような形でDB保存します。）

|id|username|password|secret_key|
|:---|:----|:---|:---|
|1|yuyake0084|hoge|FBDXWRCNIBFUA5JKGNKF2SBRGVYUOKJQ|

## 2. QRコードの画像生成

先ほど取得した`secret`という変数にはオブジェクトが入っているのですが、`base32`というプロパティ以外にも`otpauth_url`というプロパティがあるのでそれをもとにQRコードの画像のURLを生成します。

```js
const speakeasy = require('speakeasy');
const QRcode = require('qrcode'); //追記
const secret = speakeasy.generateSecret({ length: 20 });

...

// Secret KeyをDBに入れる処理

...

QRCode.toDataURL(secret.otpauth_url, (err, qrcode) => {
  console.log(qrcode); // base64のQRコードの画像パスが入ってきます
});
```

取得できたQRコードはそのままクライアント側に返して表示してあげればユーザーはQRコードリーダーを介してGoogle Authenticatorにトークンを追加できます。

## 3. ユーザーが打ち込んだ認証コードが正しいか確認

Google Authticatorでは30秒ごとにパスワードが変更されます。
リアルタイムで変更されていく値とユーザーが入力したパスコードが一致するかどうかの確認をNode.js側でする必要があるので、その処理の紹介を。

```js:POSTリクエスト内容
{
  url: '/api/qrcode/1',
  method: 'POST',
  data: {
    authcode: '123456' //　ユーザーが入力した値
  }
}
```

```js
const { authcode } = req.body;
const { secret_key } = ... // usersテーブルからidが1のレコードを取得してくる処理

const verified = speakeasy.totp.verify({
  secret: secret_key,
  encoding: 'base32',
  token: authcode
});

console.log(verified); // true or false
```

speakeasyにはbase32で生成されたSecret Keyと入力した認証コードを比較してBooleanを返してくれるメソッドがあるのでそれがtrueであれば無事認証完了です。なんて簡単。

## いいかんじにしたい

上記までのフローでGoogle Authenticatorで認証は取れるんですが…………

![Image-uploaded-from-iOS_02.png](https://qiita-image-store.s3.amazonaws.com/0/88810/13157a9d-b415-48e3-c145-436c66fd0431.png)


なんか…………………


物足りないですよね……………


それもそのはず。
上記画像の通りで、**サービス名が無い**し、下には**SecretKey**と記載があるのみ。
これでは何のサービスのトークンを保持しているのか分からないのでユーザーには優しくありませんし、どうせやるなら**それっぽく**したいのでしましょう。

### 1. Secret Key作成時にnameとissuerを追記

```js
const secret = speakeasy.generateSecret({
  length: 20,
  name: 'test.com', // 追記
  issuer: 'test' // 追記
});
```

issuerがサービス名で、nameが『SecretKey』 と書かれてるところに上書きされます。

### 2. QRコードを生成する際にもlabelとissuerを追記

```js
// 以下のように書き換える
const url = speakeasy.otpauthURL({
  secret: secret.ascii,
  label: encodeURIComponent('test.com'),
  issuer: 'test'
});

QRCode.toDataURL(url, (err, qrcode) => {
  console.log(qrcode);
});
```

labelというプロパティですが、Secret Key生成時のnameと同じ値にしてあげてください。
これで一通りやってみると…、

![Image uploaded from iOS (1).png](https://qiita-image-store.s3.amazonaws.com/0/88810/389f76cb-0c54-da47-cde6-43b5a607ab76.png)

できましたね！
やはりかの有名企業と同じアプリ内で名を連ねるのはなんか………こう…………………ほっこりしますね☺️

あと、個人的にハマったことがあってこのnameとlabelですが、上記のようにSecret Key作成時とQRコード作成時に同じ値を入れてあげないとSecret Keyが同じでも認証コードが一致することはありません。（当たり前と言えば当たり前なんですが…）

## 参考にさせてもらった記事

- https://davidwalsh.name/2fa
- https://github.com/speakeasyjs/speakeasy/issues/86