📗
既存サービスのユーザーとLINE公式アカウントのユーザーを紐付ける実装例 by Remix
前書き
運営しているサービスについて、LINE公式アカウント(以下、公式アカウント)をユーザー訴求などで利用することがあるかと思います。
そのような既にリリースしているサービスのユーザー情報
と、関連する公式アカウントに登録しているLINEユーザー
を紐付けるための実装について、公式のガイドラインに従って解説していきます。
公式アカウントとは?
実装ガイドライン
※記載の内容は執筆時点(2025/02/10)のものとなります。
前提
- 公式アカウントの
Messaging API
という機能を使用します。 - Messagin APIで提供してくれるものは
公式アカウントのユーザーと既存サービスのユーザーを安全に紐付ける導線
となります。そのため、連携済みの定義
、及び連携処理
については自前で設計、実装が必要となります。
要件
ここでの実装例の要件を記します。
※あくまで一例であり、詳細な各要件はお持ちのサービスに合わせて設計してください。
- LINEユーザー連携済みの定義
- userテーブルの
lineId
カラムにLINEユーザーIDが格納されている
- userテーブルの
- 連携実行のトリガー
- ユーザーが公式アカウントを追加した時
- DBテーブル定義
- user: ユーザーの管理
- user_line_nonce: 発行するnonceの管理
- session: ログインセッションの管理
- 実装画面
- ログイン画面:
/login
- TOP画面:
/
- 連携画面:
/link
- ログイン画面:
- 実装API
- Webhook API:
/webhook POST
- Webhook API:
準備
-
Messaging API
を有効にしたLINE公式アカウントの作成- Webhook URLの設定が必要です。(実装 > 1 の実施時に設定でも問題ありません。)
- ローカル環境で動かす場合にはngrokなどを使用して、外部にアドレスを公開して設定する必要があります。
実装
公式記載のシーケンスはこちらになります。
こちらを、幾つかのセクションに区切って解説、実装例を記していきます。
引用: https://developers.line.biz/ja/docs/messaging-api/linking-accounts/#account-link-sequence
※実装例はRemixと公式のSDKを使用して書かれています。
※userデータは格納済みの前提での記載となります。
1. 連携トークンの発行と連携URLの生成
実装するもの
- Webhook API
実装例
Webhook API
app/routes/webhook.ts
import {
LINE_SIGNATURE_HTTP_HEADER_NAME,
type WebhookEvent,
validateSignature,
} from "@line/bot-sdk";
import { type ActionFunctionArgs, json } from "@remix-run/node";
import { getMessagingApiClient } from "~/lib/lineBot";
import { prisma } from "~/lib/prisma";
export const action = async ({ request }: ActionFunctionArgs) => {
if (request.method !== "POST") {
return json({ message: "Method not allowed" }, 405);
}
// 1. リクエストの検証
const signature = request.headers.get(LINE_SIGNATURE_HTTP_HEADER_NAME) || "";
const body = await request.clone().text();
if (
!validateSignature(body, process.env.LINE_CHANNEL_SECRET || "", signature)
) {
return json({ message: "Invalid signature" }, 403);
}
// 2. イベントの処理
const payload = await request.json();
const events = payload.events as WebhookEvent[];
for (const event of events) {
switch (event.type) {
case "follow": {
try {
const client = getMessagingApiClient();
if (event.source.type !== "user") {
break;
}
// 3. 連携トークンの発行
const linkTokenResponse = await client.issueLinkToken(
event.source.userId,
);
// 4. 公式アカウントへ連携画面のURLを送信
await client.replyMessage({
replyToken: event.replyToken,
messages: [
{
type: "text",
text: "友達追加ありがとうございます!\nアカウントを連携してください!",
},
{
type: "template",
altText: "アカウントを連携する",
template: {
type: "buttons",
text: "アカウントを連携する",
actions: [
{
type: "uri",
label: "アカウントを連携する",
uri: `${process.env.FRONTEND_URL}/link?linkToken=${linkTokenResponse.linkToken}`,
},
],
},
},
],
});
} catch (e) {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
console.error((e as any).status);
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
console.error((e as any).body);
}
break;
}
// -- 省略 --
}
}
}
return json({ success: true }, 200);
};
解説
-
- 処理を行う前に、本当にWebhookからのリクエストであるか検証を行います。
- 検証方法については公式の方針に基づいて実装しています。
- https://developers.line.biz/ja/docs/messaging-api/receiving-messages/#verify-signature
-
- Webhookのイベントを処理します。ここでは公式アカウント追加時に連携処理が実行されるように実装しています。
-
- 連携トークンを発行します。
-
- 連携ページへのURLを公式アカウントへ送信します。その際、発行したトークンをクエリパラメータとして設定しています。
※連携トークンの発行には以下のエンドポイントを使用しますが、ここではjsのsdkの関数を使って呼び出しています。
2. 連携画面へのアクセス
1で送信したURLへアクセスされた後の処理を実装します。
実装するもの
- 連携画面
実装例
連携画面
app/routes/link.tsx
import crypto from "node:crypto";
import { type LoaderFunctionArgs, redirect } from "@remix-run/node";
import { prisma } from "~/lib/prisma";
import { getSession } from "~/lib/session";
export async function loader({ request }: LoaderFunctionArgs) {
const linkToken = new URL(request.url).searchParams.get("linkToken");
if (!linkToken) {
return redirect("/", { status: 400 });
}
const session = await getSession(request.headers.get("Cookie"));
// 1. 認証状態の検証
if (!session.has("userId")) {
return redirect(
`/login?r=${process.env.FRONTEND_URL}/link?linkToken=${linkToken}`,
);
}
try {
const nonce = getNonce();
// 2. 生成したnonceを保持
await prisma.user_line_nonce.upsert({
where: {
userId: session.get("userId") as number,
},
update: {
// 1000 * 60 * 60 = 1 hour
expiresAt: new Date(Date.now() + 1000 * 60 * 60),
nonce,
},
create: {
userId: session.get("userId") as number,
nonce,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
},
});
// 3. 連携実行
const linkUrl = `https://access.line.me/dialog/bot/accountLink?linkToken=${linkToken}&nonce=${nonce}`;
return redirect(linkUrl);
} catch (e) {
console.error(e);
return redirect("/", { status: 500 });
}
}
/**
* セキュアなランダム生成関数を使う。
* 少なくとも128ビット(16バイト)以上にする。
* Base64エンコードする。
*/
const getNonce = () => {
return crypto.randomBytes(16).toString("base64");
};
解説
-
- 認証状態を検証し、未認証であればログイン画面へリダイレクトさせます。
-
- nonceを生成し、それを認証ユーザーと紐付け可能な形で保持します。
- nonceの要件については公式の推奨がございます。
- https://developers.line.biz/ja/docs/messaging-api/linking-accounts/#step-four-verifying-user-id
-
- 連携トークンとnonceをクエリパラメータに加え、LINE側で提供している連携URLへリダイレクトさせます。
3. 連携成功後の処理
連携が成功した後の処理を実装します。
実装するもの
- Webhook API
- 1で実装したものに連携イベントの処理を追加します。
実装例
app/routes/webhook.ts
export const action = async ({ request }: ActionFunctionArgs) => {
// 省略
case "accountLink": {
try {
// 1. 成功可否を検証
if (event.link.result !== "ok") {
throw new Error("Invalid nonce");
}
// 2. nonceを検証し、紐付けたユーザー情報を取得
const userLineNonce = await prisma.user_line_nonce.findFirst({
where: {
nonce: event.link.nonce,
expiresAt: {
gt: new Date(), // 有効期限が過ぎていないか
},
},
});
if (!userLineNonce) {
throw new Error("Invalid nonce");
}
// 3. LINEユーザーIDを保存する
await prisma.user.update({
where: {
id: userLineNonce.userId,
},
data: {
lineId: event.source.userId,
},
});
const client = getMessagingApiClient();
await client.replyMessage({
replyToken: event.replyToken,
messages: [
{ type: "text", text: "アカウント連携が完了しました!" },
],
});
} catch (e) {
const client = getMessagingApiClient();
await client.replyMessage({
replyToken: event.replyToken,
messages: [
{
type: "text",
text: "アカウント連携に失敗しました。再度お試しください。",
},
],
});
}
break;
}
// 省略
}
解説
-
- パラメータに成功可否が含まれているので、それを元に結果を検証します。
-
- 2で発行したnonceがパラメータに含まれているので、それを元にuserテーブルのレコードを見つけます。
-
- 該当のレコードのカラムに、パラメータに含まれているLINEユーザーIDを格納します。
LINE側での連携判定が成功した場合には、以下のような画面が表示されます。
完成
- 公式アカウントの追加からのフローを実施し、最終的にLINEユーザーIDがDBに格納されていれば成功です。
- なお、実際に運用する場合には連携解除の処理と、連携解除が可能な旨をユーザーに事前通知する必要がありますが、特記事項などはないためここでは割愛しております。
余談
以下が全体の実装サンプルとなります。
後書き
公式のドキュメントを読んだだけでは分かりづらいと思う箇所が多々あり、実装例を交えて解説を記してみました。
皆さんの実装の一助となれば幸いです。
Discussion