0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amplify Gen 2とAWS BlocksでCognito認証付きAPIを作ってみた

0
Last updated at Posted at 2026-09-30

はじめに

前回の記事「AWS Blocksを触ってみた。Amplify Gen 2と比較して感じたこと」で、AWS BlocksにはAmplify Gen 2と組み合わせる公式の方法があることを知った。そこで今回は、Amplify Gen 2のプロジェクトにAWS Blocksを追加し、Cognito認証付きAPIを実際に動かしてみた。

AWS Blocksは2026年6月16日にPublic Previewとして発表された。本稿は2026年9月18日時点の検証結果であり、CLIやパッケージの挙動は今後変わる可能性がある。

今回試す構成

Amplify Gen 2側のCognito User Poolを認証に使い、AWS Blocks側には活動履歴を登録・取得するAPIと、履歴を保存するDistributedTableを追加する。

役割 担当
ユーザー認証 Amplify Auth / Amazon Cognito
独自API AWS Blocks / API Gateway / Lambda
活動履歴の保存 AWS Blocks DistributedTable / DynamoDB
APIの認証 Amplify AuthのIDトークン / Cognito JWT検証

リクエストは次の経路で処理される。

Amplifyが認証を担当し、Blocksが認証済みユーザー向けの独自APIとデータ処理を担当する構成だ。

検証環境

項目 内容
検証日 2026年9月18日
OS macOS
Node.js / npm 24.21.0 / 10.9.2
AWSリージョン ap-northeast-1(東京)
@aws-amplify/backend 1.25.0
@aws-amplify/backend-cli 1.10.0
aws-amplify 6.20.0
@aws-blocks/blocks 0.5.0
@aws-blocks/core 0.4.0
@aws-blocks/bb-distributed-table 0.1.7
aws-jwt-verify 5.2.1

Blocksを追加してSandboxへデプロイする

Amplify Gen 2のプロジェクトを作成した後、プロジェクトルートでAWS BlocksのCLIを実行した。

npx @aws-blocks/create-blocks-app .
npm run sandbox

CLIを実行すると、aws-blocks/、amplify/blocks.ts、amplify/backend.ts、package.json、amplify.ymlが作成または更新される。この記事では、このうち認証情報をBlocksへ渡すamplify/blocks.tsの設定を見ていく。

Amplify側のCognitoをBlocksで使う

CLIが生成したamplify/blocks.tsでは、BlocksのバックエンドをAmplifyのNested Stackに配置している。CognitoはBlocks側で新しく作らず、Amplifyが作成したUser PoolとUser Pool Clientの情報をBlocksのLambdaへ環境変数として渡す。

amplify/blocks.ts
const blocksStack = backend.createStack('blocks');
const blocks = await createBlocksBackend(
  blocksStack,
  process.env.AMPLIFY_SANDBOX === 'true',
);

const { cfnUserPool, cfnUserPoolClient } =
  backend.auth.resources.cfnResources;

blocks.handler.addEnvironment('COGNITO_USER_POOL_ID', cfnUserPool.ref);
blocks.handler.addEnvironment('COGNITO_CLIENT_ID', cfnUserPoolClient.ref);
blocks.handler.addEnvironment('COGNITO_REGION', blocksStack.region);

backend.addOutput({
  custom: { blocks_api_url: blocks.apiUrl },
});

これで、Blocks側のLambdaからAmplifyの認証基盤を参照できる。通常のAmplifyデプロイにBlocksのNested Stackが含まれる仕組みは、How AWS Blocks works with Amplifyで説明されている。

Blocks側に認証付きAPIを定義する

Blocks側では、活動履歴を扱う2つのメソッドをApiNamespaceに定義した。認証とデータ処理に関わる部分を抜粋する。

aws-blocks/index.ts
const scope = new Scope('my-app');

const activitySchema = z.object({
  userId: z.string(),
  activityId: z.string(),
  action: z.string(),
  createdAt: z.string(),
});

const activities = new DistributedTable(scope, 'activities', {
  schema: activitySchema,
  key: { partitionKey: 'userId', sortKey: 'activityId' },
});

const auth = new CognitoVerifier({
  userPoolId: process.env.COGNITO_USER_POOL_ID!,
  clientId: process.env.COGNITO_CLIENT_ID!,
  tokenUse: 'id',
});

export const api = new ApiNamespace(scope, 'api', (context) => ({
  async recordActivity(action: string) {
    const user = await auth.requireAuth(context);
    const activity = {
      userId: user.sub,
      activityId: `${new Date().toISOString()}-${randomUUID()}`,
      action,
      createdAt: new Date().toISOString(),
    };

    await activities.put(activity, { ifNotExists: true });
    return { activityId: activity.activityId, action: activity.action };
  },

  async listMyActivities(limit = 10) {
    const user = await auth.requireAuth(context);
    const items = [];

    for await (const item of activities.query({
      where: { userId: { equals: user.sub } },
      order: 'desc',
      limit: Math.min(Math.max(limit, 1), 20),
    })) {
      items.push(item);
    }

    return { items };
  },
}));

ここで使っているCognitoVerifierは、aws-blocks/cognito-verifier.tsで実装したクラスだ。内部ではaws-jwt-verifyのCognitoJwtVerifierを使い、requireAuthでBearerトークンを検証する。認証に失敗すると、JSON-RPCのエラーコードとして401を返す。

トークンの取り出しからJWT検証、401の返却までの主要部分は次のとおり。

aws-blocks/cognito-verifier.ts
private async getVerifier() {
  if (!this.verifier) {
    this.verifier = CognitoJwtVerifier.create({
      userPoolId: this.options.userPoolId,
      clientId: this.options.clientId,
      tokenUse: this.options.tokenUse || 'id',
    });
  }
  return this.verifier;
}

private extractToken(context: BlocksContext): string | null {
  const authHeader = context.request.headers.get('authorization') ||
    context.request.headers.get('Authorization');
  if (!authHeader?.startsWith('Bearer ')) return null;
  return authHeader.slice(7);
}

async getCurrentUser(context: BlocksContext) {
  const token = this.extractToken(context);
  if (!token) return null;

  try {
    const verifier = await this.getVerifier();
    const payload = await verifier.verify(token) as Record<string, unknown>;
    return {
      userId: payload.sub as string,
      username: (payload['cognito:username'] as string) || (payload.sub as string),
      sub: payload.sub as string,
      groups: (payload['cognito:groups'] as string[]) || [],
      email: payload.email as string | undefined,
      claims: payload,
    };
  } catch {
    return null;
  }
}

async requireAuth(context: BlocksContext) {
  const user = await this.getCurrentUser(context);
  if (!user) {
    throw new ApiError('Unauthorized', 401);
  }
  return user;
}

recordActivityは、検証済みトークンから取得したCognitoのsubをuserIdに設定する。クライアントからユーザーIDを受け取らないため、APIの引数で別ユーザーの保存先を指定することはできない。listMyActivitiesも同じsubを検索条件に使い、ユーザー単位で履歴を絞り込む。

フロントエンドからIDトークンを渡す

Sandboxへのデプロイ後、次のコマンドでBlocks APIの型付きクライアントを生成する。

npm run blocks:generate-client

生成されたaws-blocks/client.jsは、amplify_outputs.jsonからAPIのURLを読み込む。また、Amplify AuthのセッションからIDトークンを取得し、Blocks APIへのリクエストにBearerトークンとして付与する。

aws-blocks/client.js
registerMiddleware({
  async onRequest(req) {
    const session = await fetchAuthSession();
    const token = session.tokens?.idToken?.toString();
    if (token) req.headers['Authorization'] = `Bearer ${token}`;
    return req;
  },
});

アプリの起動時にAmplifyを設定し、生成されたapiをインポートして呼び出す。

import { Amplify } from 'aws-amplify';
import outputs from '../amplify_outputs.json';
import { api } from 'aws-blocks';

Amplify.configure(outputs);

const result = await api.recordActivity('signed-in');

検証結果

Amplify Sandboxへデプロイした環境で、IDトークンを付けないリクエストと、2人の認証済みユーザーからのリクエストを実行した。未認証のリクエストが拒否され、認証済みユーザーのsubによって活動履歴が分離されることを確認した。

認証なしのリクエストは拒否される

まず、IDトークンを付けずにlistMyActivitiesを呼び出した。

{
  "jsonrpc": "2.0",
  "method": "api.listMyActivities",
  "params": [10],
  "id": 4
}

Blocks側からは、次のJSON-RPCエラーが返った。

{
  "jsonrpc": "2.0",
  "error": {
    "code": 401,
    "message": "Unauthorized",
    "data": {
      "name": "ApiError"
    }
  },
  "id": 4
}

このときHTTPステータスは200だった。認証エラーは、JSON-RPCレスポンス内のerror.codeに401として格納されている。HTTPステータスだけで成否を判定せず、生成クライアントを使うか、JSON-RPCのerrorを確認する必要がある。

requireAuthはlistMyActivitiesの検索処理より前に実行されるため、認証情報のないリクエストをデータ取得前に拒否できた。

認証済みユーザーの活動履歴を登録する

次に、Amplify Authでサインインして取得したIDトークンを、Authorization: Bearer <IDトークン>として付けてrecordActivityを呼び出した。

{
  "jsonrpc": "2.0",
  "method": "api.recordActivity",
  "params": ["signed-in"],
  "id": 2
}

レスポンスには、渡したアクションと生成された活動履歴IDが含まれていた。

{
  "jsonrpc": "2.0",
  "result": {
    "activityId": "<検証時刻>-<UUID>",
    "action": "signed-in"
  },
  "id": 2
}

CognitoVerifierがIDトークンを検証し、トークンから取得したuser.subをDistributedTableのuserIdに設定する。リクエストの引数にユーザーIDは含まれていない。

登録したユーザーの活動履歴を取得する

続けて、同じIDトークンでlistMyActivitiesを呼び出した。

{
  "jsonrpc": "2.0",
  "method": "api.listMyActivities",
  "params": [10],
  "id": 3
}

レスポンスのitemsには、先ほど登録した活動履歴が含まれていた。

{
  "jsonrpc": "2.0",
  "result": {
    "items": [
      {
        "createdAt": "<検証時刻>",
        "action": "signed-in",
        "userId": "<サインインユーザーのsub>",
        "activityId": "<検証時刻>-<UUID>"
      }
    ]
  },
  "id": 3
}

ユーザーごとに活動履歴が分離される

検証用の一時ユーザーAとユーザーBを用意し、それぞれ異なるアクションを登録した。ユーザーAではuser-a、ユーザーBではuser-bを登録し、その後、それぞれのIDトークンでlistMyActivitiesを呼び出した。

ユーザーAのレスポンスには、ユーザーAが登録した履歴だけが含まれていた。以下は、分離の確認に必要なフィールドを抜粋したものだ。

{
  "result": {
    "items": [
      {
        "action": "user-a",
        "userId": "<ユーザーAのsub>"
      }
    ]
  }
}

ユーザーBのレスポンスも同様に、ユーザーBが登録した履歴だけだった。こちらも同じフィールドだけを抜粋している。

{
  "result": {
    "items": [
      {
        "action": "user-b",
        "userId": "<ユーザーBのsub>"
      }
    ]
  }
}

ユーザーAの取得結果にuser-bは含まれず、ユーザーBの取得結果にuser-aは含まれなかった。DistributedTableの検索条件に検証済みトークンのsubを使うことで、ユーザーごとの履歴分離が実際のレスポンスでも確認できた。

検証して分かったこと

Amplify Gen 2が作成したCognitoをそのまま使い、AWS BlocksのAPIへ認証状態を引き継げた。実際のリクエストでも、未認証アクセスの拒否と、Cognitoのsubを使ったユーザーごとのデータ分離を確認できた。Blocks側に別の認証基盤を作る必要はなかった。

一方、Cognitoの情報をLambdaへ渡す設定と、Blocks側でのJWT検証は自分で実装する必要がある。認証や標準的なデータ操作がAmplify Gen 2だけで完結するなら、無理にBlocksを追加する必要はないと感じた。Amplifyの認証基盤を残したままアプリ固有のAPIを追加したい場合に、AWS Blocksが選択肢になる。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?