概要
ユーザーがアップロードした画像データをS3に保存するケースにおいて Serverless Frameworkを使用して、AWS API Gateway 経由しLambdaで処理をするときに、 Cognitoで認証したユーザーのIAMをSTSを使用してS3にPUTするときの説明です。
今日は上記の図の四角で囲った部分の話をします。
serverless.yml
provider: name: aws runtime: nodejs6.10 stage: dev region: us-west-2 # iamRoleStatements: # - Effect: "Allow" # Action: # - "s3:PutObject" # Resource: # - "arn:aws:s3:::yukashita-image-uploads/original-files/${self:provider.stage}/*" functions: auth: handler: auth.auth upload: handler: handler.upload events: - http: path: upload method: post authorizer: auth response: headers: Content-Type: "'application/json'" template: $input.path('$') resources: Resources: UploadBucket: Type: AWS::S3::Bucket Properties: BucketName: "bucket"
iamRoleStatements
をコメントアウトしていますが、Lambda自体にIAM(認可)の必要はありません。
というのも、認証されたCognitoIdentity UserのIAMで操作をするので、
必要な権限はCognitoのIAMに設定していきます。(設定自体はCognitoのIAM設定で説明します。)
Lambda
'use strict'; const aws = require("aws-sdk"); var cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18', region: 'us-west-2' }); module.exports.upload = (event, context, callback) => { var AWS = require('aws-sdk'); AWS.config.region = 'us-west-2'; console.log(AWS.config.credentials) // Enviroment Credentialとなる。つまり、Lambdaで設定されているCredential var options = { params: { apiVersion: '2006-03-01', Bucket: "bucket" } }; var bucket = new AWS.S3(options); var idToken = event.headers.Authorization; // 認証されたCognitoIdentity UserのidTokenを取得 // AWS IAM STSでCognitoAuthUserに紐づいたクレデンシャルを取得。 // CognitoIdentityCredentialsは内部的にSTSを返してくれる。 AWS.config.credentials = new AWS.CognitoIdentityCredentials({ region: "us-west-2", IdentityPoolId : 'us-west-2:your-identity--pool-id', RoleArn: "arn:aws:iam::123456789:role/Cognito_your_poolAuth_Role", // AuthRoleであることに注意 Logins : { 'cognito-idp.us-west-2.amazonaws.com/us-west-2_ABCDEFG' : idToken // idToken != accessTokenであること } }); // AWS.config.credentialsを書き換えただけでは反映はされない // この時点では、AWS.config.credentials.needsRefresh == trueとなる // AWS.config.credentials.getを呼ぶと、未反映の場合はAWS.config.credentials.refreshを呼び出し、反映させる AWS.config.credentials.get(function(err) { // この時点で、AWS.config.credentials.needsRefresh == falseとなる // Cognito Credentialとなる。 console.log(AWS.config.credentials) if (err) console.log(err); var params = { Key: ["bucket", AWS.config.credentials.identityId, "object_name.jpg"].join("/"), // AWS.config.credentials.identityIdで認証をされたCognitoIdentity Userしか触れない領域を確保する。 ContentType: "image/jpg", Body: event.body }; bucket.putObject(params, function(err, data) { if (err) { console.log("Error", err); } else { console.log("Success", err); } }); }); callback(null, event); };
ちょっと長いのですが、注意点はコードコメントにしています。 かいつまんでポイントを説明します。
- Lambda起動直後は
AWS.config.credentials
はEnviroment Credentialとなる。つまり、Lambdaで設定されているCredentialです。この時点ではLambdaにはなんの権限がないので、AWSリソースにアクセスしようとした場合、AccessDenyになります。 AWS.CognitoIdentityCredentials
でCognitoIdentity Userのクレデンシャルを取得します。また、このときに渡すTokenはidTokenです。accessTokenではありません。公式ドキュメント- AWS.config.credentials = new AWS.CognitoIdentityCredentialsとしただけでは未反映の状態です。AWS.config.credentials.getで反映させる必要があります。
- AWS.config.credentials.get後の
AWS.config.credentials
はCognitoCredentialとなります。これでCognitoで設定したIAMの権限に切り替わります。
最後にCognitoのIAM設定を確認します。
CognitoのIAM設定
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "mobileanalytics:PutEvents", "cognito-sync:*", "cognito-identity:*" ], "Resource": [ "*" ] }, { "Effect": "Allow", "Action": [ "s3:PutObject" ], "Resource": [ "arn:aws:s3:::bucket/${cognito-identity.amazonaws.com:sub}/*" ] } ] }
Resourceで設定する内容は、LambdaのS3.putObjectの際に指定するKeyの許可について指定します。
${cognito-identity.amazonaws.com:sub}
を指定していますが、これはCognito Identity poolのIdentityIDです。
設定するIAM 変数名がものすごくわかりにくいのですが、Cognito User pool の Userに割り当てられるsubではありません。*1
このIAM変数を利用すると、s3://bucket/Cognito Identity poolのIdentityID/オブジェクト
にしかアクセスできなくなります。
言い換えると、他のユーザーの情報にアクセスが不可能になります。
また、同様にDynamoDB Finegrain アクセスも同じ概念となります。
解決できなかったこと
今回はCognitoのIAMをAWSマネジメントコンソールから手で書き換えるしかありませんでした。
理由はserverless.ymlでIAMのresourceを作成できないからです。
具体的に言うと、IAM > Roles > Trust relationship の Principal > Federatedをcognito-identity.amazonaws.com
に設定できません。
そのため、ここだけは手動でやらなくてはならないという悲しい状況でした。
Cognitoは設定値が紛らわしく、どれを使えば良いの?といった感じです。 今回出てきたものは、下記の通りです。
- idToken != accessToken
- Cognito Identity pool の IdentityID != Cognito User pool の User sub
まとめ
一言でまとめると、 Lambdaの処理中にCognito Identity で設定したIAMを使う だけだと思います。
serveless.ymlでIAMを定義できなかったり、AWS.congif.credentialsがrefreshしないと反映されなかったり、そもそもCognitoの設定値が紛らわしかったりで、散々ハマりましたが、
おかげで各サービスの理解が深まりました。
*1:ここでめちゃくちゃハマりました。