カテゴリー
【Angularユーザーのための認証API自作講座②】Serverless FrameworkでCognitoオーソライザー付きRestAPIを構築する
※ 当ページには【広告/PR】を含む場合があります。
2021/06/30
2022/08/10

前回に引き続きAWS Cognitoを利用したAngularアプリケーションの作成の話の第二弾です。
前の回では手動でCognito及びAPI Gatewayの設定しながら基本的な手順を説明しましたが、その内容はなかなか煩雑で毎回同じような作業ではやってられない...
そんなわけで今回はまっさらのAngularプロジェクトからServerless Framework(以降SLS)でCognitoオーソライザ付きのAPI関数を呼び出すアプリをステップ・バイ・ステップで作成する方法を検証します。
SLSで簡単なAPI作成
まずはとりあえずCurlでエンドポイントを叩いて、シンプルなJSONレスポンスが返ってくるAPIをSLSで立ち上げます。
お手元の環境に既にnode.jsは導入済みであるとして、以下のコマンドでSLSがグローバル環境にも導入できます。
$ npm install -g serverless
$ serverless --version
Framework Core: 2.48.0
Plugin: 5.4.2
SDK: 4.2.3
Components: 3.12.0
グローバル環境で入れて使ってもOKですが、SLSのバージョン管理を後々しやすいように、今回はpackage.jsonからローカルインストールして利用します。 ローカルインストールの場合、CLI的に利用するには
/node_modules/.bin/
$./node_modules/.bin/sls --version
Framework Core: 2.48.0 (local)
Plugin: 5.4.2
SDK: 4.2.3
Components: 3.12.0
まずプロジェクトフォルダの最小の構成は以下のような感じに空のリソースを配置しておきます。
$ tree
.
├── package.json
├── functions
│ └── get_public.js
├── resources
│ └── functions.yml
└── serverless.yml
各ソースコードの実装
では以下で新規作成したファイルの中身を具体的に実装していきます。
package.json
まずはpackage.jsonを以下のようにしておきます。
{
"name": "my_sls_learning",
"version": "0.0.1",
"scripts": {
"deploy": "serverless deploy -v",
"remove": "serverless remove -v",
"deploy:fn": "serverless deploy function -f",
"invoke": "serverless invoke --function",
"invoke:lcl": "serverless invoke local --function",
"logs": "serverless logs --function",
"pkg": "serverless package -p tmp",
},
"private": false,
"dependencies": {
"aws-sdk": "^2.7.0"
},
"devDependencies": {
"@types/aws-sdk": "^2.7.0",
"@types/node": "^15.0.0",
"serverless": "^2.0.0"
}
}
としておいて、
$ yarn install
するとnpmパッケージがインストールされます。
serverless.yml
以下のように
serverless.yml
service: my-sls-learning
frameworkVersion: "2"
provider:
name: aws
runtime: nodejs12.x
lambdaHashingVersion: 20201221
memorySize: 128
timeout: 10
stage: ${opt:stage,"dev"}
region: ap-northeast-1
httpApi:
cors: true
functions:
- ${file(./resources/functions.yml)}
package:
exclude:
- '**'
include:
- 'functions/**'
- 'package.json'
なおここではserverless.ymlのファイル分割や、
functions.yml
APIのハンドラ関数のみ定義した設定は
functions.yml
getPublic:
#👇リソースはserverless.ymlから見た相対パスで指定
handler: functions/get_public.handler
events:
- http: GET /
- http: GET /public
エンドポイントのルーティングパターンは
/
/public
get_public.js
先程のgetPublic関数のハンドラの中身は以下で与えます。
module.exports.handler = async (event, context, callback) => {
callback(null, {
statusCode: 200,
body: JSON.stringify({
result: 'Greeting!'
}),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
},
})
}
これだけでAPI Gayeway & Lambda周りを手動でポチポチしてダッシュボードを行ったり来たりしなくても、認証なしのRestAPIが出来上がってしまいました。 改めてSLSの便利さが実感できます。
余談 ~ serverless createコマンド
今回は利用しませんが、用意されたテンプレートからServerlessサービスを始める場合には
serverless create
aws-nodejs
$ serverless create --template aws-nodejs \
--name [新規作成するSLSサービス名] \
--path [新規作成するSLSサービスのプロジェクトフォルダ]
すると、--pathで指定したフォルダの中にそのまま利用できるリソースが以下の内容で生成されます。
+ serverless.yml
+ handler.js
+ event.json
関数のローカルテスト
さて、package.jsonのscriptにいくつか便利なコマンドを仕込んでいましたが、デプロイ前に動作確認を行うためのコマンドで
$ yarn invoke:lcl getPublic
#👇ローカルでレスポンスが正常に返ることを確認する
{
"statusCode": 200,
"body": "{\"result\":\"Greeting!\"}",
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true"
}
}
Done in 18.02s.
デプロイ
それでは上記の最低限のリソースでSLSサービスをAWS上にデプロイさせてみます。
ここでは詳細は解説しませんが、AWSのサインアップの方法は
個人的には
export AWS_ACCESS_KEY_ID="[IAMロールのID値]"
export AWS_SECRET_ACCESS_KEY: "[IAMロールのアクセスキー]"
export AWS_DEFAULT_REGION: "ap-northeast-1(例)"
AWSアカウントの設定が終わったら、以下のコマンドでデプロイしてみます。
$ yarn deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
........
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service my-serverless-learning.zip file to S3 (52.57 KB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..................................
Serverless: Stack update finished...
Service Information
service: my-serverless-learning
stage: dev
region: ap-northeast-1
stack: my-serverless-learning-dev
resources: 12
api keys:
None
endpoints:
GET - https://**********.execute-api.ap-northeast-1.amazonaws.com/dev
GET - https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/public
functions:
getPublic: my-serverless-learning-dev-getPublic
layers:
None
**************************************************************************************************************************************
Serverless: Announcing Metrics, CI/CD, Secrets and more built into Serverless Framework. Run "serverless login" to activate for free..
**************************************************************************************************************************************
Done in 140.42s.
ビルドが正常終わってエンドポイントが与えられていたらデプロイ完了です。
もし、ここでビルドやデプロイに問題がある場合には、利用しているIAMユーザーの権限が不足している場合があります。 IAMロールに
AdministratorAccess
成功していれば以下のようにAPI Gatewayのダッシュボードに新しいインスタンスが出来上がっているのも確認できます。

関数部の部分的デプロイ
もし関数のリソースだけ一部修正して再度デプロイしたい場合、再び
serverless deploy
そんな場合には、
$ yarn deploy:fn getPublic
Serverless: Packaging function: getPublic...
Serverless: Excluding development dependencies...
Serverless: Uploading function: getPublic (1.58 KB)...
Serverless: Successfully deployed function: getPublic
Serverless: Configuration did not change. Skipping function configuration update.
Done in 113.55s.
現状、関数は1つしかないのでほぼ
sls deploy
デプロイ前にアップロードされるzipの中身を確認する
tmp
$ yarn pkg
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Done in 113.05s.
#👇zipinfoコマンドで中身を確認
$ zipinfo -1 tmp/my-serverless-learning.zip
functions/get_public.js
functions/lambda.js
functions/local.js
functions/server.js
デプロイされた関数のテスト
デプロイ後の動作確認を行うためのコマンドで
$ yarn invoke getPublic
#👇レスポンスが正常に返ることを確認する
{
"statusCode": 200,
"body": "{\"result\":\"Greeting!\"}",
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true"
}
}
Done in 17.04s.
実行後のログをみたい場合には、
$ yarn logs getPublic
START RequestId: 310e2a2a-cb3c-4908-aeda-4b86d6c789ba Version: $LATEST
END RequestId: 310e2a2a-cb3c-4908-aeda-4b86d6c789ba
REPORT RequestId: 310e2a2a-cb3c-4908-aeda-4b86d6c789ba Duration: 3.64 ms Billed Duration: 4 ms Memory Size: 128 MB Max Memory Used: 64 MB Init Duration: 133.48 ms
#...
Curlでエンドポイントを叩く
ではこのAPIが正常に動作しているかCurlでレスポンスを見てみます。
$ curl --include https://*************.execute-api.ap-northeast-1.amazonaws.com/dev/public
HTTP/2 200
content-type: application/json
content-length: 22
date: Mon, 28 Jun 2021 17:48:34 GMT
x-amzn-requestid: a215270c-7ef8-4e63-89ab-f7eccb38bf56
access-control-allow-origin: *
x-amz-apigw-id: Bpa52FurNjMFjGA=
x-amzn-trace-id: Root=1-60da0b72-7111879c1c6117853b43330a;Sampled=0
access-control-allow-credentials: true
x-cache: Miss from cloudfront
x-amz-cf-pop: NRT57-C4
x-amz-cf-id: O0mXVZtOwvDwYm4oMgmhDbVdkWjX_8hy0CtMrvRX0D978UltmNSsaQ==
{"result":"Greeting!"}
と、正常にAPIが稼働しています。
Cognitoオーソライザー付きAPIの作成
今のままでは誰にでもエンドポイントを叩けてしまうため、ユーザー認証付きのエンドポイントを作成してみます。
既存のCognitoユーザープールからオーソライザーを作成する
一例として
CognitoユーザープールのARN値を使って、以下のようにリソースからオーソライザーを定義して利用します。
Resources:
ApiGatewaySharedAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: ApiGatewaySharedAuthorizer
RestApiId:
Ref: ApiGatewayRestApi
IdentitySource: method.request.header.Authorization
Type: COGNITO_USER_POOLS
ProviderARNs:
#👇CognitoユーザープールのARN値
- !Join
- ''
- - '[ユーザープールのARN]'
なお、
「ApiGatewayRestApi」
場合によっては、RestAPIが複数あったり、別のAWSアカウントのものを使ったりと、いうケースでRestApiIdを細かく指定するようですが、特に利用したいRestAPIが指定がないと
この設定を
shared-authorizer.yml
$ tree
.
├── package.json
├── functions
│ └── get_public.js
├── resources
│ ├── shared-authorizer.yml #👈新規作成
│ └── functions.yml
└── serverless.yml
更に以下のように、この
shared-authorizer.yml
serverless.yml
#...中略
resources:
#👇Cognitoオーソライザー
- ${file(resources/shared-authorizer.yml)}
#...以下略
さらに新しいAPIハンドラ関数用に
get_private.js
$ tree
.
├── package.json
├── functions
│ ├── get_private.js #👈新規作成
│ └── get_public.js
├── resources
│ ├── shared-authorizer.yml
│ └── functions.yml
└── serverless.yml
このget_private.jsファイルには以下のように編集しておきます。
module.exports.handler = async (event, context, callback) => {
callback(null, {
statusCode: 200,
body: JSON.stringify({
result: 'Greeting!!'
}),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
},
})
}
このハンドラ関数を使って、functions.ymlへ新しいAPI関数を登録しておきます。
#...中略
getPrivate:
handler: functions/get_private.handler
events:
- http:
path: /private
method: get
authorizer:
type: COGNITO_USER_POOLS
authorizerId:
Ref: ApiGatewaySharedAuthorizer
ここでローカルでビルドエラーが無いか
invoke local
$ yarn invoke:lcl getPrivate
{
"statusCode": 200,
"body": "{\"result\":\"Greeting!!\"}",
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true"
}
}
ローカル環境ではバックエンドで動いているサービスが無いのでエンドポイントで認証させることはできませんが、シンタックスエラーが無いかなどを確認するのに便利です。
特にエラーなど問題なければコードとしてはコンパイルできています。
ではApi Gateway側へデプロイしてみます。
$ yarn deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
#...中略
endpoints:
GET - https://**********.execute-api.ap-northeast-1.amazonaws.com/dev
GET - https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/public
GET - https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/private
functions:
getPublic: my-serverless-learning-dev-getPublic
getPrivate: my-serverless-learning-dev-getPrivate
layers:
None
#...
この
https://**********.execute-api.ap-northeast-1.amazonaws.com/dev/private
$ curl --include https://*********.execute-api.ap-northeast-1.amazonaws.com/dev/private \
-H "Authorization:[正しいIdトークン]"
HTTP/2 200
content-type: application/json
#...中略
{"result":"Greeting!!"}
$ curl --include https://*********.execute-api.ap-northeast-1.amazonaws.com/dev/private \
-H "Authorization:[間違ったIdトークン]"
HTTP/2 401
content-type: application/json
#...中略
{"message":"Unauthorized"}
新規作成したCognitoユーザープールでオーソライザーを作成する
前回紹介した
そこでCognitoユーザープールの構築もymlで書ければ再生産性も上がり、後々管理しやすいというメリットがあります。
ということで、先程の既存のCognitoユーザープールを、新規作成したCognitoユーザープールに置き換えて、新しくCognitoオーソライザーを設定してみます。
shared-authorizer.yml
serverless.yml
#...中略
resources:
#👇前のCognitoオーソライザーはコメントアウト
#- ${file(resources/shared-authorizer.yml)}
#👇新しいCognitoオーソライザー
- ${file(resources/cognito-authorizer.yml)}
#👇Cognitoユーザープール構築用
- ${file(resources/cognito-user-pool.yml)}
#...以下略
ではこのymlの中身を以降で作成していきます。
cognito-user-pool.yml
まずはCognitoユーザープールの設計図にあたるyamlファイルを以下のように与えます。
だいたい
Resources:
#👇新規Cognito User Pool作成の宣言
MySLSAuthUserPool:
Type: 'AWS::Cognito::UserPool'
Properties:
#👇ユーザーパスワード復旧方法
AccountRecoverySetting:
RecoveryMechanisms:
- Name: 'verified_email'
Priority: 1
#👇管理者権限を持つLambdaでのみユーザー作成
AdminCreateUserConfig:
#👇ユーザーがSDKでサインアップする場合はfalse
AllowAdminCreateUserOnly: false
InviteMessageTemplate:
EmailMessage: 'Your username is {username} and temporary password is {####}.'
EmailSubject: 'Your temporary password'
SMSMessage: 'Your username is {username} and temporary password is {####}.'
AliasAttributes:
- email
- preferred_username
#👇ユーザーのサインアップ方法(EmailかSMSを選択)
AutoVerifiedAttributes:
#👇今回はEmailタイプを指定
- email
DeviceConfiguration:
#👇ユーザーデバイスは記憶しない
ChallengeRequiredOnNewDevice: false
DeviceOnlyRememberedOnUserPrompt: false
EmailConfiguration:
EmailSendingAccount: COGNITO_DEFAULT
EmailVerificationMessage: 'Your verification code is {####}.'
EmailVerificationSubject: 'Your verification code'
#👇MFA認証は利用しない
MfaConfiguration: OFF
#👇パスワードの強さを設定
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: false
RequireNumbers: false
RequireSymbols: false
RequireUppercase: false
#👇一時パスワードはとりあえず最長の1年で設定
TemporaryPasswordValidityDays: 365
Schema:
- AttributeDataType: String
DeveloperOnlyAttribute: false
#👇IDプロバイダを追加し属性マッピングを拡張する場合には変更可能に設定
Mutable: true
Name: email
Required: true
SmsAuthenticationMessage: 'Your verification code is {####}.'
SmsVerificationMessage: 'Your verification code is {####}.'
UsernameConfiguration:
CaseSensitive: true
UserPoolAddOns:
#👇CloudWatchにてログ確認のみに設定
AdvancedSecurityMode: AUDIT
#👇ユーザープールの名前(サービス名&ステージ付き)を指定
UserPoolName: ${self:service}-${self:provider.stage}-user-pool
UserPoolTags:
Service: ${self:service}-${self:provider.stage}
VerificationMessageTemplate:
#👇認証リンクなしでコード送信のみ
DefaultEmailOption: CONFIRM_WITH_CODE
EmailMessage: 'Your verification code is {####}.'
EmailSubject: 'Your verification code'
SmsMessage: 'Your verification code is {####}.'
#👇アプリクライアントの設定
MySLSAuthUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
#👇クライアント名(ステージ別)
ClientName: ${self:service}-${self:provider.stage}-user-pool-client
#👇認証フローの指定
ExplicitAuthFlows:
- ALLOW_ADMIN_USER_PASSWORD_AUTH
- ALLOW_CUSTOM_AUTH
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: false
#👇Cognitoから返却するエラー内容を返す
PreventUserExistenceErrors: ENABLED
ReadAttributes:
- email
- preferred_username
#👇リフレッシュトークンの生存日数
RefreshTokenValidity: 10
SupportedIdentityProviders:
- COGNITO
#👇どのPoolに帰属するClientかをRef関数を使って先ほど設定したPoolと関連付け
UserPoolId:
Ref: MySLSAuthUserPool
#👇外部のIDプロバイダー利用時に以下の属性情報の書き込みを有効化
WriteAttributes:
- email
- preferred_username
各属性の大体の説明書きはコメントに残してありますので、何かしらの参考になる程度で、詳しい詳細はCloudFormationのドキュメントをお調べください。
cognito-authorizer.yml
次に先ほど構築したCognitoユーザープールを使ってCognitoオーソライザーを作成します。
オーソライザーの書き方は上記の項目で述べた内容とほぼ同じですが、
Fn::GetAtt
Resources:
MySLSAuthAuthorizer:
Type: AWS::ApiGateway::Authorizer
Properties:
Name: MySLSAuthAuthorizer
RestApiId:
Ref: ApiGatewayRestApi
IdentitySource: method.request.header.Authorization
Type: COGNITO_USER_POOLS
ProviderARNs:
- { Fn::GetAtt: [MySLSAuthUserPool, Arn] }
functions.ymlの修正
#...中略
getPrivate:
handler: functions/get_private.handler
events:
- http:
path: /private
method: get
# integration: lambda ... x
# integration: lambda-proxy も可
authorizer:
type: COGNITO_USER_POOLS
authorizerId:
# Ref: ApiGatewaySharedAuthorizer
Ref: MySLSAuthAuthorizer
ちなみに、API GatewayにLambdaを設定する際にLambdaプロキシ統合をするかしないかが選択できます。
デフォルトではLambdaプロキシ有りの設定になります(
integration: lambda-proxy
Lambdaプロキシ統合を選択すると、レスポンスの出力フォーマットが既に定義されているものを使うことが出来るようになります。 よって基本は全てAPI Gatewayにレスポンスをお任せすることで、開発者にリソースの設計まで深く考えることなしにAPIを利用することが可能です。
一方、ここでは利用しませんが、非プロキシ統合(
integration: lambda
マッピングテンプレートなどで任意の出力フォーマットでレスポンス返したいときに利用します。
デプロイ
これらのリソースを追加・修正してみたらデプロイを行ってみましょう。
$ yarn deploy
正常にデプロイが完了すると、Cognitoユーザープールが以下のように新規追加されてることがわかります。

手っ取り早く試すため
$ curl --include https://************.execute-api.ap-northeast-1.amazonaws.com/dev/public
HTTP/2 200
content-type: application/json
#...中略
{"result":"Greeting!"}
$ curl --include https://************.execute-api.ap-northeast-1.amazonaws.com/dev/private \
-H "Authorization:[正しいIdトークン]"
HTTP/2 200
content-type: application/json
#...中略
{"result":"Greeting!!"}
$ curl --include https://************.execute-api.ap-northeast-1.amazonaws.com/dev/private \
-H "Authorization:[間違ったIdトークン]"
HTTP/2 401
content-type: application/json
#...中略
{"message":"Unauthorized"}
以上のようなレスポンスになっていたら万事OKです。
まとめ
今回はCognitoユーザープールを自動構築したい人のためのSLSの設定ファイルの書き方について、手動構築の場合と比較しながら解説していきました。
構築したCognitoユーザープールはもっとも単純な構成のあり、Idプールなどとの連携機能もこれから盛り込んで色々と拡張してみたいところですが、次回以降はAngularプロジェクトでサーバーサイドレンダリングしたページの認証操作をどう実現していくか見ていこうと思います。
参考サイト
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー