概要
AWS CDKでCognito認証されたAPI Gateway(HTTP API)を構築するをcdk v2で試してみた。
環境
- windows10
- docker-desktop(Docker version 20.10.13, build a224086)
- cdk 2.16.0
CDK構築
package.json
{
"name": "cdk",
"version": "0.1.0",
"bin": {
"cdk": "bin/cdk.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"lambda-with-cognito-deploy": "cdk -a \"npx ts-node --prefer-ts-exts bin/lambdaWithCognito.ts\" deploy LambdaWithCognitoStack -c @aws-cdk/core:newStyleStackSynthesis=true"
},
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.4.1",
"@types/node": "17.0.21",
"aws-cdk": "2.16.0",
"fs-extra": "^10.0.1",
"jest": "^27.5.1",
"ts-jest": "^27.1.3",
"ts-node": "^10.7.0",
"typescript": "~4.6.2"
},
"dependencies": {
"@aws-cdk/aws-apigatewayv2-alpha": "^2.16.0-alpha.0",
"@aws-cdk/aws-apigatewayv2-authorizers-alpha": "^2.16.0-alpha.0",
"@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.16.0-alpha.0",
"aws-cdk-lib": "2.16.0",
"constructs": "^10.0.89",
"dotenv": "^16.0.0",
"eslint": "^8.11.0",
"source-map-support": "^0.5.21"
}
}
PROJECT_ID=hogefuga
DOMAIN_PREFIX=piyo
CALLBACK_URLS=http://localhost:3200/oauth2-redirect.html
LOGOUT_URLS=http://localhost:3200/oauth2-redirect.html
FRONTEND_URLS=http://localhost:3200
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as cognito from 'aws-cdk-lib/aws-cognito'
import * as apigw from '@aws-cdk/aws-apigatewayv2-alpha'
import * as intg from '@aws-cdk/aws-apigatewayv2-integrations-alpha'
import * as authz from '@aws-cdk/aws-apigatewayv2-authorizers-alpha'
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs'
interface Props extends StackProps {
projectId: string
domainPrefix: string
frontendUrls: string[]
callbackUrls: string[]
logoutUrls: string[]
}
export class LambdaWithCognitoStack extends Stack {
constructor(scope: Construct, id: string, props: Props) {
super(scope, id, props)
const userPool = new cognito.UserPool(this, `${props.projectId}-userPool`, {
selfSignUpEnabled: false,
standardAttributes: {
email: { required: true, mutable: true },
phoneNumber: { required: false },
},
signInCaseSensitive: true,
autoVerify: { email: true },
signInAliases: { email: true },
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
removalPolicy: RemovalPolicy.DESTROY,
})
userPool.addDomain(`${props.projectId}-userPool-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 authorizer = new authz.HttpUserPoolAuthorizer(
`${props.projectId}-CognitoAuthorizer`,
userPool,
{
authorizerName: 'CognitoAuthorizer',
userPoolClients: [userPoolClient],
},
)
const handler = new NodejsFunction(
this,
`${props.projectId}-scenario-lambda`,
{
runtime: lambda.Runtime.NODEJS_14_X,
entry: '../src/handler/api/getScenario.ts',
functionName: 'scenario',
description: 'シナリオ',
},
)
const httpApi = new apigw.HttpApi(this, `${props.projectId}-apigw`, {
createDefaultStage: false,
corsPreflight: {
allowOrigins: props.frontendUrls,
allowMethods: [apigw.CorsHttpMethod.ANY],
allowHeaders: ['authorization'],
},
})
httpApi.addRoutes({
methods: [apigw.HttpMethod.GET],
path: '/scenario',
integration: new intg.HttpLambdaIntegration(
`${props.projectId}-scenarioIntegration`,
handler,
),
authorizer,
})
const stage = new apigw.HttpStage(this, `${props.projectId}-apistage`, {
httpApi,
stageName: 'api',
autoDeploy: true,
})
new CfnOutput(this, 'OutputApiUrl', { value: stage.url })
new CfnOutput(this, 'OutputDomainPrefix', { value: props.domainPrefix })
new CfnOutput(this, 'OutputClientId', {
value: userPoolClient.userPoolClientId,
})
}
}
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { LambdaWithCognitoStack } from '../lib/LambdaWithCognitoStack'
import * as dotenv from 'dotenv'
dotenv.config()
const envList = [
'PROJECT_ID',
'DOMAIN_PREFIX',
'CALLBACK_URLS',
'LOGOUT_URLS',
'FRONTEND_URLS',
] as const
for (const key of envList) {
if (!process.env[key]) throw new Error(`please add ${key} to .env`)
}
const processEnv = process.env as Record<typeof envList[number], string>
const app = new cdk.App()
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
}
const projectId = processEnv.PROJECT_ID
new LambdaWithCognitoStack(app, `${projectId}-stack`, {
projectId,
domainPrefix: processEnv.DOMAIN_PREFIX,
callbackUrls: processEnv.CALLBACK_URLS.split(','),
logoutUrls: processEnv.LOGOUT_URLS.split(','),
frontendUrls: processEnv.FRONTEND_URLS.split(','),
env
})
デプロイ後に表示されるURL、clientID、OutputDomainPrefixは次のSwaggerの環境変数に使うため控えておく。
SWaggerUIでの確認
SwaggerUIが現在(2022/03/17)openapi3.0.3までしか対応していないので、3.1で書いたファイルを一つにまとめた後、versionを3.1から3.0に書き換えるスクリプトを追記した。
openapi
{
"name": "app",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"redoc": "redoc-cli bundle ./openapi.yml --options.menuToggle --options.pathInMiddlePanel -o ./dist/index.html",
"preswaggerui": "openapi bundle openapi.yml --output swagger.yaml && bash replace.sh",
"swaggerui": "bash ./bin/swagger.sh"
},
"keywords": [],
"author": "hibohiboo <hibohiboo66+github@gmail.com>",
"license": "MIT",
"devDependencies": {
"@redocly/openapi-cli": "^1.0.0-beta.87",
"redoc": "^2.0.0-rc.65",
"redoc-cli": "^0.13.9"
}
}
OUTPUT_API_URL=https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/api
OUTPUT_DOMAIN_PREFIX=piyo
#!/bin/bash
. .env
sed -i -e "s/openapi: 3.1.0/openapi: 3.0.3/" swagger.yaml
sed -i -e "s|{{OutputApiUrl}}|$OUTPUT_API_URL|" swagger.yaml
sed -i -e "s|{{OutputDomainPrefix}}|$OUTPUT_DOMAIN_PREFIX|g" swagger.yaml
openapi: 3.1.0
info:
title: hoge
description: fugaAPI
version: 0.0.1
servers:
- url: "{{OutputApiUrl}}"
paths:
/scenario:
$ref: ./paths/cartagraph.yml
components:
securitySchemes:
OAuth2:
$ref: ./components/schemas/oauth2.yml
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
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
get:
operationId: scenarios
security:
- OAuth2: [ openid, token ]
responses:
200:
description: 200 OK
content:
application/json:
schema:
required:
- scenarios
properties:
scenarios:
type: array
items:
required:
- name
properties:
name:
type: string
example:
scenarios:
- name: hina
- name: koharu
- name: konatsu
swaggerui
version: '3.8'
services:
swagger:
image: swaggerapi/swagger-ui:latest
environment:
API_URL: /swagger.yaml
BASE_URL: /
env_file: .env
volumes:
- ../swagger.yaml:/usr/share/nginx/html/swagger.yaml
- ../components:/usr/share/nginx/html/components
- ../paths:/usr/share/nginx/html/paths
ports:
- 3200:8080
OAUTH_CLIENT_ID=発行されたcliendID
DOMAIN_PREFIX=piyo
cognitoの設定
デプロイされたらコンソールにログインし、cognitoの画面から検証用のユーザを作成する。
動作確認
dockerを起動し、 http://localhost:3200/に移動。
APIの認証有効確認
認証
client secretには何も入れずにAuthorizeをクリック
メールアドレスとパスワードを入力し、認証に成功すると元の画面が以下の表示となる。
再度executeとして結果が変化することを確認。
遭遇したエラー
認証画面に飛ぼうとすると以下のエラー。
An error was encountered with the requested page.
原因はコールバックURLの設定ミス。
# エラーとなるコード
CALLBACK_URLS=http://localhost:3200/
# 成功するコード
CALLBACK_URLS=http://localhost:3200/oauth2-redirect.html
参考
AmplifyでCognitoのHosted UIを利用した認証を最低限の実装で動かしてみて動作を理解する
AWS SAMでCognito UserPoolsの認証付きAPIを作成する
cdk
Amazon Cognito ユーザープールをオーソライザーとして使用して REST API へのアクセスを制御する
Amplify auth は複数のコールバックURLに対応していない
OPENAPI
redocly