[LINE Bot] LIFFとリッチメニューでも管理画面が作りたい! 3 -認証編-

この記事はこの記事の続きです
[[LINE Bot] LIFFとリッチメニューでも管理画面が作りたい! 2 -リッチメニュー切替-](https://qiita.com/alivelime/items/2d16441153af9c444619)

# ここまでのまとめ
さて、ここまでで「認証用のLINEグループを作る」「グループへの招待・退出」でメニューを切り替える、というところまでできました。
あとはLIFFで作った管理画面に飛ばせばOKですが、当然そのさきのAPIでも認証をかけないといけません。

#### 構成図
ちなみに構成は次のようになっています。

- LIFF : ユーザ用メニューと管理者メニュー (React)
- bot : linebotのロジック(Go)
- line_api : LIFFからLINEの認証などのLINE固有の処理をするAPI (Go)
- omoinas : アプリケーションのメインロジックやmodel・repository層など (Rust)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/d5568e1a-fc57-ffc8-cd6f-525539333fb1.png)

# LIFFでの認証の考え方
今回はログインしているユーザのLINE IDを見て管理者かどうかを判別します。

さて、LIFFでLINE IDを取得するにはsocial api 2.1 を使えば良さそうです。
が、[公式マニュアル](https://developers.line.biz/ja/docs/line-login/secure-login-process/#%E5%AE%89%E5%85%A8%E3%81%AB%E9%80%81%E5%8F%97%E4%BF%A1%E3%81%A7%E3%81%8D%E3%82%8B%E6%83%85%E5%A0%B1%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)にもあるように、
LIFFでLINEのUserIDを取得して、それを直接サーバに送り付けてはいけません。
(直接、ユーザIDヲ指定してAPIを叩けてしまうとセキュリティ的によろしくない)

上記のドキュメントにある通り、LIFFではアクセストークンを取得し、サーバー側でプロフアイルを取得してユーザIDを確認します。

# LIFFとWebページを作る
LIFFを使うには、LINE Developers から新しく「LINE Login」チャネルを作成します。
なお、現在はLine Messaging APIでLIFFアプリは作れません。

今回はReactで作りました。

#### Reac/TypescriptでLIFFを作るためのあれこれ
意外と面倒です。。。
今回は create-react-appで作りましたが、はじめGatsbyあたりを使おうとして色々読み込み方がわからず、というのがありました。。

#### LIFFを読み込む

```html:public/index.html
<html>
  <head>
    <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script>
  </head>
  ...
</html>
```
#### Typescript用の定義ファイルを追加する。

```shell
npm install --save liff-type
```

#### 環境変数にLIFFのIDを書く

```shell:.env
# 開発用
REACT_APP_DEV_LIFF_ID="xxxx"

# 本番環境用
REACT_APP_PRD_LIFF_ID="yyyy"
```

#### 環境変数の切り替えについて
LIFFの開発では、UIの確認程度ならローカルでもできますが、
結局はサーバー上で確認しないとけないため、開発環境と本番環境の構築と切り替えをしないといけません。

process.env.NODE_ENVは、 npm startではdevelopmentになりますが、
npm run build すると強制的に prodcutionになる為、
今回は REACT_APP_ENVという変数を使い、npm run buildするときに環境変数を指定することにしました。

```shell
# 開発環境用の変数でビルドする
REACT_APP_ENV=dev npm run build
```


#### LIFF初期化
LIFF初期化の注意点としては、liff.login()をかける前はURLがエンコードされており、そのままではルーティングされません。
そのため、Routerの外側でまずLIFFの初期化を行い、LIFFがURLをデコードするときちんとルーティングされます。

```react:src/App.tsx
import React from 'react';
import {
  BrowserRouter as Router,
  Route,
  Switch,
} from 'react-router-dom';


... 何やらかんやら初期化

function App() {
  liff.init({ liffId: (process.env.REACT_APP_ENV === "prd"
    ? process.env.REACT_APP_PRD_LIFF_ID
    : process.env.REACT_APP_DEV_LIFF_ID)as string }).then(() => {})
  return (
    <MuiThemeProvider theme={theme}>
      <CssBaseline />
      <Router>
        <Switch>
          <Route path="/" exact>
            This is liff.
          </Route>
          <Route path="/:env/user/omise/:clientId/:omiseId" exact>
            <ユーザ用のページ />
          </Route>

          <Route path="/:env/staff/omise/:clientId/:omiseId" exact>
            <管理用ペーシ />
          </Route>
        </Switch>
      </Router>
    </MuiThemeProvider >
  );
}

export default App;
```

#### 管理者用のページでアクセストークンを取得する
[公式ドキュメント](https://developers.line.biz/ja/docs/line-login/secure-login-process/#%E5%AE%89%E5%85%A8%E3%81%AB%E9%80%81%E5%8F%97%E4%BF%A1%E3%81%A7%E3%81%8D%E3%82%8B%E6%83%85%E5%A0%B1%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)に従い、LIFFの初期化とアクセストークンの取得をします。

```react:src/StaffOmise.rs
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom';
import { useForm, Controller } from "react-hook-form";

// ↓サーバサイドのAPI
import {getOmise, Omise, setOmise, OmiseForm} from 'utils/api/omise';

... 色々コンポーネント読み込みとか

function StaffOmise() {
  console.log("aaaa")
  const {env, clientId, omiseId, charaId} = useParams<RouteParams>();

   ...

   // ロード時にデータ読み込む処理
  const load = () => {
    getOmise(env, clientId, omiseId, (omise: Omise) => {
       ... // フォームへの値の設定とか
    })
  }

    // 初期化処理
    useEffect(() => {
    liff.ready.then(() => {
      let accessToken = ""
      if (!liff.isLoggedIn()) {
         // ログインしていなければログインさせる。(ローカル環境ではスルーする)
        if (process.env.NODE_ENV === "production") {
          liff.login({})
        }
      } else {
        accessToken = liff.getAccessToken()
        setToken(accessToken)
      }
      load()
    })
  },[env, clientId, omiseId, charaId])

  return {
    ...フォームとか
  } 
}
export default StaffOmise;
```

#### API呼び出す時にLINEのトークンを付与する
上記でアクセストークンを取得したので、APIのヘッダーにつけます。

```react:src/utils/api.tsx
// 更新API
export function setOmise(token: string, ...パラメータ) {
  fetch (
   env === 'prd'
      ? `${process.env.REACT_APP_PRD_LINE_API_HOST}/line-api/omise/set`
      : `${process.env.REACT_APP_DEV_LINE_API_HOST}/line-api/omise/set`,
    {
      method: "POST",
      mode: "cors",
      cache: "no-cache",
      headers: { 'Authorization': 'Bearer: '+token},
      body: JSON.stringify({
        ...更新するデータ
      }),
    }
  )... // thenとかcatchとか
}

```

# サーバサイドで認証する
これでLIFFからLINEのアクセストークンが送信されますので、これを元にLINEのユーザIDを取得します。

```go:line_api/set_omise.go

func setOmise(request events.APIGatewayProxyRequest) (string, error) {
    // ちょっと雑ですが、ヘッダーからアクセストークンを取得します。
    accessToken := strings.TrimPrefix(request.Headers["Authorization"], "Bearer: ")
    if accessToken == "" {
        // 401
        return "", errors.New("access token is empty.")
    }

    // ユーザ名取得
    prof, err := client.GetUserProfile(accessToken).Do()
    if err != nil {
        log.Println(err)
        return "", err
    }

    param = {...}
    param.UserID = prof.UserID

    ... // ラムダでRustの更新APIを呼び出し、そこでDynamoDBからユーザIDをチェックする。
    payload, _ := json.Marshal(param)
    res, err := lambda.New(session.New()).Invoke(&lambda.InvokeInput{
        FunctionName:   aws.String(ARN + "hoge-" + os.Getenv("ENV") + "-setOmise"),
        Payload:        payload,
        InvocationType: aws.String("RequestResponse"),
    })
    if err != nil {
        log.Println(err)
        return "", err
    }
}
```

(Rustで書いたラムダは当然、このAPIからしか呼び出せないように設定しておきます)

# まとめ
以上で、LIFFで管理画面を作ってリッチメニューを切り替えたり認証したりする方法を紹介しました。
システムを開発すると、何かにつけて管理画面が必要になりますし、
作ったら作ったで、やれスマホ対応しろだのメアド・パスワード機能をつけろ、というのが世の常です。
ですが、管理画面なんてそもそもシンプルな方が良いですし、パスワード管理なんてしたくありません。
(Auth系サービスもありますが、結局、招待やらアカウント管理やら手間がかかります)

できるだけシンプルにしたい! という発想からの試みでした。