LINE Messaging APIの新機能「ユーザーアカウント連携」を使ってみるサンプル(C#)

Line.Messaging v1.2.5 をリリースしました!

www.nuget.org

今回のアップデートでは、ユーザーアカウントの連携機能に対応しています。

ユーザーアカウント連携については以下の公式ドキュメントをご確認ください。
https://developers.line.me/ja/docs/messaging-api/linking-accounts

Messaging APIに追加された機能は以下の2つです。

  • アカウント連携イベントの追加

    • WebhookApplicationクラスにOnAccountLinkAsync 仮想メソッドを追加しました
  • アカウント連携用Link Tokenの取得関数

    • LineMessagingClientクラスにIssueLinkTokenAsyncメソッドを追加しました

ユーザーアカウント連携のサンプル

上記公式ドキュメントに記載の連携シーケンスに従って、サンプルアプリを作ってみました。

github.com

サンプルアプリは、以下の2つのアプリケーションで構成されています。

  • Botサーバー(Provider's Bot Server)

    • Azure FunctionでLINE BotのWebhookを処理するボットアプリ
  • 連携するWebサービス(Provider's Web Server)

    • ASP.NET Core MVC のテンプレートを用いたWebアプリ
    • ログインにメールアドレスとパスワードを使用する、テンプレートに組み込みの認証を使用します。 (プロジェクト作成時に「認証の変更」-「個人のユーザアカウント」-「アプリ内のストア ユーザーアカウント」を選択)

f:id:pierre3:20180417221708p:plain:w600

連携手順

サンプルでは、ユーザーからメッセージを受け取った際にアカウント連携の処理を行うようにします。
BOTサーバーのWebhook(BotFunction/LineBotApp.cs)のメッセージイベント(OnMessageAsync)で以下のような実装にしています。

  • アカウント連携済みでない場合
    • 任意のメッセージで「アカウント連携を開始」
    • テキストメッセージ"解除"で「アカウント連携を解除」
  • アカウント連携済みの場合
/// <summary>
/// メッセージイベント
/// </summary>
protected override async Task OnMessageAsync(MessageEvent ev)
{
    switch (ev.Message)
    {
        case TextEventMessage textMessage:

            //Azureストレージに保存しているユーザー情報を取得
            var status = await Status.FindAsync(
                partitionKey: BotStatus.DefaultPartitionKey,
                rowKey: ev.Source.Id);

            //アカウント連携していない
            if (string.IsNullOrEmpty(status?.AccountLinkNonce))
            {
                //連携開始
                await StartAccountLinkAsync(ev);
            }
            //アカウント連携済みの場合
            else
            {
             if (textMessage.Text == "解除")
                {
                    //連係解除
                    await UnlinkAsync(ev, status);
                    return;
                }
                //連携済みのWebサービスのAPIを利用
                await InvokeWebApiAsync(ev, status);
            }
            break;
    }
}

アカウント連携を開始する(BOT サーバー側)

連携開始処理は、BotFunction/LineBotApp.csで以下の様に実装しています。

/// <summary>
/// アカウント連携を開始する
/// </summary>
private async Task StartAccountLinkAsync(MessageEvent ev)
{

    //LINEサーバーからLink Tokenを取得
    var linkToken = Uri.EscapeDataString(await Line.IssueLinkTokenAsync(ev.Source.Id));
    //連携用のリンクをユーザーに返信
    await Line.ReplyMessageAsync(ev.ReplyToken, new[]
    {
        new TemplateMessage("account link",
            new ButtonsTemplate("アカウント連携をします。", null, "LINE Account Link", new[]
            {
                new UriTemplateAction("OK", $"https://lineaccountlinkapp.azurewebsites.net/Account/Link?linkToken={linkToken}")
            }))
        });
}

以降、公式ドキュメントの「アカウント連携を実装する」の記載内容に合わせて実装の流れを追っていきます。

1. 連携トークンを発行する

HTTP POSTリクエストを/bot/user/{userId}/linkTokenエンドポイントに送信して、連携しようとしているユーザー用の連携トークンを発行します。

//LINEサーバーからLink Tokenを取得
var linkToken = Uri.EscapeDataString(await Line.IssueLinkTokenAsync(ev.Source.Id));

2. ユーザーを連携URLにリダイレクトする

ボットからユーザーに、連携URLを開くメッセージを送信します。たとえば、URIアクションを設定したテンプレートメッセージに連携URLを指定します。このとき、ステップ1で取得した連携トークンをURLのクエリパラメータとして指定します。

//連携用のリンクをユーザーに返信
await Line.ReplyMessageAsync(ev.ReplyToken, new[]
{
    new TemplateMessage("account link",
        new ButtonsTemplate("アカウント連携をします。", null, "LINE Account Link", new[]
        {
            new UriTemplateAction("OK", $"https://lineaccountlinkapp.azurewebsites.net/Account/Link?linkToken={linkToken}")
        }))
    });

ユーザーから見ると、以下のようなテンプレートメッセージがリプライされてきます。

f:id:pierre3:20180416232818p:plain:w400

アカウント連携処理(Webサービス側)

ユーザーがテンプレートメッセージの「OK」をタップすると、"(WebサービスのBase URL)/Account/Link”にリダイレクトされます。

Webサービス側の実装は、LineAccountLinkApp/Controllers/AccountController.csLinkLinkEx メソッドになります。

3. 自社サービスのユーザーIDを取得する

ユーザーが連携URLにアクセスしたら、自社サービスへのログイン画面を表示します。ユーザーがサービスにログインすると、自社サービスのユーザーIDを取得できます。

Account/Link アクションでは、ログインページへのリダイレクトのみを行います。その際、ログイン成功後にアカウント連携を実行するアクション(/Account/LinkEx)へリダイレクトするようにreturnUrlパラメータを設定しておきます。
また今回は、ログイン済みのユーザーでも必ず再ログインを求める様にしています。

/// <summary>
/// アカウント連携
/// </summary>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Link([FromQuery]string linkToken)
{
    //ログイン済みでも再ログインを求める
    await _signInManager.SignOutAsync();
    //ログイン後、Account/LinkEx に遷移するようにreturnUrlを設定
    var returnUrl = Uri.EscapeDataString($"/Account/LinkEx?linkToken={linkToken}");
    //ログイン画面へリダイレクト
    return LocalRedirect($"/Account/Login?returnUrl={returnUrl}");
}

ユーザー側に表示されるログインページ

f:id:pierre3:20180416232837p:plain:w400

ログインに成功すると、returnUrlパラメータに設定していたAccount/LinkExのアクションへ遷移します。

4. ノンスを生成してユーザーをLINEプラットフォームにリダイレクトする

ステップ3で取得したユーザーIDからノンスを生成します。ノンスの要件は以下のとおりです。 - 予測が難しく一度しか使用できない文字列であること。セキュリティ上問題があるため、自社サービスのユーザーIDなどの予測可能な値は使わないでください。 - 長さは10文字以上255文字以下であること

ノンスとしてランダムな値を生成する際の推奨事項は以下のとおりです。 - 128ビット(16バイト)以上のデータで、セキュアなランダム生成関数(SecureRandomなど)を使う。 - Base64エンコードする。

今回は、セキュアな乱数の生成にRNGCryptoServiceProvider を使用しました。

//Nonceを生成
var rngcp = new RNGCryptoServiceProvider();
var bytes = new byte[32];
rngcp.GetBytes(bytes);
var nonce = Convert.ToBase64String(bytes);

ノンスは取得したユーザーIDと紐づけて保存します。たとえば、Redisのようなキーバリューストアを使う場合は、ノンスがキーで、ユーザーIDがバリューのペアを保存するとよいでしょう。

サンプルでは、組み込みのユーザー管理で使用しているSQLサーバーのDBにユーザーIDとNonceを保存するエンティティを追加しています。
(LineAccountLinkApp/Models/LineLink.cs, LineAccountLinkApp/Data/AccountLinkAppDbContext.cs)

 //ユーザーIDとNonceをDBに保存
var userId = User.FindFirst(ClaimTypes.NameIdentifier).Value;
var link = await _appDbContext.FindAsync<LineLink>(userId);

if (link == null)
{
    await _appDbContext.AddAsync(new LineLink()
    {
        Nonce = nonce,
        UserId = userId
    });
}
else
{
    link.Nonce = nonce;
    _appDbContext.Update(link);
}
await _appDbContext.SaveChangesAsync();

ノンスとユーザーIDを保存したら、ユーザーを以下のエンドポイントにリダイレクトします。

クエリパラメータにLink Token と Nonce を付けて、LINEサーバーへリダイレクトします。
(生成したNonce に/+ の文字が入っている場合があるため、System.Uri.EscapeDataStringメソッドでエスケープしています)

//生成したNonceを付けてLINEサーバーにリダイレクト
return Redirect($"https://access.line.me/dialog/bot/accountLink?linkToken={linkToken}&nonce={Uri.EscapeDataString(nonce)}");

5. アカウントを連携する

ボットサーバーでアカウント連携イベントを受信したら、連携処理を実行します。 Webhookイベントには、連携対象のLINEのユーザーIDとステップ4で生成したノンスが含まれています。このノンスを使って、ノンスと紐づけて保存しておいた自社サービスのユーザーIDを特定します。このIDとLINEのユーザーIDを連携すれば、アカウントの連携が完了します。

連携に成功していると、ユーザー側には以下のページが表示されます。

f:id:pierre3:20180416232937p:plain:w400

BOTサーバー側には、アカウント連携の結果はWebhookイベントOnAccountLinkAsyncに送られます。(BotFunction/LineBotApp.cs)

連携に成功した場合、イベントパラメータのLink.Resultに「OK」が、Link.NonceWebサービス側で発行したNonceが設定されてきます。
Botサーバー側でも、ユーザーID(こちらはLINEアカウントに紐づくID)とNonceを保存しておきます。

/// <summary>
/// アカウント連携イベント
/// </summary>
protected override async Task OnAccountLinkAsync(AccountLinkEvent ev)
{
    if (ev.Link.Result == LinkResult.Failed)
    {
        await Line.ReplyMessageAsync(ev.ReplyToken, $"アカウント連携に失敗しました....orz");
        return;
    }

    await Line.ReplyMessageAsync(ev.ReplyToken, $"アカウント連携に成功しました!");

    //連携に成功したらNonceを保存しておく
    await Status.UpdateAsync(
        new BotStatus(
            userId: ev.Source.Id,
            accountLinkNonce: ev.Link.Nonce));

これで、アカウント連携処理は完了です。

f:id:pierre3:20180416232948p:plain:w400

ユーザーアカウント連携の利用例

連携後の処理方法については、Webサービスの特性によっていろいろと考えられますが、今回は単純に「BOTからWebサービス側のAPIを実行」できるようにしてみたいと思います。
その際、連携時に取得したNonceをそのままAPIキーとして使用することとします。

以下は、Webサービス側に登録しているユーザーアカウントの情報を取得するAPIの実行例です。

Webサービス側では以下のようなAPIを実装しておきます。(LineAccountLinkApp/Controllers/ApiController.cs)

/// <summary>
/// ユーザー情報を取得する
/// </summary>
[HttpGet("user/info")]
public async Task<IActionResult> GetUserInfo([FromQuery]string nonce)
{
    //指定したNonceを持つユーザーを検索
    var link = _dbContext.Set<LineLink>().FirstOrDefault(o => o.Nonce == nonce);
    if (link == null)
    {
        return Forbid("Invalid account link nonce.");
    }
    //ユーザーIDからユーザー情報を取得して返す
    var user = await _userManager.FindByIdAsync(link.UserId);
    if (user == null)
    {
        return NotFound("User not found.");
    }
    return new JsonResult(new { user.Id, user.UserName, user.Email });
}

BOT側では、以下の様にパラメータにNonceを指定してAPIを呼び出します。(BotFunction/LineBotApp.cs)

/// <summary>
/// アカウント連携時に取得したNonceを利用してWebAppのAPIを実行
/// </summary>
private async Task InvokeWebApiAsync(MessageEvent ev, BotStatus status)
{
    //「Webサービスに登録しているユーザーアカウント情報」を取得するAPIを使用する
    //リクエストURLのパラメータにNonceを渡して実行する
    var nonce = Uri.EscapeDataString(status.AccountLinkNonce);
    var userInfo = await _httpClient.GetStringAsync($"https://lineaccountlinkapp.azurewebsites.net/api/user/info?nonce={nonce}");
    
    await Line.ReplyMessageAsync(ev.ReplyToken, userInfo);
}

上手くいけばユーザー情報がJSONで帰ってきます。今回は、結果をそのままユーザーに返信しています。

f:id:pierre3:20180416233022p:plain:w400

アカウント連携を解除する

連携解除について

アカウント連携機能を使う場合は、以下の2点を遵守してください。 - ユーザーに連携解除機能を必ず提供すること - ユーザーがアカウントを連携するときに、連携解除機能があることを通知すること

と、公式ドキュメントにも記載されている通り、連携解除機能を実装しておく必要があります。

Webサービス側のアカウント連携解除用APIを用意する

AccountControllerUnlinkアクションを追加します。
パラメータにNonceを受け取り、該当するレコードを検索してDBから削除します。

/// <summary>
/// アカウント連携解除
/// </summary>
[HttpDelete]
[AllowAnonymous]
public async Task<IActionResult> Unlink(string nonce)
{
    //指定されたNonceを持つユーザーを削除する
    var link = _appDbContext.Set<LineLink>().FirstOrDefault(o => o.Nonce == nonce);
    if (link == null)
    {
        return NotFound("User not found.");
    }
    _appDbContext.Remove(link);
    await _appDbContext.SaveChangesAsync();

    return Ok();
}

BotからWebサービス側の連携解除APIを呼び出す

BOT側では、ユーザーから"解除"のメッセージを受け取った場合に以下の処理を行います。

/// <summary>
/// アカウント連携を解除する
/// </summary>
private async Task UnlinkAsync(MessageEvent ev, BotStatus status)
{
    var ret = await _httpClient.DeleteAsync($"https://lineaccountlinkapp.azurewebsites.net/Account/Unlink?nonce={Uri.EscapeDataString(status.AccountLinkNonce)}");
    if (!ret.IsSuccessStatusCode)
    {
        await Line.ReplyMessageAsync(ev.ReplyToken, "アカウントリンクの解除に失敗しました。");
    }
    else
    {
        await Status.DeleteAsync(ev.Source.Type.ToString(), ev.Source.Id);
        await Line.ReplyMessageAsync(ev.ReplyToken, "アカウントリンクを解除しました。");
    }
}

まとめ

ここまでで、ユーザーアカウント連携に必要な手順の説明は終わりです。
今回の利用例では、連携することでWebサービス側のAPIを簡単に利用することができるようになりました。

次は、公式ドキュメントで推奨しているように、リッチメニューを利用して「連携」・「解除」を簡単にできるようにしてみたいと思います。

たとえば、Messaging APIを使えば、表示するリッチメニューをユーザーごとに変更できます。アカウントを連携していないユーザーにはアカウントを連携するメニューを表示し、アカウントを連携済みのユーザーには連携を解除するメニューを表示すれば、ユーザーにとって使いやすい形でアカウント連携機能を提供できるでしょう。