SSTの独自軽量Authを複数APIにアタッチする

SST独自実装の軽量Authコンストラクタは、実は複数のAPI Gatewayにアタッチできるよ。SSTのDiscordで紹介されていたけれど、公式ドキュメントではあまり言及されていない(実例なし)から、この記事で紹介するよ。

※画像引用:https://sst.dev/

SST(Serverless Stack。CDKをラップして便利機能を追加したサーバレスアプリ爆速開発ツール)には独自実装の軽量Authコンストラクトが用意されているんだ。このAuthコンストラクトは、一般的には次のようにAPIにアタッチして使用する。

import { StackContext, Api, Auth } from "sst/constructs";

export function MyAuth({ stack }: StackContext) {
  const auth = new Auth(stack, "myAuth", {
    // 認証処理を下記で指定した関数に実装(実装内容の紹介は割愛)
    authenticator: "packages/functions/src/auth.handler",
  });
  const myApi = new Api(stack, "myApi", {
    // ここにAPI Gatewayのルート定義等を記述(割愛)
  });
  // 認証処理をAPI Gatewayの/auth以下のルートにアタッチする
  auth.attach(stack, {
    api: myApi,
    prefix: "/auth",
  });

  // エンドポイントの標準出力(確認用)
  stack.addOutputs({
    MyApiEndpoint: myApi.url,
  });
}

こうすれば、アタッチしたAPI Gatewayの各ルートでトークンの検証を行いセッション情報を取得できるようになる。ただ、アプリケーションの規模が拡大して、複数のAPI Gatewayを運用するようになった時、認可処理が可能なAPI Gatewayがひとつしかなかったら、不便だよね。

そうならないように、SSTのAuthコンストラクタは、複数のAPI Gatewayにアタッチできるようになっているんだ。

実例を挙げるならこうだね。

// ---- Auth提供用スタック(AuthStack.ts) ----
import { StackContext, Auth } from "sst/constructs";

export function MyAuth({ stack }: StackContext) {
  const auth = new Auth(stack, "myAuth", {
    authenticator: "packages/functions/src/auth.handler",
  });
  return {
    auth,
  }
}
// ---- APIその1用スタック(ApiStack01.ts) ----
import { StackContext, Api, use } from "sst/constructs";
const { auth } = use(MyAuth);

export function MyApi01({ stack }: StackContext) {
  const myApi01 = new Api(stack, "myApi01", {
    // ここにAPI Gatewayのルート定義等を記述(割愛)
  });
  // 認証処理をAPI Gatewayの/auth以下のルートにアタッチする
  auth.attach(stack, {
    api: myApi01,
    prefix: "/auth",
  });

  // エンドポイントの標準出力(確認用)
  stack.addOutputs({
    MyApi01Endpoint: myApi01.url,
  });
}
// ---- APIその2用スタック(ApiStack02.ts) ----
import { StackContext, Api, use } from "sst/constructs";
const { auth } = use(MyAuth);

export function MyApi02({ stack }: StackContext) {
  const myApi02 = new Api(stack, "myApi02", {
    // ここにAPI Gatewayのルート定義等を記述(割愛)
  });
  // 認証処理をAPI Gatewayの/auth以下のルートにアタッチする
  auth.attach(stack, {
    api: myApi02,
    prefix: "/auth",
  });

  // エンドポイントの標準出力(確認用)
  stack.addOutputs({
    MyApi02Endpoint: myApi02.url,
  });
}
// ---- sst.config.ts ----
import { SSTConfig } from "sst";
import { MyAuth } from "./stacks/AuthStack";
import { MyApi01 } from "./stacks/ApiStack01";
import { MyApi02 } from "./stacks/ApiStack02";

export default {
  config(_input) {
    return {
      name: "myapp",
      region: "ap-northeast-1",
    };
  },

  stacks(app) {
    app
      .stack(MyAuth)
      .stack(MyApi01)
      .stack(MyApi02);

    // ※下記でもOK
    // app.stack(VaesiteAuth);
    // app.stack(VaesiteApi);
    // app.stack(VaesiteSite);
  },
} satisfies SSTConfig;

これで複数のAPI Gatewayに同一のセッションをグローバルに共有可能な認可処理をアタッチできた。

注意点

現在のSSTのバージョン(2.2.4)の時点では、なぜか下記のように 「同一スタック内で複数のAPI Gatewayを作成&Authにアタッチする」 と、npm run dev(sst dev)時にエラーが発生するケースがあるよ。

// ---- API2つ同時定義スタック ----
import { StackContext, Api, use } from "sst/constructs";
const { auth } = use(MyAuth);

export function MyApi({ stack }: StackContext) {
  const myApi01 = new Api(stack, "myApi01", {});
  auth.attach(stack, {
    api: myApi01,
    prefix: "/auth",
  });
  const myApi02 = new Api(stack, "myApi02", {});
  auth.attach(stack, {
    api: myApi02,
    prefix: "/auth",
  });
}
Error: There is already a Construct with name 'Lambda_ANY_--auth--{proxy+}' in EmptyStack [sig-vaesite-VaesiteApi]
at Node.addChild (/Users/sig/[project_name]/node_modules/constructs/src/construct.ts:403:13)
at null.Node (/Users/sig/[project_name]/node_modules/constructs/src/construct.ts:71:17)
at null.Construct (/Users/sig/[project_name]/node_modules/constructs/src/construct.ts:463:17)
at new Resource (/Users/sig/[project_name]/node_modules/aws-cdk-lib/core/lib/resource.js:1:622)
at new FunctionBase (/Users/sig/[project_name]/node_modules/aws-cdk-lib/aws-lambda/lib/function-base.js:1:614)
at new Function (/Users/sig/[project_name]/node_modules/aws-cdk-lib/aws-lambda/lib/function.js:1:1155)
at new Function (file:///Users/sig/[project_name]/node_modules/sst/constructs/Function.js:144:13)
at Function.fromDefinition (file:///Users/sig/[project_name]/node_modules/sst/constructs/Function.js:430:24)
at Api.createFunctionIntegration (file:///Users/sig/[project_name]/node_modules/sst/constructs/Api.js:644:27)
at file:///Users/sig/[project_name]/node_modules/sst/constructs/Api.js:479:26

スタックを分ければ発生しないけれど、もし同一スタックそのままでエラーを回避したいなら、現状では下記のようにAuth割り当てルート名を変えることで対応可能だったよ。

// ---- API2つ同時定義スタック ----
import { StackContext, Api, use } from "sst/constructs";
const { auth } = use(MyAuth);

export function MyApi({ stack }: StackContext) {
  const myApi01 = new Api(stack, "myApi01", {});
  auth.attach(stack, {
    api: myApi01,
    prefix: "/auth",
  });
  const myApi02 = new Api(stack, "myApi02", {});
  auth.attach(stack, {
    api: myApi02,
    prefix: "/auth2", // 名前を変えた
  });
}

レアケースかもしれないけれど、今後のためにメモメモ……

関連記事