daikiojm’s diary

ブログを書くときがやってきたどん!

Slack Appの認証にAPI GatewayのCustom Authorizerを使おうと思ったら使えなかった話

Serverless Frameworkを使うと、API Gateway + Lambdaを使ってCustom Authorizerを簡単に実装することができる。
Slack のSlash Commandsの認証にこれを使おうと思ったけど使えなかった話と、Custom Authorizerを使わないで実装した話。

Slack Appの認証について

最新のSlack API(2019/09/23 現在)では、リクエスト署名方式が使われており、SlackからのPOSTリクエストを受ける側のアプリケーションではこの署名を検証する必要がある。
実際に送信されるリクエストには、 X-Slack-Signature というHTTPヘッダーが付加されていて、この内容が署名になっている。
詳しい検証方法はドキュメントにもあるので触れないが、検証には次の情報が必要になる。

  • APIバージョン番号 (v0)
  • Slack Appの設定ページから取得できる、共有シークレット Signing Secret
  • リクエストbodyの内容
  • リクエスト時のタイムスタンプ ( X-Slack-Request-Timestamp HTTPヘッダーに付加されている)

Custom Authorizerを使おうと思った...

注意: 以下で紹介する方法は動作しません

serverless.yaml

functions:
  hello:
    handler: handler.slashCommand
    events:
      - http:
          method: post
          path: slashCommand
          cors: true
          authorizer:
            name: slashCommandAuthorizer
            identitySource: method.request.header.X-Slack-Signature, method.request.header.X-Slack-Request-Timestamp
            type: request
  slashCommandAuthorizer:
    handler: handler.slashCommandAuthorizer

handler.ts

export async function slashCommandAuthorizer(
  event: APIGatewayEvent & CustomAuthorizerEvent,
  context: Context,
): Promise<CustomAuthorizerResult> {
  const result: CustomAuthorizerResult = {
    principalId: '*',
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: 'Deny' as Effect,
          Resource: 'arn:aws:execute-api:*:*:*/*/*/',
        },
      ],
    },
  };

  const timestamp = event.headers['x-slack-request-timestamp'];
  const signature = event.headers['x-slack-signature'];
  const body = event.body;

  // verify slack secret.
  const isValidSecret = verifySlackSignature(timestamp, signature, body);

  if (isValidSecret) {
    result.policyDocument.Statement[0].Effect = 'Allow' as Effect;
  } else {
    result.policyDocument.Statement[0].Effect = 'Deny' as Effect;
  }

  return result;
}

serverless.yamlのfunctionsの設定をしているときにドキュメントを見ながら、identitySourceにbodyを設定できないことを知ったあたりから薄々怪しさを感じていたもの、
念の為、 slashCommandAuthorizer が受け取っているevent内容をconsole.logで出力してCloudWatchLog上から覗いてみることに...
しかし、この時点でCustom Authorizerで扱えるのはheaderのみと認識し軌道修正。
どうやら、aws-sdkのインターフェースにも定義されている通り、Custom Authorizerに設定したLambdaには次のようなイベントが渡ってきており、その中にbodyは含まれないようです。

// API Gateway CustomAuthorizer "event"
export interface CustomAuthorizerEvent {
    type: string;
    methodArn: string;
    authorizationToken?: string;
    resource?: string;
    path?: string;
    httpMethod?: string;
    headers?: { [name: string]: string };
    multiValueHeaders?: { [name: string]: string[] };
    pathParameters?: { [name: string]: string } | null;
    queryStringParameters?: { [name: string]: string } | null;
    multiValueQueryStringParameters?: { [name: string]: string[] } | null;
    stageVariables?: { [name: string]: string };
    requestContext?: APIGatewayEventRequestContext;
    domainName?: string;
    apiId?: string;
}

代替案

Custom Authorizerを使わずにそれぞれのhandlerで verifySlackSignature メソッドを呼び出す実装にした。

// handler.ts
export async function slashCommand(event: APIGatewayEvent, context: Context): Promise<any> {
  try {
    // ここで authorizeRequest を直接呼び出す
    authorizeRequest(event);

    // メインのロジック
    // ...

  } catch (e) {
    if (e instanceof AuthError) {
      // 認証エラーのハンドリング
    }

    // サーバーエラーのハンドリング
  }
};

// authorize-request.ts
export function authorizeRequest(event: APIGatewayEvent): void {
  try {
    const timestamp = event.headers['X-Slack-Request-Timestamp'];
    const signature = event.headers['X-Slack-Signature'];
    const body = event.body;

    // verify slack secret.
    const result = verifySlackSignature(timestamp, signature, body);
    if (!result) {
      throw new AuthError('Authentication failed.');
    }
  } catch (e) {
    throw e;
  }
}

まとめ

  • bodyの内容が含まれているタイプの署名の検証にはCustom Authorizerは使えない
  • おとなしくメインのhandlerから署名の検証に必要な情報を取得するのがよい
  • Custom Authorizerが今後対応してくるかは未知(そもそも今回のようなユースケースは想定されていなそう?)

参考