AWS AppSync + Amplifyで、AWS_IAM認証を使って、認証されていないユーザーに対してQueryを許可してみた

AWS AppSyncを使ってGraphQL APIを公開する場合、 Default authorization mode が必須なため、何らかの方法での認証を行う必要があります。
セキュリティ - AWS AppSync

認証されていないユーザーでもAPIを使えるようにしたいと考えた場合、簡単なのはAPI_KEY認証です。

ただし、APIで共通のAPIキーを使い、またAPIキーの有効期限があるなどの問題があり、ドキュメントにも

API キーは、最大 365 日間有効に設定可能で、該当日からさらに最大 365 日、既存の有効期限を延長できます。API キーは、パブリック API の公開が安全であるユースケース、または開発目的での使用が推奨されます。

と書かれています。

 
他の認証方法のうち、OPENID_CONNECT認証とAMAZON_COGNITO_USER_POOLS認証については、何らかの形でログインが必要になります。

残るはAWS_IAM認証ですが、

  • 認証されていないユーザーがアクセスする場合、Amazon CognitoのIDプールの 認証されていないロール が適用される
  • Amazon Cognitoには、 認証されていない ID に対してアクセスを有効にする という設定がある

ことから、これを使えばよいのではと考えました。

 
そこで、AWS_IAM認証を使って、認証されていないユーザーに対してQueryを許可してみた時のメモを残します。

 
なお、記載に誤りがありましたら、ご指摘ください。

 
ちなみに、現在のAmplify CLI (1.8.2)でAppSync APIを作成する場合、AWS_IAM認証の設定を行えません。
Add AWS_IAM via Identity Pool as an authorization option when adding a GraphQL API · Issue #1386 · aws-amplify/amplify-cli

そのため、Amplify CLIで可能な限り作業しつつ、足りないことはAWSの各Consoleで設定する形にします。

 
目次

 

環境

  • @aws-amplify/cli 1.8.2
  • 使用するAmplifyモジュール

 
また、今回Queryを実行する対象のDynamoDBテーブル Todo は以下とします。

項目 備考
title String 必須
content String

 

Amplifyモジュールの追加

Amplify CLIを使って、必要なモジュールを追加していきます。

 

amplify init
$ amplify init
Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project unauthaccesswithIAM
? Enter a name for the environment dev
? Choose your default editor: Visual Studio Code
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using none
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script start
Using default provider  awscloudformation

For more information on AWS Profiles, see:
https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html

? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use default

 

Authモジュールの追加

ポイントは、Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM)」を Yes とすることです。これにより、IDプールの 認証されていない ID に対してアクセスを有効にする がチェックされます。

$ amplify auth add
Using service: Cognito, provided by: awscloudformation
 
 The current configured provider is Amazon Cognito. 
 
 Do you want to use the default authentication and security configuration? Manual configuration
 Select the authentication/authorization services that you want to use: User Sign-Up, Sign-In, connected with AWS IAM controls (Enables per-user Storage features for images or oth
er content, Analytics, and more)
 Please provide a friendly name for your resource that will be used to label this category in the project: UnauthDynamoDBResource
 Please enter a name for your identity pool. UnauthDynamoDBRIdentityPool

 # 認証されていないIDに対してアクセスを有効にするにチェックを入れる
 Allow unauthenticated logins? (Provides scoped down permissions that you can control via AWS IAM) Yes

 Do you want to enable 3rd party authentication providers in your identity pool? No
 Please provide a name for your user pool: UnauthDynamoDBRUserPool
 Warning: you will not be able to edit these selections. 
 How do you want users to be able to sign in? Username
 Multifactor authentication (MFA) user login options: OFF
 Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
 Please specify an email verification subject: Your verification code
 Please specify an email verification message: Your verification code is {####}
 Do you want to override the default password policy for this User Pool? No
 Warning: you will not be able to edit these selections. 
 What attributes are required for signing up? (Press <space> to select, <a> to toggle all, <i> to invert selection)Email
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? No
 Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection)
 Do you want to use an OAuth flow? No
? Do you want to configure Lambda Triggers for Cognito? No

 

APIモジュールの追加

今回は、Amplify CLIの実行後に、その内容を修正・追加する形をとります。

なお、前述の通り 「Choose an authorization type for the API」は

  • API_KEY認証
  • AMAZON_COGNITO_USER_POOLS認証

のどちらかしか選択できないため、ひとまず動作確認が楽な API key を選んでおきます。

$ amplify api add
? Please select from one of the below mentioned services GraphQL
? Provide API name: UnauthDynamoDBAPI

# API_KEY認証にする
? Choose an authorization type for the API API key

? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? No
? Provide a custom type name Todo

 
なお、<root>/amplify/backend/api/UnauthDynamoDBAPI/build ディレクトリにある

  • stacks/Todo.json
  • resolvers/

などは、amplify push すると上書きされてしまうため、どこか別のところに保存しておくと参照できるので便利です。

 

schema.graphqlの修正

Amplify CLIで作成されたschema.graphqlでは、@model ディレクティブがあるためにQuery/Mutation/Subscriptionができてしまいます。

そのため、今回必要なものだけに修正します。Queryの他に、きちんとアクセス制限されるかを確認するためにMutationも用意します。

type Todo {
    title: String!
    content: String
}

input CreateTodoInput {
    title: String!
    content: String
}


type Query {
    getTodo(title: String!): Todo
}

type Mutation {
    createTodo(input: CreateTodoInput!): Todo
}

 

stacks/Todo.jsonの修正

こちらも今回不要なものがいくつもあるため、CloudFormationのドキュメントを参照しつつ、修正を加えます。

AmplifyにおけるCloudFormationの設定は、今のところJSONしか対応していないため、行数が多くなってしまうのが残念です。
Cloudformation template format. · Issue #1286 · aws-amplify/amplify-cli

# YAMLで書いた時のエラー
Yaml is not yet supported. Please convert the CloudFormation stack ExistsDynamoDB.yaml to json.

 

Parametersまで

必要なパラメータだけに絞ります。

なお、AppSyncAPIのIDは、 AppSyncApiId というパラメータ名で受け取れます。
How to resolve GetAttGraphQLAPIApiId in CustomResources.json · Issue #1109 · aws-amplify/amplify-cli

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Todo stack",
    "Metadata": {},
    "Parameters": {
        "AppSyncApiId": {
            "Type": "String",
            "Description": "The id of the AppSync API associated with this project."
        },
        "env": {
            "Type": "String",
            "Description": "The environment name. e.g. Dev, Test, or Production",
            "Default": "NONE"
        },
        "DynamoDBBillingMode": {
            "Type": "String",
            "Description": "Configure @model types to create DynamoDB tables with PAY_PER_REQUEST or PROVISIONED billing modes.",
            "Default": "PAY_PER_REQUEST",
            "AllowedValues": [
                "PAY_PER_REQUEST",
                "PROVISIONED"
            ]
        },
        "S3DeploymentBucket": {
            "Type": "String",
            "Description": "The S3 bucket containing all deployment assets for the project."
        },
        "S3DeploymentRootKey": {
            "Type": "String",
            "Description": "An S3 key relative to the S3DeploymentBucket that points to the root of the deployment directory."
        }
    },
    "Resources": {
...

 

DynamoDBテーブルの作成

まずは、今回使用するDynamoDBテーブルの作成をResourcesに記述します。

 
今回は、必須設定の

  • TableName
  • KeySchema
  • AttributeDefinitions
  • BillingMode

のみ行い、後はデフォルトのままです。

"TodoTable": {
    "Type": "AWS::DynamoDB::Table",
    "Properties": {
        "TableName": {
            "Fn::Join": [
                "-",
                [
                    "TodoUnauth",
                    {
                        "Ref": "env"
                    }
                ]
            ]
        },
        "KeySchema": [
            {
                "AttributeName": "title",
                "KeyType": "HASH"
            }
        ],
        "AttributeDefinitions": [
            {
                "AttributeName": "title",
                "AttributeType": "S"
            }
        ],
        "BillingMode": {
            "Fn::If": [
                "ShouldUsePayPerRequestBilling",
                "PAY_PER_REQUEST",
                {
                    "Ref": "AWS::NoValue"
                }
            ]
        }
    }
},

 
また、 Conditions に、 ShouldUsePayPerRequestBilling の設定をします。

    },  // Resources の閉じカッコ
    "Conditions": {
        "ShouldUsePayPerRequestBilling": {
            "Fn::Equals": [
                {
                    "Ref": "DynamoDBBillingMode"
                },
                "PAY_PER_REQUEST"
            ]
        }
    }
}

 

DynamoDBを操作するIAMロールを作成

AppSyncのDataSourceを指定する場合、 SerciceRoleArn (データソースがリソースへの接続に使用するIAMロール) が必要になるため、IAMロールを作成します。

 
ポイントとしては、AppSyncでの利用で誤った操作をされないよう、PoliciesのActionは dynamodb:GetItem だけ許可しておきます。

また、AppSyncで利用するためのAssumeRole設定を、AssumeRolePolicyDocumentに記載します。
IAMロール徹底理解 〜 AssumeRoleの正体 | DevelopersIO

"AssumeRolePolicyDocument": {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "appsync.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
},

 
全体は以下です。

"TodoIAMRole": {
    "Type": "AWS::IAM::Role",
    "Properties": {
        "RoleName": {
            "Fn::Join": [
                "-",
                [
                    "TodoTableUnauthAccessRole",
                    {
                        "Ref": "env"
                    }
                ]
            ]
        },
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "appsync.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        },
        "Policies": [
            {
                "PolicyName": "DynamoDBAccess",
                "PolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Action": [
                                "dynamodb:GetItem"
                            ],
                            "Resource": [
                                {
                                    "Fn::Sub": [
                                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}",
                                        {
                                            "tablename": {
                                                "Ref": "TodoTable"
                                            }
                                        }
                                    ]
                                },
                                {
                                    "Fn::Sub": [
                                        "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tablename}/*",
                                        {
                                            "tablename": {
                                                "Ref": "TodoTable"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            }
        ]
    }
},

 

AppSyncのDataSourceを指定

ここまでで、DynamoDBテーブル & 操作するIAMロールができましたので、次はAppSyncまわりの設定です。

まずはDataSourceを設定します。
AWS::AppSync::DataSource - AWS CloudFormation

 
ポイントとしては ServiceRoleArn に、先ほど作成した TodoIAMRoleArn を指定することです。

"ServiceRoleArn": {
    "Fn::GetAtt": ["TodoIAMRole", "Arn"]
},

 
全体です。

"TodoDataSource": {
    "Type": "AWS::AppSync::DataSource",
    "Properties": {
        "ApiId": {"Ref": "AppSyncApiId"},
        "Name": "TodoTable",
        "Type": "AMAZON_DYNAMODB",
        "ServiceRoleArn": {
            "Fn::GetAtt": ["TodoIAMRole", "Arn"]
        },
        "DynamoDBConfig": {
            "AwsRegion": {
                "Ref": "AWS::Region"
            },
            "TableName": {"Ref": "TodoTable"}
        }
    }
},

 

ゾルバーの設定

必要なのはQueryリゾルバーだけですが、今回AppSync Consoleからも確認できるようにMutaitionリゾルバーも用意します。
AWS::AppSync::Resolver - AWS CloudFormation

 
ポイントは

  • DataSourceNameに、上記で作成したDataSourceのNameを指定
    • "Fn::GetAtt": ["TodoDataSource", "Name"]
  • FieldNameは、GraphQLのQuery/Mutationの名前と合わせる

あたりです。

"GetTodoResolver": {
    "Type": "AWS::AppSync::Resolver",
    "Properties": {
        "ApiId": {"Ref": "AppSyncApiId"},
        "DataSourceName": {
            "Fn::GetAtt": [
                "TodoDataSource",
                "Name"
            ]
        },
        "FieldName": "getTodo",
        "TypeName": "Query",
        "RequestMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Query",
                                "getTodo",
                                "req",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        },
        "ResponseMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Query",
                                "getTodo",
                                "res",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        }
    }
},

"CreateTodoResolver": {
    "Type": "AWS::AppSync::Resolver",
    "Properties": {
        "ApiId": {"Ref": "AppSyncApiId"},
        "DataSourceName": {
            "Fn::GetAtt": [
                "TodoDataSource",
                "Name"
            ]
        },
        "FieldName": "createTodo",
        "TypeName": "Mutation",
        "RequestMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Mutation",
                                "createTodo",
                                "req",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        },
        "ResponseMappingTemplateS3Location": {
            "Fn::Sub": [
                "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/${ResolverFileName}",
                {
                    "S3DeploymentBucket": {
                        "Ref": "S3DeploymentBucket"
                    },
                    "S3DeploymentRootKey": {
                        "Ref": "S3DeploymentRootKey"
                    },
                    "ResolverFileName": {
                        "Fn::Join": [
                            ".",
                            [
                                "Mutation",
                                "createTodo",
                                "res",
                                "vtl"
                            ]
                        ]
                    }
                }
            ]
        }
    }
}

 

IDプールの「認証されていないロール」にアタッチするポリシーを作成

ここまででDynamoDBとAppSyncの設定が終わりました。

最後に、IDプールの 認証されていないロール にアタッチする管理ポリシーを作成します。このポリシーは、後で「認証されていないロール」に手動でアタッチします。Amplify CLIAWS_IAM認証に対応していないためです。

 
ポイントとしては、Statementで

を行うことです。

"UnauthAccessPolicy": {
    "Type": "AWS::IAM::ManagedPolicy",
    "Properties": {
        "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "appsync:GraphQL"
                    ],
                    "Resource": [
                        {
                            "Fn::Sub": [
                                "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiID}/types/Query/*",
                                {
                                    "apiID": {
                                        "Ref": "AppSyncApiId"
                                    }
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        "ManagedPolicyName": {
            "Fn::Sub": [
                "MyUnauthAccessPolicy-${apiID}",
                {
                    "apiID": {
                        "Ref": "AppSyncApiId"
                    }
                }
            ]
        }
    }
},

 

リクエスト/レスポンスマッピングテンプレートの作成

ゾルバで指定した、リクエスト/レスポンスマッピングテンプレートを <root>/amplify/backend/api/UnauthDynamoDBAPI/resolvers ディレクトリに作成します。

 

Query.getTodo.req.vtl

operationをTodoIAMRoleで指定した GetItem にして、リクエストテンプレートを作成します。

{
  "version": "2017-02-28",
  "operation": "GetItem",
  "key": {
    "title": $util.dynamodb.toDynamoDBJson($ctx.args.title)
  }
}

 

Query.getTodo.res.vtl

結果をそのまま返します。

$util.toJson($context.result)

 

Mutation用のマッピングテンプレート

今回の動作確認用のMutation用マッピングテンプレートも用意します。

Mutation.createTodo.req.vtl

## [Start] Prepare DynamoDB PutItem Request. **
$util.qr($context.args.input.put("createdAt", $util.defaultIfNull($ctx.args.input.createdAt, $util.time.nowISO8601())))
$util.qr($context.args.input.put("updatedAt", $util.defaultIfNull($ctx.args.input.updatedAt, $util.time.nowISO8601())))
$util.qr($context.args.input.put("__typename", "Todo"))
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "title": { "S" : "${context.args.input.title}" }
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($context.args.input),
  "condition": {
      "expression": "attribute_not_exists(#title)",
      "expressionNames": {
          "#title": "title"
    }
  }
}
## [End] Prepare DynamoDB PutItem Request. **

 
Mutation.createTodo.res.vtl

$util.toJson($context.result)

 

pushして動作確認

ここまでで設定ができたため、

$ amplify push

します。

その後、AppSync ConsoleのQueriesで、MutationとQueryが成功することを確認します。

 

Mutation
mutation CreateTodo($params: CreateTodoInput!) {
  createTodo(input: $params) {
    title
    content
  }
}

 
QUERY_VALUESです。

{
  "params": {
    "title": "Hello, title!",
    "content": "Hello, content!"
  }
}

 
実行すると以下が表示されます。

{
  "data": {
    "createTodo": {
      "title": "Hello, title!",
      "content": "Hello, content!"
    }
  }
}

 

Query

Mutationと同様です。

query GetTodo($title: String!) {
  getTodo(title: $title) {
    title
    content
  }
}

 
QUERY_VALUESです。

{
  "params": {
    "title": "Hello, title!",
    "content": "Hello, content!"
  },
  "title": "Hello, title!"
}

 
結果です。

{
  "data": {
    "createTodo": {
      "title": "Hello, title!",
      "content": "Hello, content!"
    }
  }
}

 

AWS_IAM認証化

ここまではAPY_KEY認証でしたので、AWS_IAM認証へと切り替えます。

 

AppSync Consoleでの認証切り替え

Settings にある Default authorization mode を、 AWS Identity and Access Management (IAM) へと変更し、 Save をクリックします。

 

IAMロールで、認証されていないロールにポリシーをアタッチ

まずは、CognitoのIDプールにて、編集画面から 認証されていないロール のロール名を確認します(今回の場合、 unauthaccess-dev-20190720202229-unauthRole )。

f:id:thinkAmi:20190721115435p:plain:w300

 
次に、IAMのロールで確認したロール名を選択し、stacks/Todo.jsonUnauthAccessPolicy セクションで作成したポリシー (MyUnauthAccessPolicy-<AppSync API ID>)をアタッチします。

f:id:thinkAmi:20190721115618p:plain:w200

 

動作確認

Amplify Framework JavaScriptを使って、ボタンを押したらQuery/Mutationを実行する動作確認アプリを作成します。

以下ではindex.htmlとsrc/app.jsのみ掲載します。

  • package.json
  • webpack.config.js

は、Amplify公式チュートリアルを流用したものを用意します。
Amplify JavaScript - Getting Started

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Amplify Framework</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div class="app">
    <button id="query">Run Query</button>
</div>
<div>
    <input type="text" id="title" placeholder="title">
    <button id="mutation">Run Mutation</button>
</div>
<script src="main.bundle.js"></script>
</body>
</html>

 

src/app.js
import Amplify, {API} from 'aws-amplify';
import awsconfig from './aws-exports';
import * as queries from "./graphql/queries";
import * as mutations from "./graphql/mutations";


const queryButton = document.getElementById('query');
queryButton.addEventListener('click', () => {
  console.log('Run Query!');
  Amplify.configure(awsconfig);
  API.graphql(
    {
      query: queries.getTodo,
      authMode: 'AWS_IAM',
      variables: {
        title: "Hello, title!"
      }
    }
  ).then((data) => {
    console.log(data);
  })
    .catch((e) => {
      console.log('error!');
      console.log(e)
    });
});


const mutationButton = document.getElementById('mutation');
mutationButton.addEventListener('click', () => {
  console.log('Run Mutation!');

  const title = document.getElementById('title').value;
  Amplify.configure(awsconfig);
  API.graphql(
    {
      query: mutations.createTodo,
      authMode: 'AWS_IAM',
      variables: {
        input: {
          title: title,
          content: "hello"
        }
      }
    }
  ).then((data) => {
    console.log(data);
  })
    .catch((e) => {
      console.log('error!');
      console.log(e)
    });
});

 

結果

$ npm start してアプリを起動後、動作を確認してみます。

すると、

  • Queryは成功
  • MutationはHTTP 401 エラー

となり、認証されていないユーザーに対してQueryのみ許可できました。

f:id:thinkAmi:20190721111936p:plain:w450

 

ソースコード

Githubに上げました。 unauth_access ディレクトリ以下が今回のファイルです。
https://github.com/thinkAmi-sandbox/AWS_AppSync_Amplify-sample