はじめに
前回の記事「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へ環境変数として渡す。
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に定義した。認証とデータ処理に関わる部分を抜粋する。
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の返却までの主要部分は次のとおり。
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トークンとして付与する。
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が選択肢になる。