LINEログインを、Node.js + Herokuでやってみた

お仕事でLINEログインをチームで実装する機会があったので、自分でもできるように小さなサービスを作ってみようと思います。

まずは、LINEログインボタンを表示し、それを押すとLINEログインの実行を行い、ログインしたアカウント情報を表示するところまで実装します。

サーバサイドはNode.jsを利用し、インフラとしてはHerokuを利用します。
（LINEからのcallback　URLを指定する必要があるので、Herokuが便利です。）

## 事前準備

Node: v12.13.1
Heroku CLI: 7.35.0

## Nodeアプリ環境の構築

https://github.com/noboru-i/line-note/commit/d0d935c6075b5f3f007b306d7aeaf14b2134355e
https://github.com/noboru-i/line-note/commit/94518c502c16b300e63bc8d4bdf73eb95d9b3d9d

まずは、サンプルを持ってきて、フォルダ名などをリネーム。

```
git clone https://github.com/heroku/node-js-getting-started.git
mv node-js-getting-started line-note
cd line-note
npm install
```

その後、READMEやpackage.jsonのアプリ名部分などを整理。

## Heroku環境の作成・仮デプロイ

```
heroku create line-note
git push heroku master
heroku open
```

これで、とりあえずサンプルのNode.jsアプリが動作していることを確認できた。

## LINE Developersにて、LINEログインチャンネルを作成

https://developers.line.biz/console/ より、個人のLINEアカウントでログイン。

適当な名前を入れてProviderを作成。

いろいろ適当に入力し、LINEログインチャンネルを作成。

"LINE Login"タブの"Callback URL"に、 https://line-note.herokuapp.com/callback を指定。

Herokuアプリの環境変数として、チャンネルID / チャンネルシークレットを保存しておく。

```
heroku config:set LINECORP_PLATFORM_CHANNEL_CHANNELID=(コンソールで確認できる"Channel ID")
heroku config:set LINECORP_PLATFORM_CHANNEL_CHANNELSECRET=(コンソールで確認できる"Channel secret")
```

ローカルでの確認用に、 `.env` にも同様に設定しておく。
（とはいえ、callbackが本番ドメインの方に来るので、ローカル環境は使いませんでした。）

```.env
LINECORP_PLATFORM_CHANNEL_CHANNELID=(コンソールで確認できる"Channel ID")
LINECORP_PLATFORM_CHANNEL_CHANNELSECRET=(コンソールで確認できる"Channel secret")
```

## コードの修正し、LINEログインを実装

https://github.com/noboru-i/line-note/commit/11af00edf4399acbc0452ba316d3a094031fd989

とりあえずログインボタンを作れればいいので、 `views/pages/index.ejs` を開いて、上の方にあるボタンを以下のように書き換える。

```html
<a type="button" class="btn btn-lg btn-default" href="/login">LINEログイン</a>
```

次に、 `/login` にアクセスされたときに、LINEログインのページにリダイレクトする仕組みを作っておく。
同様に、 `/callback` にアクセスが来たとき、リクエストパラメータの code を出力することにしておく。
`index.js` を、以下のように変更。

```js
const express = require('express')
const path = require('path')
const PORT = process.env.PORT || 5000

const querystring = require('querystring');

express()
  .use(express.static(path.join(__dirname, 'public')))
  .disable('etag')
  .set('views', path.join(__dirname, 'views'))
  .set('view engine', 'ejs')
  .get('/', (req, res) => res.render('pages/index'))
  .get('/login', (req, res) => {
    const query = querystring.stringify({
      response_type: 'code',
      client_id: process.env.LINECORP_PLATFORM_CHANNEL_CHANNELID,
      redirect_uri: 'https://line-note.herokuapp.com/callback',
      state: 'hoge', // TODO: must generate random string
      scope: 'profile',
    })
    res.redirect(301, 'https://access.line.me/oauth2/v2.1/authorize?' + query)
  })
  .get('/callback', (req, res) => {
    res.send('code: ' + req.query.code)
  })
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))
```

これを `git push heroku master` にてデプロイすると、ログインのフローが確認できる。
まだ、codeが見えるだけ。

## LINEの情報を取得する

https://github.com/noboru-i/line-note/commit/85ba05cae417ea5b004ff60fac1dc159c97f13b6
https://github.com/noboru-i/line-note/commit/d6e3f23e4a533147132c4ee613e57de746085d39

codeからaccess tokenを取得、それを利用してユーザ情報を取得する。

まずは、サーバからAPIを実行するために、requestモジュールを導入。

```
npm install request --save
```

そして、以下のように実装する。

```js
const request = require('request')

express()
  // ...
  .get('/callback', (req, res) => {
    request
      .post({
        url: `https://api.line.me/oauth2/v2.1/token`,
        form: {
          grant_type: "authorization_code",
          code: req.query.code,
          redirect_uri: 'https://line-note.herokuapp.com/callback',
          client_id: process.env.LINECORP_PLATFORM_CHANNEL_CHANNELID,
          client_secret: process.env.LINECORP_PLATFORM_CHANNEL_CHANNELSECRET,
        }
      }, (error, response, body) => {
        if (response.statusCode != 200) {
          res.send(error)
          return
        }
        request
          .get({
            url: 'https://api.line.me/v2/profile',
            headers: {
              'Authorization': 'Bearer ' + JSON.parse(body).access_token
            }
          }, (error, response, body) => {
            if (response.statusCode != 200) {
              res.send(error)
              return
            }
            res.send(body)
          })
      })
  })
  .listen(PORT, () => console.log(`Listening on ${ PORT }`))
```

これによって、以下のようなレスポンスが画面に表示される。

```
{
    userId: "XXX",
    displayName: "XXX",
    pictureUrl: "https://profile.line-scdn.net/ch/v2/p/XXX"
}
```

## 今回やらなかったこと

### access_token / refresh_token

`access_token` は30日で有効期限切れとなります。
`access_token` が失効したあと、10日以内に `refresh_token` を利用すると、再度 `access_token` と `refresh_token` を取得できます。

### stateのチェック

CSRF対策のため、 `/authorize` にアクセスする際のstateパラメータにランダムな文字列を生成して、 `/callback` で返却されたstateとチェックする必要があります。
セッションなどに一時的に保存して、チェックする必要があります。


## 参考

https://developers.line.biz/ja/docs/line-login/web/try-line-login/
line-login-starterというサンプルアプリの使い方が書いてある。

https://github.com/nkjm/line-login
LINEログインのunofficial SDK。今回は参考として。