LINEアカウントと自サービスのユーザ紐付けを実装してみた

# はじめに

LINEの公式アカウント機能を使用することで、
ユーザーとのデータ授受のインタフェースとしてLINEを活用することができます。

LINEを活用すると何が嬉しいのかというと、

- 自サービスからプッシュでユーザーに行動を促せる
- 画像などファイルデータも扱えるので、アップローダの実装工数を減らせる

など、大きなメリットがあると思っています。

## この記事で書きたいこと

LINEをインタフェースとして活用するにあたり、
実装に迷ったポイントがあったため、共有いたします。

（もっといい方法があるよ、などご指摘・ご意見ございましたら是非コメントください）

なお、本内容は先日開催した「[Personium Meetup #2](https://personium.connpass.com/event/168426/)」で
「[LINEでPersoniumにデータを貯める](https://drive.google.com/file/d/1_7hPrW8vQ6G4o-hkzzGRKgIIqtagbGgi/view)」として発表した内容を修正したものです。

# 今回取り上げる課題

さて、LINEを入力のインタフェースとして活用する場合に、課題となるのは何があるでしょうか？
私がまっさきにぶち当たったのは**「LINEのユーザーと自サービスユーザーの紐付け」**の難しさでした。

## LINE公式アカウントの仕様(2020年05月現在)

ではLINEの仕様から見ていきましょう。

LINEは下記のように [LINE Developers](https://developers.line.biz/ja/)で設定することで、
指定したURLにWEBフックでトークのイベントを送信させることが可能になります。

![upload](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/f4143d88-4482-7893-0868-e70780abbb2d.png)

ドキュメントはこちら → [メッセージ（Webhook）を受信する](https://developers.line.biz/ja/docs/messaging-api/receiving-messages/)


## 送信されるイベント形式

公式アカウントに送信されたWebフックでは下記に示されるような形式で送信されます。

```js
{
  "destination": "<公式アカウントのユーザーID>",
  "events": [],     // <Webhookイベントのリスト>
}
```

「Webhookイベント」は [Webhookイベントのタイプ](https://developers.line.biz/ja/docs/messaging-api/receiving-messages/#webhook-event-types) にあるように、様々な種類が存在します。

たとえば、トークにメッセージが送られたときには下記のように、`"type": "message"` な
オブジェクトが `events` に含まれて、データが送られてきます。
（[Webhookイベントオブジェクト](https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects) より一部抜粋）

```js
{
  "destination": "xxxxxxxxxx",
  "events": [
    {
      "replyToken": "0f3779fba3b349968c5d07db31eab56f",
      "type": "message",
      "mode": "active",
      "timestamp": 1462629479859,
      "source": {
        "type": "user",
        "userId": "U4af4980629..."
      },
      "message": {
        "id": "325708",
        "type": "text",
        "text": "Hello, world"
      }
    },
  /* 中略 */
  ]
}
```

簡単に内容を説明しますと

|プロパティ名|意味|
|:--|:--|
|replyToken| このトークンを付与してLINE APIに送信すると、このイベントへの返信として扱われる|
|timestamp| イベントの発生時刻 |
|source| イベントの送信元情報 |
|message| typeが `message` の場合含まれる、メッセージの内容 |

つまり、 event の `source` と `message` を読み解けば、
「誰が何と発言したのか」がわかります。

## 問題点

ではこれで完成か？というとそうではありません。

[送信元ユーザー](https://developers.line.biz/ja/reference/messaging-api/#source-user) によれば、送信元のユーザーを示す`source` のオブジェクトは、
下記の形式で表されます。

|プロパティ名|型|概要|
|:--|:--|:--|
|type|String| 文字列`user` （固定）|
|userId|String| 送信元ユーザーのID|

ここで厄介なのが、「送信元ユーザーのID」です。

ドキュメントによれば、

```
ユーザーIDの値は、U[0-9a-f]{32}の正規表現にマッチする文字列です。
```

とあり、

- `Udeadbeefdeadbeefdeadbeefdeadbeef`
- `U00112233445566778899aabbccddeeff`

みたいな文字列であることがわかります。
各自が検索で設定できる「hogehoge」のようなIDではなく、
LINEで一意になるように振られたIDが送られてくるのです（どういうロジックで振られるのかは不明です）。

## 上記を踏まえた上での実装案

ここまでで、LINEの発言データがどのように送られてくるのかわかったと思います。
では、それを自サービスでも使用できるように、
LINEのユーザーIDと自サービスのユーザーIDをマッピングする方法を検討していきます。

### その①：（NG）ユーザーID直打ち

一番最初に思いつくのがサービスにおける本人のIDをLINEで入力させる方法。

![スライド14.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/687d456b-c1c2-ae45-6283-c284b302b085.png)


これは他者が勝手に自分のIDを使ってサービスを利用できてしまう問題が生じます。

### その②：（NG）自サービスでLINEのIDを打たせる

では逆にLINEのIDを自サービスで打たせたらどうでしょうか？

![スライド17.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/13e65c96-c93e-50f0-bade-8dcaaf0b5fae.png)


これも、他人のトーク内容を自サービスに入力させることができてしまう問題が発生します。
というより、そもそもLINEのIDをユーザーは普通知り得ないので実現困難な方法でしょう。

### その③：一時キーを発行して紐付ける（無料でやる）

そこで考えたのが、登録時に一時キーを発行して、それを自サービスで
認証しているユーザーに入力させる方法です。

![スライド19.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/e22b6e3f-7bfc-dd86-294a-221c40f60eac.png)


1. ユーザーは公式アカウントを「ともだち登録」する（⇒フォローイベント発火）
2. サービスは受信したフォローイベントの送信元ユーザーIDに紐づく**一時キーA** を発行する
3. サービスはreplyTokenを使用してユーザーに**一時キーA** を送信する（1000通/月まで無料！）
4. ユーザーはサービスを開き（Aliceとして認証中）、**一時キーA** を入力する
5. サービスは受信した**一時キーA**に紐づくLINE上のIDをAliceと紐付け、確定用の **一時キーB** を発行する
6. ユーザーはLINEを開き、**一時キーB**を入力する
7. サービスは受信したメッセージイベントに含まれる **一時キーB**のLINE IDと、送信元のLINE IDが一致することを確認し、 AliceとLINE上のIDの紐付けを確定する

このようなフローです。

なぜ 4. の後に続くフローが必要かというと、**一時キーA** を繰り返し試行されると
他者による乗っ取りが成立してしまうためです。

一方で、**一時キーA** を十分に長いものにすれば問題ないという見方もあるとは思いますが、
そこは人間に入力させる都合上、ユーザビリティとのトレードオフになるのではないでしょうか。
（実サービスに組み込む上では専門家の意見を仰ぐべきだと思います。）

### その④：承認用URLを生成する

「その③」の方法では2種類の一時キーを生成するため、紐付けにやや面倒なフローを取ります。
そこで、**一時キーB**の入力による紐付けの確定ではなく、
承認用URLのクリックによる確定も考えました。

![スライド21.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/13399faf-2c38-2c61-f9e5-ef34176db512.png)

このようなフローであれば、自分のLINEアカウントがどのユーザーIDに紐付けられるかが
ユーザーにとって一目瞭然ですし、クリックするだけで成立するため、面倒さは軽減されます。

しかし、「公式アカウント」側からメッセージを送信するのにはコストがかかるため、
ユーザビリティとコストのトレードオフといったところでしょうか。

# 実装

今回は「その③」の方法で試しに作ってみました。

## ともだち登録

ユーザーはまず公式アカウントの「ともだち登録」から実施していきます。

![スライド27.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/ca4187b2-7e72-f22c-0abd-b624fff1df6e.png)

## 一時キーA発行

すると、**一時キーA**が送付されてくるので、

![スライド28.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/e3e6fd26-c14d-74ff-7da4-7e8fcef9a62b.png)

## サービスでの入力

それを自サービスの画面（認証済み）で入力してもらいます。

![スライド29.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/55ccf8bd-ec8a-a4df-f36a-4645fd13fa9f.png)

## 一時キーB発行

**一時キーB** をLINEで入力するように促されます。

![スライド30.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/8844bd83-79c8-3042-0d69-88cb5788cc00.png)

LINEのURLスキームを使用することで、ボタンを押したときにそのテキストが入力された状態でトーク画面を開くことができます。
（[テキストメッセージを送る](https://developers.line.biz/ja/docs/line-login/using-line-url-scheme/#sending-text-messages) 参照）

## 一時キーB送信

一時キーを送信すると紐付けが確定されます。

![スライド31.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/100f1793-03a1-9e6f-21cf-b14905134a71.png)

## 紐付け完了

あとはLINEのトークで自サービスを利用できるようになります。

![スライド32.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328319/5aadfa56-b9df-a6e3-7e40-eb9fee9bdd04.png)

今回のサンプルでは画像を送信し、
ライフログ的に日々の画像を保存できるようにしてみました。


# 終わりに

今回は自サービスとLINEのアカウントを連携する方法について検討を行いました。
「どのように一時キーを生成するか？」など議論の余地はまだまだありますが、一助となれば幸いです。

また、今回は「Personium」というOSSの上でサービスを構築し、
そこのアカウント連携をサンプルとして実装してみました。

LINEのようなチャットのプラットフォームは、
ライフログを取っていく上ではPDSととても親和性が高く、
個人データの入力インタフェースとして非常に可能性を感じました。

Personiumについては → [こちら](https://qiita.com/yoh1496/items/e4df4e3585af0158a246)

Personiumに興味を持っていただけた方、いらっしゃいましたら是非一緒に開発しましょう。


# 参考URL

- [Personium | オープンソースPDSソフトウェア](https://personium.io/)
- [LINE Developers](https://developers.line.biz/ja/)