nest.jsでLIFFアプリのトークンをセキュアに運用する

nest.jsでLIFFアプリのトークンをセキュアに運用する

ども!今回はLINE Front-end Frameworkから取得したアクセストークンとIDトークンを用いて、LIFFアプリからのアクセスを担保する方法とユーザIDを取得する方法について解説しました。トークンを用いて検証することでセキュアに運用することができます。公式もトークンを用いての運用をお勧めしてます!

導入

ども!いろんな業務を同時進行で進めていて、頭が程よくパンクしかけている龍ちゃんです。イベントの運営まで手を出して、だいぶ忙しいですね。忙しさの中に程よく息抜きとして、ブログがあるぐらいがちょうどいいですね。ブログの質向上も図っていきましょう。

さて、今回もLINE Front-end Frameworkにまつわるお話です。前回記事では、フロントエンドのアプリの作成とLIFFから情報を取得する部分を解説しました。

ReactでLINE Front-end Frameworkの初期開発

今回は、取得したアクセストークンとIDトークンを用いてセキュアに運用する方法とnest.jsで同実装するのか?という部分について書いていきます。

この記事でわかることは以下になります。

  • LINE Front-end FrameworkからのAPIアクセスをアクセストークンで確かめる(Guard)
  • IDトークンからアクセスしてきたユーザIDを特定する(Function)

それでは本題に入ってきましょう。

nest.jsでトークンを用いてセキュアに運用する

ここでは、バックエンド側との通信をどのようにセキュアに運用していくかについて解説して実装に入っていこうと思います。

LIFFのドキュメントにも記載されていますが、LIFFアプリで取得することができる情報でトークン以外をサーバー(バックエンド側)に送信することは気を付けるように忠告されています。その代わりにアクセストークンを使用して、LIFFアプリからのアクセスであることを担保して、ユーザー情報はIDトークンから取得する必要があります。イメージとしては以下になります。

LINE Front-end Frameworkとバックエンド間でどのようにトークンを運用するか

nest.jsからLINEに情報を問い合わせトークンを検証する

それでは、それぞれの方法について書いていきます。

アクセストークンを用いてLIFFアプリからのアクセスを担保する

こちらの実装では、nest.jsのGuardを用いてコントローラーの処理に挟み込みます。認証方法としては、Bearer Tokenの形式で送信して運用したいと思います。HeaderのauthorizationにTokenを付与して送信する方法ですね。

ドキュメントのこちらの部分を参考に構築しています。

import { CanActivate, ExecutionContext, Injectable, RawBodyRequest } from '@nestjs/common';

@Injectable()
export class IsLiffAuthenticatedGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<RawBodyRequest<Request>>();
    const token = this.extractTokenFromHeader(request);
    if (!token) return false;

    const accessTokenVerifyResponse = await fetch(`https://api.line.me/oauth2/v2.1/verify?access_token=${token}`);
    if (!accessTokenVerifyResponse.ok) return false;

    return true;
  }
  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers['authorization'].split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

リクエストから、生のリクエストを取得するにはmain.tsに設定を追記する必要があります。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    rawBody: true,
    bodyParser: true,
  });
  await app.listen(3000);
}
bootstrap();

あとは、LIFFアプリからAPIにアクセスするコントローラーにGuardとして設定すれば完了です。

IDトークンからユーザIDを取得する

ユーザ情報に関しては、アクセストークンを用いて取得する方法とIDトークンを検証して取得する方法の2つのパターンがあります。今回は、IDトークンを取得しているのでユーザー情報の代わりにIDトークンを送信する方法でユーザIDを取得する方法をまとめておきます。(公式リファレンスではこのページ

必要な情報としては、アクセス元のLIFFアプリのチャンネルIDが必要となります。コンソールのトップにあるので、必要な情報はそちらで取得しましょう。

try {
    const idToken = "xxxxxxxxxxxxxxxxxxx本当はめっちゃ長い"
    const channelID = "xxxxxxxxxxxxxxxxxxLIFFアプリのコンソールから取得"

    const response = await fetch('<https://api.line.me/oauth2/v2.1/verify>', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: `id_token=${idToken}&client_id=${channelID}`,
    });
    const userID = (await response.json())['sub'];
  } catch (e) {
    throw new Error('Unauthorized');
  }

もし、IDトークンの有効期限が過ぎていればエラーが戻ってきます。成功している場合は、body内の情報として、こちらのリンクに記載されている情報が返答されます。情報としては”sub”に含まれている情報が、こちらのIDトークンを発行したユーザIDとなります。nest.jsの共通Serviceとして切り出した場合は、以下のサンプルになります。

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { EnvironmentsService } from 'src/config/enviroments.service';

@Injectable()
export class TokenDecodeService {
  constructor(private readonly enviromentService: EnvironmentsService) {}
  async decodeUserID(idToken: string): Promise<string> {
    try {
      const response = await fetch('https://api.line.me/oauth2/v2.1/verify', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: `id_token=${idToken}&client_id=${this.enviromentService.ChannelID}`,
      });
      const data = (await response.json())['sub'];
      return data;
    } catch (e) {
      throw new UnauthorizedException();
    }
  }
}

まとめ

アクセストークンとIDトークンの両方が流出して、尚且つLIFFアプリケーションが開きっぱなしではない限りリソースにアクセスすることができない仕組みを実装することができました。

僕自身は、nest.jsのレベル自体はあまり高くありません。これからもまとめた内容を少しずつブログにしていきます。案件が始まりそうなところもありますが、始まったら始まったで月に8本程度は執筆できるように調整していきます。

これまでのLIFF関連の記事はこちらになります。

今回参考にした記事は以下です。

ではまた~

Twitterのほうもよろしくお願いします。

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です