【Next.js】自サービスユーザとLINEユーザを連携してみた

Next.js×Firebaseで実装したWebアプリのユーザとLINEのユーザを連携してみたので、記事にまとめておきます。今後の機能の幅が広がりそうです！


# 処理の流れ
処理の大まかな流れとしては、LINEログイン→アクセス許可→連携という流れです。
処理の流れはすごい簡単だけど、設定しないといけないことが多いので、記事が長くなりそうです・・。
![](https://storage.googleapis.com/zenn-user-upload/e6e6d15bb4af-20230823.png)
![](https://storage.googleapis.com/zenn-user-upload/211a62e78364-20230823.png)

# LINE Developersの設定
LINEのDevelopersからLINEログインとMessaging APIを有効化し、設定します。

 ## 1.Messaging API
 Messaging APIを選択して、作成します。入力項目は全て任意の値で構いません。
 ![](https://storage.googleapis.com/zenn-user-upload/4a254fbb4c71-20230823.png)

 Meesaging APIでは、チャネル基本設定タブの「チャネルシークレット」とMessaging APIタブの「チャネル  アクセストークン」が必要です。ここでチャネルアクセストークンを発行しておきましょう。
 ![](https://storage.googleapis.com/zenn-user-upload/1f1e6d8f1821-20230823.png)

 これで、公式ラインのアカウントが作成されて、Messaging APIを使用することができます。
 次は、LINEと連携ができるように、LINEログインの設定をしていきます。

 ## 2.LINEログイン
 LINEログインを選択して、作成します。
 ここでは、「ウェブアプリ」を選択し、それ以外の入力項目は任意の値で構いません。
 ![](https://storage.googleapis.com/zenn-user-upload/4a254fbb4c71-20230823.png)
 
 リンクされたボット欄から、先ほど作成したMessaging APIで作成したチャネルを選択します。
 ![](https://storage.googleapis.com/zenn-user-upload/11798187b892-20230823.png)
 
 LINEログインのチャネルでは、「チャネルID」と「チャネルシークレット」の2つの値が必要になります。
 
ここで一旦、LINE Developersの設定は完了です。必要なキーたちはNext.jsで使用していきます！

# Next.js×FirebaseのWebアプリ
Next.jsとFirebaseを組み合わせたアプリについては、今後、記事にまとめようと思います（多分...）。

ここでは、前提として、Next.jsにAuth機能があり、Firestoreにユーザ情報が格納されていることが前提とします。
そのユーザ情報の項目として、LINE側のユーザIDを登録して、連携するような形を取ります。
LINEのユーザIDを使用することで、公式アカウントから対象のユーザにのみメッセージを送信することができます。
公式アカウントを追加しているユーザ全員にもメッセージが送信できるメソッドもあるので、それも今後、記事にまとめようと思います。
長くなりましたが、Next.js側の実装をしていきます。
1つ目の画面を実装していきます（ヘッダー等の説明は省きます）。
![](https://storage.googleapis.com/zenn-user-upload/1cf6b6f9be09-20230823.png)

コードは以下になります。`url`がだいぶ長いですが、この`url`でLINEログインのAPIを呼び出します。
```typescript: /line_login/page.tsx
import NextLink from 'next/link'

import { Box, Button, Flex, Text } from '@/common/design'

export default function LineLoginScreen() {
  const url = `https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_DOMAIN}/line_connect&state=hoge&bot_prompt=normal&scope=profile%20openid&nonce=foobar&prompt=consent`
  return (
    <Flex
      flexDirection='column'
      width='100%'
      justifyContent='center'
      alignItems='center'
      gap='10px'
    >
      <Button
        as={NextLink}
        href={url}
        color='white'
        bg='green.400'
        marginTop='20px'
        _hover={{ bg: 'green.500' }}
      >
        LINEへログインする
      </Button>
      <Box>
        <Text fontSize='12px'>
          LINEと連携することで評価コメントの通知等を受け取ることができます。
        </Text>
        <Text fontSize='12px'>
          LINEログインし、アクセスの許可後、連携ボタンを押下してください。
        </Text>
      </Box>
    </Flex>
  )
}
```
ここで、LINE Developers側で生成されたキーとLINEログイン後に戻ってくるリダイレクトURLを設定しないといけません。この2つの値を環境変数として、`.env`ファイルに変数を定義しています。
リダイレクURLは`redirect_uri=${process.env.NEXT_PUBLIC_DOMAIN}/line_connect`で設定しています。`process.env.NEXT_PUBLIC_DOMAIN`は環境変数として下記で宣言します。

```typescript
# LINE developers
NEXT_PUBLIC_LINE_CHANNEL_SECRET='{Messaging APIのチャネルアクセストークン}'
NEXT_PUBLIC_LINE_ACCESS_TOKEN='{Messaging APIのチャネルシークレット}'
NEXT_PUBLIC_CLIENT_ID='{LINEログインチャネルのチャネルID}'
NEXT_PUBLIC_CLIENT_SECRET='{LINEログインチャネルのチャネルシークレット}'

# 環境変数
NEXT_PUBLIC_DOMAIN='http://127.0.0.1:3000'
```

これで、ボタンを押下すると、LINEログイン画面（2つ目の画面）、自身のアカウントでログインすると、アクセス許可画面（3つ目の画面）へ遷移します。手順を進めて、アクセスを許可すると、リダイレクトURLに設定したURLに戻ってきます。

また、ここでLINE DevelopersのLINEログイン側のアカウントでコールバックURLを設定します。
![](https://storage.googleapis.com/zenn-user-upload/0481b182c03e-20230825.png)

リダイレクトURLは、`http://127.0.0.1:3000/line_connect`なので、下記ファイルを作成します。

```
src 
 └ app
 　　　　　|- /line_login
 　　　　　| 　　　　　└ page.tsx
 　　　　　└ /line_connect　(新規フォルダ)
 　　　　　　　　　　　　　　└ page.tsx（リダイレクト先のファイル）
```

リダイレクト先のLINE連携画面を実装します。
```typescript: line_connect/page.tsx
'use client'
import axios from 'axios'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'

import { HOME } from '@/common/constants/path'
import { Button, Flex, useToast } from '@/common/design'
import { updateUserInfoByLineId } from '@/lib/firebase/apis/user'

export default function LineConnectScreen() {
  const searchParams = useSearchParams()
  const code = searchParams!.get('code')
  const router = useRouter()
  const toast = useToast()
  const [btnState, setBtnState] = useState<boolean>(false)

  const getToken = async () => {
    var params = new URLSearchParams()
    params.append('grant_type', 'authorization_code')
    // 生成されたコードをパラメータに追加
    params.append('code', code as string)
    // リダイレクトURIをパラメータに追加
    params.append(
      'redirect_uri',
      `${process.env.NEXT_PUBLIC_DOMAIN}/line_connect`
    )
    // クライアントIDをパラメータに追加
    params.append('client_id', process.env.NEXT_PUBLIC_CLIENT_ID as string)
    // クライアントシークレットをパラメータに追加
    params.append(
      'client_secret',
      process.env.NEXT_PUBLIC_CLIENT_SECRET as string
    )

    // アクセストークン生成
    const token = await axios.post(
      'https://api.line.me/oauth2/v2.1/token',
      params
    )

    // IDトークンの検証
    var params_second = new URLSearchParams()
    // 生成されたトークンをパラメータに追加
    params_second.append('id_token', token.data.id_token)
    // クライアントIDをパラメータに追加
    params_second.append(
      'client_id',
      process.env.NEXT_PUBLIC_CLIENT_ID as string
    )
    // アクセストークンの検証
    const userdata = await axios.post(
      'https://api.line.me/oauth2/v2.1/verify',
      params_second
    )
    // コンソールで確認
    console.log(userdata)

    // ユーザー情報の更新
    await updateUserInfoByLineId(userdata.data.sub).then((res) => {
      if (res.isSuccess) {
        toast({
          title: res.message,
          status: 'success',
          duration: 3000,
          isClosable: true,
        })
        router.push(HOME.path)
      } else {
        toast({
          title: res.message,
          status: 'error',
          duration: 3000,
          isClosable: true,
        })
        setBtnState(true)
      }
    })
  }

  return (
    <Flex
      flexDirection='column'
      width='100%'
      justifyContent='center'
      alignItems='center'
      gap='10px'
    >
      <Button
        color='white'
        bg='green.400'
        marginTop='20px'
        _hover={{ bg: 'green.500' }}
        onClick={() => getToken()}
      >
        アカウントを連携する
      </Button>
      {btnState ? (
        <Button
          color='white'
          bg='green.400'
          marginTop='20px'
          _hover={{ bg: 'green.500' }}
          onClick={() => router.push(HOME.path)}
        >
          トップへ戻る
        </Button>
      ) : null}
    </Flex>
  )
}
```

コメントアウトでも少し説明してますが、大まかな流れとしては、アクセストークンを生成して、返ってきたトークンが有効か検証します。検証後、返り値からLINEのユーザIDを取得することができます。
アクセストークンは、ユーザ情報にアクセスする許可があるかどうかを示します。LINEログインした時に、アクセス許可が聞かれていたと思います。

検証後の返り値をコンソールで確認したら、構造が分かりますが、`userdata.data.sub`でLINE側のユーザIdを取得することができます。

取得したLINEのユーザIDを最後に、`updateUserInfoByLineId`メソッドからFirebaseのユーザドキュメントに追加します。
```typescript
/**
 * LINEのユーザ情報と紐付ける
 * @param lineId
 */
export const updateUserInfoByLineId = async (
  lineId: string
): Promise<FirebaseResult> => {
  let result = { isSuccess: false, message: '' }
  const uid = auth.currentUser?.uid!
  const docRef = doc(db, 'users', uid)
  await updateDoc(docRef, {
    lineId: lineId,
  })
    .then(() => {
      result = { isSuccess: true, message: 'LINEとの紐付けに成功しました' }
    })
    .catch((error) => {
      result = { isSuccess: false, message: error.message }
    })

  return result
}
```

# 動作確認
最後に、一連の処理の動作確認をしてみましょう。Firestoreが更新されていた成功です。
これで自サービスから公式アカウントを通して個人に対してメッセージも送信できちゃいます。
何か、ユーザに対して通知したい時とかに応用できそうですね！
![](https://storage.googleapis.com/zenn-user-upload/529533eb5d62-20230825.png)

# 参考

https://developers.line.biz/ja/reference/line-login/