(LIFF/React/AWS/IoT)LINEアカウントと自社サービスID/IoTデバイスを連携させる方法

# はじめに
IoTデバイスなどの自社サービス・IDとLINEアカウントを紐づけることで、通知やメッセージ送信など様々なことが可能になります。実装方法は公式リファレンスに記載されています。しかし、全て0から実装するとなると大変です。フロント・バックエンドが絡み合い、さらにはログイン画面も作らねばならず大掛かりな作業となります。ポイントを絞って実装内容を紹介致します。

# 事前準備
LIFFやLINEミニアプリなどの説明は割愛致します。
あらかじめ登録が必要なものです。

## LINE Developersの準備

下記2点を先にLINE Developersで登録しておきましょう。
この時点ではLIFFのエンドポイントはなくても大丈夫です。

* LINE ログイン
* Messaging API
* LIFFアプリの環境構築

https://developers.line.biz/ja/

#### LIFFアプリの環境構築

今回はReactで実装してます。下記コマンドを実装することでLIFFアプリを構築することができます。
```
$ npx @line/create-liff-app
```

https://developers.line.biz/ja/docs/liff/cli-tool-create-liff-app/



# 実装方法

## 公式フロー図
公式リファレンスは下記のようになります。この通りに進めれば問題ありません。

https://developers.line.biz/ja/docs/messaging-api/linking-accounts/#account-link-sequence

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342722/e5f4a3ee-6950-c973-4a1a-44f80dcc7205.png)

## フロー図をより具体化したもの

* フロントをLIFF(React) + AWS amplify
* バックエンドをLambda
* DBをDynamoDB
とした際のフロー図はこちらになります。



![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342722/9abb487f-12be-43a6-b018-45577ef34310.png)

### ゴールはどこか？
#### フロント(LIFF+React)側
このフローの最終目標は```連携用トークン```と```nonce```を入手してユーザに下記エンドポイントにリダイレクトさせることが最終目的になります。

```
https://access.line.me/dialog/bot/accountLink?linkToken={LINE ID連携用トークン}&nonce={nonce}
```

#### バックエンド(AWS)側
DynamoDBなどのDBへ自社IDとLINEユーザIDを紐づけて登録することです。2回レコードの登録をする必要があります。

```Name:本レコード
nonceをキーにして仮レコードを上書きする
・自社ID
・LINEID（本登録）
・timestamp
・・・・・
```



## 連携手順

### 各トークンの概要

* access_token： liff.getAccessToken()で取得できる短期トークン
* channel_access_token = 各チャンネル固有の長期トークン
* client_id = 各チャンネル固有の長期ID
* 連携用トークン(link_token) = userIDとchannel access tokenを使用して入手する短期ID
* ログイントークン = ログインに成功した際に発行される短期トークン

### ①LIFFアプリを開く
React等でLIFFアプリを作成してデプロイしましょう。そのLIFFアプリをチャンネルに登録することでアクセスできるようになります。

https://developers.line.biz/ja/docs/liff/registering-liff-apps/


### ②アクセストークン(access_token)を取得する

ログインした状態でLIFFアプリを立ち上げる。その状態で```liff.getAccessToken()```を実行することで短期のアクセストークンを取得することができます。

### ③連携用トークン(link_token)を取得する lambda①

#### ③-① アクセストークンを検証


::: note warn
フロント側のLIFF(React)ではトークン認証は行えません。Lambdaなどのバックエンドに処理させるようにしましょう。
:::

検証コマンド+pythonでの実装例

```curl!
curl -v -X GET 'https://api.line.me/oauth2/v2.1/verify?access_token=eyJhbG~~~'
```

```python
import requests

params = {
    'access_token': 'eyJhbG~~~',
}

response = requests.get('https://api.line.me/oauth2/v2.1/verify', params=params)
```

レスポンス
```json
{"client_id":"1234567890","expires_in":11111,"scope":"profile openid"
```

#### ③-② ユーザIDの取得

検証済みのアクセストークンを使用します。

検証コマンド+pythonでの実装例

```curl!
curl -v -X GET https://api.line.me/v2/profile -H 'Authorization: Bearer eyJhb~~~'
```

``` python
import requests

headers = {
    'Authorization': 'Bearer eyJhb~~~',
}

response = requests.get('https://api.line.me/v2/profile', headers=headers)
```

レスポンス
```json
{"userId":"Uc000000~~","displayName":"なまえ","pictureUrl":"https://profile.line-scdn.net/hogehogehoge~~~"}
```

#### ③-③ 連携用トークン(link_token)を取得

検証コマンド+pythonでの実装例

先ほど取得した```useID```とチャンネル固有の```channel access token```を使用します。

```curl!
curl -X POST https://api.line.me/v2/bot/user/{userId}/linkToken \
-H 'Authorization: Bearer {channel access token}'
```

```python
import requests

headers = {
    'Authorization': 'Bearer {channel access token}',
}

response = requests.post('https://api.line.me/v2/bot/user/{userId}/linkToken', headers=headers)

```


レスポンス
```
{"linkToken":"0123456789123456789abcdefg~~~~~~"}
```


### ④自社ログイン画面へ遷移
このタイミングで自社サービスのログイン画面へ遷移します。
例としてAWS cognito + amplifyで実装できますので下記記事を参考にして頂き実装してみてください。

私が実装した例。cognito+amplifyで実装してます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342722/f63414e5-0ed4-2cd8-4e62-ee5c261d2797.png)


参考記事

https://blog.denet.co.jp/amplify-cognito-react/


### ⑤nonceを生成し、仮レコードとして登録する　(lambda②)

ログインに成功したタイミングで下記内容を実行します。
* nonceの生成
* 仮レコードをDynamoDBに登録

#### ⑤-① nonceの生成

nonceは最終的には削除されるものですが、LINEユーザIDと自社IDを紐づけるために一時的に必要なセキュアなキーの役割を果たします。

公式リファレンスによるnonceの要件は下記の通りです。

> 予測が難しく一度しか使用できない文字列であること。セキュリティ上問題があるため、自社サービスのユーザーIDなどの予測可能な値は使わないでください。
> 長さは10文字以上255文字以下であること
> nonceとしてランダムな値を生成する際の推奨事項は以下のとおりです。
> 
> 128ビット（16バイト）以上のデータで、セキュアなランダム生成関数を使う。
> Base64エンコードする。
> nonceは取得したユーザーIDと紐づけて保存します。

方法は要件以内で任意となります。例としてIoTデバイスなどの自社IDがある場合はそのIDをオリジナルとして、加工するのが手っ取り早いと思います。

参考記事

https://developers.line.biz/ja/docs/messaging-api/linking-accounts/#step-four-verifying-user-id


#### ⑤-②仮レコードを登録

ここでは下記内容をDynamoDBへ書き込みます。

```Name:仮レコード
① 自社ID
② nonce
```

### ⑥連携完了ページへリダイレクトする

フロントの処理は最後の工程になります。
③と⑤で取得した```link_token```と```nonce```を使用して、下記連携完了ページへリダイレクトさせます。

```
https://access.line.me/dialog/bot/accountLink?linkToken={LINE ID連携用トークン}&nonce={nonce}
```

#### ハマったポイント
LIFFブラウザでそのままリダイレクトさせようとすると多重ループになる場合があります。```liif.open```など使い、外部ブラウザで開くように誘導を行うことで回避できます。下記私の記事です。

https://qiita.com/taiyyytai/items/03775fdaa8a8e1896e2a

:::note info
**フロント(LIFF+React)側はこれにて終了です。**
:::

### ⑦結果通知 & 本レコード登録(lambda③)

アカウント連携が成功すると、ユーザには成功画面が出て、裏ではwebhookに通知がされます。
予めlambda等でエンドポイントを作成し、その内容からnonceをキーとして仮レコード内を検索して本レコードへ上書きしましょう。

**LINE message API設定**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/342722/11b6e381-0a3a-2297-0033-ce2c6c0215c6.png)


LINEサーバから通知されるJSON
``` json
{
  "destination": "xxx",
  "events": [
    {
      "type": "accountLink",
      "link": {
        "result": "ok",
        "nonce": "ID連携前に作成したユニークな文字列"
      },
      "timestamp": 1647936877429,
      "source": {
        "type": "user",
        "userId": "対象のLINEユーザーID"
      },
      "replyToken": "xxx",
      "mode": "active"
    }
  ]
}
```

```Name:本レコード
nonceをキーにして仮レコードを上書きする
・自社ID
・LINEID（本登録）
・timestamp
・・・・・
```

#### (おまけ)Pythonでwebhookテストする方法
AWSなどではテストイベントを作ることができますが、実際にエンドポイントを叩いて確認したい場合は下記コードを実行してください。

```python
import requests
import json

headers = {
    'Content-Type': 'application/json',
}

json_data ={
    "destination": "xxx",
    "events": [
        {
            "type": "accountLink",
            "link": {
                "result": "ok",
                "nonce": "ID連携前に作成したユニークな文字列"
            },
            "timestamp": 1647936877429,
            "source": {
                "type": "user",
                "userId": "LINEuserID"
            },
            "replyToken": "xxx",
            "mode": "active"
        }
    ]
}

print(json.dumps(json_data).encode("utf-8"))

response = requests.post('https://(webhookエンドポイント)', headers=headers, json=json_data)
print(response.text)

```

# おわりに
無事アカウント連携できましたでしょうか？ 私はIoTデバイスとアカウント連携がしたく実装したのですが、ハマるポイントが多く、情報も多くないので苦労しました。誰かの役に立てば幸いです。