production.log

ピクスタ株式会社でエンジニアのマネージャーをやっている星直史のブログです。

【ServerlessFramework】AWS LambdaとCognitoで作るセキュアなS3へのオブジェクト格納

概要

ユーザーがアップロードした画像データをS3に保存するケースにおいて Serverless Frameworkを使用して、AWS API Gateway 経由しLambdaで処理をするときに、 Cognitoで認証したユーザーのIAMをSTSを使用してS3にPUTするときの説明です。

f:id:watasihasitujidesu:20170417083912p:plain

今日は上記の図の四角で囲った部分の話をします。

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);
};

ちょっと長いのですが、注意点はコードコメントにしています。 かいつまんでポイントを説明します。

  1. Lambda起動直後はAWS.config.credentialsEnviroment Credentialとなる。つまり、Lambdaで設定されているCredentialです。この時点ではLambdaにはなんの権限がないので、AWSリソースにアクセスしようとした場合、AccessDenyになります。
  2. AWS.CognitoIdentityCredentialsでCognitoIdentity Userのクレデンシャルを取得します。また、このときに渡すTokenはidTokenです。accessTokenではありません。公式ドキュメント
  3. AWS.config.credentials = new AWS.CognitoIdentityCredentialsとしただけでは未反映の状態です。AWS.config.credentials.getで反映させる必要があります。
  4. AWS.config.credentials.get後のAWS.config.credentialsCognitoCredentialとなります。これで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:ここでめちゃくちゃハマりました。