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:ここでめちゃくちゃハマりました。