この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
おはようございます、加藤です。今回はAWS CDKを使ってAmazon API Gateway(HTTP API = v2)にAmazon Cognitoを使って認証を設定する方法をまとめてみます。 また作成されたAPIに対してOpenAPI定義を作成し、それをSwagger UIでプレビュー&(認証された状態で)API呼び出しする方法も合わせて説明します。
リポジトリのセットアップ
aws-cdkコマンドを使ってリポジトリを生成します。
mkdir cdk-demo-apigw-with-cognito
cd cdk-demo-apigw-with-cognito
npx -p aws-cdk cdk init app --language typescript
必要な依存関係をインストールします。
npm i -D \
@aws-cdk/aws-apigatewayv2 \
@aws-cdk/aws-apigatewayv2-integrations \
@aws-cdk/aws-apigatewayv2-authorizers \
@aws-cdk/aws-cognito \
@aws-cdk/aws-lambda-nodejs \
@types/aws-lambda \
esbuild@0
AWSリソースの構築
API GatewayでCognitoを使って認証するためにはCognito UserPoolとUserPool Clientを作成し、それをAuthorizerとしてAPIに関連付けします。
気をつけて欲しいポイントとしてパス/
へのアクセスは特別なものとして扱われ、特にCORS周りで特殊な設定が必要になるので特別な理由が無ければ使わないほうが無難です。
HTTP API のルートの使用 - Amazon API Gateway # $default ルートの操作
// lib/cdk-demo-apigw-with-cognito-stack.ts
import * as cdk from '@aws-cdk/core';
import * as cognito from '@aws-cdk/aws-cognito';
import * as apigw from '@aws-cdk/aws-apigatewayv2';
import {HttpMethod} from '@aws-cdk/aws-apigatewayv2/lib/http/route';
import * as intg from '@aws-cdk/aws-apigatewayv2-integrations';
import * as nodejs from '@aws-cdk/aws-lambda-nodejs';
import * as authz from '@aws-cdk/aws-apigatewayv2-authorizers';
export interface CdkDemoApigwWithCognitoStackProps extends cdk.StackProps {
callbackUrls: string[];
logoutUrls: string[];
frontendUrls: string[];
domainPrefix: string;
}
export class CdkDemoApigwWithCognitoStack extends cdk.Stack {
constructor(
scope: cdk.Construct,
id: string,
props: CdkDemoApigwWithCognitoStackProps
) {
super(scope, id, props);
const userPool = new cognito.UserPool(this, 'userPool', {
selfSignUpEnabled: false,
standardAttributes: {
email: {required: true, mutable: true},
phoneNumber: {required: false},
},
signInCaseSensitive: false,
autoVerify: {email: true},
signInAliases: {email: true},
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
userPool.addDomain('domain', {
cognitoDomain: {domainPrefix: props.domainPrefix},
});
const userPoolClient = userPool.addClient('client', {
oAuth: {
scopes: [
cognito.OAuthScope.EMAIL,
cognito.OAuthScope.OPENID,
cognito.OAuthScope.PROFILE,
],
callbackUrls: props.callbackUrls,
logoutUrls: props.logoutUrls,
flows: {authorizationCodeGrant: true},
},
});
const handler = new nodejs.NodejsFunction(this, 'Handler');
const authorizer = new authz.HttpUserPoolAuthorizer({
authorizerName: 'CognitoAuthorizer',
userPool,
userPoolClient,
});
const httpApi = new apigw.HttpApi(this, 'Api', {
defaultAuthorizer: authorizer,
corsPreflight: {
allowOrigins: props.frontendUrls,
allowMethods: [apigw.CorsHttpMethod.ANY],
allowHeaders: ['authorization'],
},
});
httpApi.addRoutes({
methods: [HttpMethod.GET],
path: '/pets',
integration: new intg.LambdaProxyIntegration({handler}),
});
new cdk.CfnOutput(this, 'OutputApiUrl', {value: httpApi.url!});
new cdk.CfnOutput(this, 'OutputDomainPrefix', {value: props.domainPrefix});
}
}
API Gatewayのバックエンドで処理を行うLambda関数を作成します。@aws-cdk/aws-lambda-nodejs
はプロパティを指定しないと${stackFileName}.${CdkComponentId}.ts
のファイルをエントリーポイントとしてesbuildを使ってバンドリングを行います。(コンポーネントIDは大文字小文字を区別します)
下記のようにペット一覧のモックデータを返す処理を実装します。
// lib/cdk-demo-apigw-with-cognito-stack.Handler.ts
import {APIGatewayProxyHandlerV2} from 'aws-lambda';
interface Pet {
name: string;
age: number;
}
interface Response {
pets: Pet[];
}
export const handler: APIGatewayProxyHandlerV2 = async () => {
return {
statusCode: 200,
body: JSON.stringify({
pets: [
{
name: 'hina',
age: 1,
},
{
name: 'koharu',
age: 2,
},
{
name: 'konatsu',
age: 3,
},
],
} as Response),
};
};
エントリーファイルを変更し、スタックが求めるプロパティを注入します。
// bin/cdk-demo-apigw-with-cognito.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import {CdkDemoApigwWithCognitoStack} from '../lib/cdk-demo-apigw-with-cognito-stack';
const app = new cdk.App();
new CdkDemoApigwWithCognitoStack(app, 'CdkDemoApigwWithCognitoStack', {
callbackUrls: ['http://localhost:3200/'],
logoutUrls: ['http://localhost:3200/'],
frontendUrls: ['http://localhost:3200'],
domainPrefix: process.env.DOMAIN_PREFIX!,
});
環境変数DOMAIN_PREFIX
を設定し、デプロイします。
デプロイ時に出力されたOutputは後ほどOpenAPI定義を作成する際とSwagger UIを使う際に使用するのでメモしておいてください。
DOMAIN_PREFIX=任意の英数記号(一部の予約語は使用できない、cognitoなど)
npm run cdk deploy
Swagger UIでのプレビューとAPI呼び出し
OpenAPI定義を作成します。なお、プレースホルダーの部分をメモしたOutputに差し替えてください。
# docs/openapi.yaml
openapi: 3.0.3
info:
title: Petstore API overview
version: 1.0.0
servers:
- url: {{OutputApiUrl}}
components:
securitySchemes:
OAuth2:
type: oauth2
description: For more information, see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html
flows:
authorizationCode:
authorizationUrl: https://{{OutputDomainPrefix}}.auth.ap-northeast-1.amazoncognito.com/oauth2/authorize
tokenUrl: https://{{OutputDomainPrefix}}.auth.ap-northeast-1.amazoncognito.com/oauth2/token
scopes:
openid: openid token
paths:
/pets:
get:
operationId: listPets
security:
- OAuth2: [ openid, token ]
responses:
200:
description: 200 OK
content:
application/json:
schema:
required:
- pets
properties:
pets:
type: array
items:
required:
- name
- age
properties:
name:
type: string
age:
type: number
example:
pets:
- name: hina
age: 1
- name: koharu
age: 2
- name: konatsu
age: 3
このOpenAPI定義をSwagger UIでプレビューします。Docker上で実行する為にdocker-compose.yaml
を書きます。
# docker-compose.yaml
version: '3.8'
services:
swagger:
image: swaggerapi/swagger-ui
environment:
API_URL: /swagger.yaml
BASE_URL: /
env_file: .env
volumes:
- ./docs/openapi.yaml:/usr/share/nginx/html/swagger.yaml
ports:
- 3200:8080
Client IDを環境変数に設定しSwagger UIを起動します。
OAUTH_CLIENT_ID=CDKからアウトプットされたClient ID
docker compose up
Swagger UIを使ったAPI呼び出しは下記のブログを参考にしてください。(今回の手順ではClient IDが予め設定されています、Client Secretは設定する必要がありません。)
Swagger 3.0のOAuth認証にCognito User PoolsのOAuth Clientを使う | DevelopersIO
あとがき
今回作成したコードはこちらで公開しています。 intercept6/cdk-demo-apigw-with-cognito
OpenAPI定義へのパラメーターは手動で書き換えていますが、CDKのアウトプットはjsonに書き出すことが可能なのでJinja2などを使えば自動的に更新することも出来そうだなーと思いました。ただ、カスタムドメインの割当ができれば大抵は不要なので必要なケースは限定的ですね。