[AWS Amplify] API(Graphql)のパブリック設定。複数認証。subscriptionまで。

2021-04-04AWS Amplify,IT記事AWS Amplify,GraphQL

前回公開したアプリでは、ログインした人たちが投稿と投票をできて、ログインしていない人もどういう投稿がされているか見ることができます。つまり、認証と公開(パブリック)設定の複数認証になっています。

今回はその部分、AWS AmplifyのAPI(GraphQL)における複数の認証設定方法の記事になります。といいたいのですが、まだまだ自分でもよくわかっていないことが多く、更新も多い部分なのでメモ程度と思って読んでください。

説明としてまず以下のような型、Comment型があるとします。

type Comment {
  title : String!       // 内容
  like : Int!           // いいね数
}

コメントの内容といいねの数があります。これをこの記事では以下のように変更していきます。

・認証(ログイン)した人は自分のコメントを作成、削除、読み、更新できる。

・認証した人は他の認証した人のコメントを読めて、いいねをインクリメントできる。

・認証していない人もすべての人のコメントを読むことだけできる。

・認証している人も、していない人も、すべてのコメントの作成と更新と削除のサブスクリプションをできる。

上記のスキーマは最終的に以下のようになります。

type Comment
  @model(subscriptions: { level: public })
  @aws_iam
  @aws_cognito_user_pools
  @auth(
    rules: [
      { allow: owner, operations: [create, read, update, delete] }
      { allow: groups, groups: ["everyone"], operations: [read] }
      { allow: public, provider: iam, operations: [read] }
    ]
  )
{
  id: ID!
  title: String!
  like: Int!
}

type Mutation {
  incrementLikeCount(id: ID!): Comment @aws_cognito_user_pools
}

type Subscription {
  onCreateCommentPublic(): Comment
    @aws_subscribe(mutations: ["createComment"])
    @aws_iam
    @aws_cognito_user_pools
  onUpdateCommentPublic(): Comment
    @aws_subscribe(mutations: ["incrementLikeCount"])
    @aws_iam
    @aws_cognito_user_pools
  onDeleteCommentPublic(): Comment
    @aws_subscribe(mutations: ["deleteComment"])
    @aws_iam
    @aws_cognito_user_pools
}

ここにはスキーマだけ書きましたが、スキーマの変更だけではなく、様々なリソースを書く必要があります。

参考

https://medium.com/@fullstackpho/aws-amplify-multi-auth-graphql-public-read-and-authenticated-create-update-delete-1bf5443b0ad1

https://github.com/aws-amplify/amplify-cli/issues/2715

認証した人が自分のコメントを書いて読めるようにする。

ではまず認証した人(cognitoでログインした人)が、自分のコメントを書いて編集できるように変更します。

スキーマを以下のようにします。

type Comment
  @model
  @aws_cognito_user_pools
  @auth(
    rules: [
      { allow: owner, operations: [create, read, update, delete] }
    ]
  ) {
  id: ID!
  title: String!
  like: Int!
}

これはチュートリアルにもある部分です。operationsを明示的に書いていますが省略できます。

認証した人同士コメントを読めるようにする。

ここから認証した人同士がコメントを読めるようにします。

仕組みとしては、まずCognitoでユーザーがアカウントを登録したときに全員everyoneグループに所属させます。そしてapiの方でeveryoneグループに所属する人はコメントが読めるという認証設定にします。

具体的にはまずスキーマを以下のように変更します。

type Comment
  @model
  @aws_cognito_user_pools
  @auth(
    rules: [
      { allow: owner, operations: [create, read, update, delete] }
      { allow: groups, groups: ["everyone"], operations: [read] }
    ]
  ) {
  id: ID!
  title: String!
  like: Int!
}

これで"everyone"グループに入っている人は全員読める、となります。

次にcognitoにアカウントを作成時にアカウントをeveryoneグループに放り込む処理が必要になります。

しかし実装する必要はなくamplify cliで用意されています。

amplify auth update

// groupの作成

// 認証のアドバンスド設定、group追加。

ここ後で追記します。

認証していない人も読めるようにする。

次に認証していない人も読めるようにします。

このパブリックにする方法には2種類あって、一つはapi key認証、もう一つがiam認証になります。

api key認証は日数制限がある開発時用の認証方法です。

公開したままにしたいならiam認証を使います。

まずamplify apiがiam認証を許可するようCLIで変更します。

amplify update api

? Please select from one of the below mentioned services: GraphQL
? Provide API name: todoamplify
? Choose the default authorization type for the API Amazon Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Do you want to configure advanced settings for the GraphQL API Yes, I want to make some additional changes.
? Configure additional auth types? Yes
? Choose the additional authorization types you want to configure for the API IAM
? Configure conflict detection? No
? Do you have an annotated GraphQL schema? Yes
? Provide your schema file path: C:\Users\haseg\todoamplify\src\schema.graphql

iamを許可している部分は以下です

・Do you want to configure advanced settings for the GraphQL APIで Yesを選ぶ。以下の質問が出ます。

・Configure additional auth types? でiamを選ぶ。これでiamでのアクセスが許可されます。

次にスキーマを記述します。

type Comment
  @model
  @aws_iam
  @aws_cognito_user_pools
  @auth(
    rules: [
      { allow: owner, operations: [create, read, update, delete] }
      { allow: groups, groups: ["everyone"], operations: [read] }
      { allow: public, provider: iam, operations: [read] }             // 追加
    ]
  ) {
  id: ID!
  title: String!
  like: Int!
}

{ allow: public, provider: iam, operations: [read] } となっている部分です。

これでpushすると、UnauthRoleロールという認証していない人用ロールが作られ、そのロールでリソースを読めるようになります。

フロント側で認証方法を切り替える。

ここまでで、ログインしている人と、ログインしていない人が読めるように設定しました。

なのでブラウザ側でログイン状況によって認証方法を切り替えるようにします。

自分はrouterでAuth.currentAthenticatedUser()でログイン状態を確認したあと、認証方法を切り替えています。

以下のように、Amplify.configureで切り替えます。

// 現在のログイン情報を取得
function getUser() {
  return Auth.currentAuthenticatedUser()
    .then((data) => {
      if (data && data.signInUserSession) {
        console.log("サインイン状態を取得。ログイン中 :", data);
        // ユーザー情報を設定
        UserDataStore.commit("setUser", data);
        // Amplifyの認証方法の設定
        Amplify.configure({
          aws_appsync_authenticationType: "AMAZON_COGNITO_USER_POOLS",
        });
        return data;
      }
    })
    .catch(() => {
      console.log("サインイン状態を取得。ログインしていない");
      // 認証をiamに設定
      Amplify.configure({
        aws_appsync_authenticationType: "AWS_IAM",
      });
      UserDataStore.commit("setUser", null);
      return null;
    });
}

これで、ブラウザ側で自動で認証方法を切り替えるようになりました。

いいねをインクリメントできるようにする。

いいね数を変更できるようにします。

好きに数字を入れられると困りますので、いいね数をインクリメントする専用のapiを作ります。

apiを作るには、カスタムリソルバーをVTLで書く方法と、Lambdaを書く方法があります。自分は使うサービスをあまり増やしたくないなと思い、ここでは前者を使いました。

カスタムリソルバーを作るには、カスタムリソルバーを用意して、そのリソルバーを配置するCloudFormationを書く必要があります。

まずスキーマに以下を書きます

type Mutation {
  incrementLikeCount(id: ID!): Comment @aws_cognito_user_pools
}

(ちょっとまだ良くわかってなくて、@aws_cognito_user_pools 効いているかわからない、注意)

次にこのapiを呼び出したときに、使われるリソルバーを書きます。そしてamplify\backend\api\{{project name}}\resolversに配置します。

以下のようなリソルバーを用意しました

{{project name}}\amplify\backend\api\{{project name}}\resolvers\Mutation.incrementLikeCount.req.vtl

{
    "version" : "2017-02-28",
    "operation" : "UpdateItem",
    "key" : {
        "id" : $util.dynamodb.toDynamoDBJson($context.arguments.id)
    },
    "update" : {
        "expression" : "ADD #li :plusOne",
        "expressionNames": {
	        	"#li": "like"  
	      },
        "expressionValues" : {
            ":plusOne" : { "N" : 1 }
        }
    }
}

それとレスポンス側のリソルバーです

{{project name}}\amplify\backend\api\{{project name}}\resolvers\Mutation.incrementLikeCount.res.vtl

#if( $ctx.error )
    $util.error($ctx.error.message, $ctx.error.type)
#else
    $util.toJson($ctx.result)
#end

このリソルバーを配置するCloudFormationを書きます。

自分で書いたCloudFormationは{{project name}}\amplify\backend\api\{{project name}}\stacksに配置するとpush時に実行されます。

{{project name}}\amplify\backend\api\{{project name}}\stacks\APICustomResouces.json

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "An auto-generated nested stack.",
    "Metadata": {},
    "Parameters": {
        "AppSyncApiId": {
            "Type": "String",
            "Description": "The id of the AppSync API associated with this project."
        },
        "AppSyncApiName": {
            "Type": "String",
            "Description": "The name of the AppSync API",
            "Default": "AppSyncSimpleTransform"
        },
        "env": {
            "Type": "String",
            "Description": "The environment name. e.g. Dev, Test, or Production",
            "Default": "NONE"
        },
        "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\nof the deployment directory."
        }
    },
    "Resources": {
        "MutationIncrementLikeCountResolver": {
            "Type": "AWS::AppSync::Resolver",
            "Properties": {
                "ApiId": {
                    "Ref": "AppSyncApiId"
                },
                "DataSourceName": "CommentTable",
                "TypeName": "Mutation",
                "FieldName": "incrementLikeCount",
                "RequestMappingTemplateS3Location": {
                    "Fn::Sub": [
                        "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.incrementLikeCount.req.vtl",
                        {
                            "S3DeploymentBucket": {
                                "Ref": "S3DeploymentBucket"
                            },
                            "S3DeploymentRootKey": {
                                "Ref": "S3DeploymentRootKey"
                            }
                        }
                    ]
                },
                "ResponseMappingTemplateS3Location": {
                    "Fn::Sub": [
                        "s3://${S3DeploymentBucket}/${S3DeploymentRootKey}/resolvers/Mutation.incrementLikeCount.res.vtl",
                        {
                            "S3DeploymentBucket": {
                                "Ref": "S3DeploymentBucket"
                            },
                            "S3DeploymentRootKey": {
                                "Ref": "S3DeploymentRootKey"
                            }
                        }
                    ]
                }
            }
        }
    }
}

これでapiが完成しました。

認証している人もしていない人もサブスクライブできるようにSubscriptionを設定する

最後に認証していない人も認証した人もサブスクライブできるようにします。

まずスキーマを以下のように設定します。

type Comment
  @model(subscriptions: { level: public })              ←ここ多分必要
  @aws_iam
  @aws_cognito_user_pools
  @auth(
    rules: [
      { allow: owner, operations: [create, read, update, delete] }
      { allow: groups, groups: ["everyone"], operations: [read] }
      { allow: public, provider: iam, operations: [read] }
    ]
  )
  {
  id: ID!
  title: String!
  like: Int!
}


type Subscription {                                         ←ここ追加
  onCreateCommnetPublic(): Commnet
    @aws_subscribe(mutations: ["createCommnet"])
    @aws_iam
    @aws_cognito_user_pools
  onUpdateCommnetPublic(): Commnet
    @aws_subscribe(mutations: ["incrementLikeCount"])
    @aws_iam
    @aws_cognito_user_pools
  onDeleteCommnetPublic(): Commnet
    @aws_subscribe(mutations: ["deleteCommnet"])
    @aws_iam
    @aws_cognito_user_pools
}

次にUnauthRoleロールに上記Subscriptionのサブスクライブ許可ポリシーを設定するCloudFormationを書きます。

{{project name}}\amplify\backend\api\{{project name}}\stacks\CustomResources.json

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "An auto-generated nested stack.",
  "Metadata": {},
  "Parameters": {
    "AppSyncApiId": {
      "Type": "String",
      "Description": "The id of the AppSync API associated with this project."
    },
    "AppSyncApiName": {
      "Type": "String",
      "Description": "The name of the AppSync API",
      "Default": "AppSyncSimpleTransform"
    },
    "env": {
      "Type": "String",
      "Description": "The environment name. e.g. Dev, Test, or Production",
      "Default": "NONE"
    },
    "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\nof the deployment directory."
    },
    "unauthRoleName": {
			"Type": "String"
		}
  },
  "Resources": {
    "EmptyResource": {
      "Type": "Custom::EmptyResource",
      "Condition": "AlwaysFalse"
    },
    "UnauthRolePolicy": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyName": "appsync-unauthrole-policy-custom",
        "Roles": [
          {
            "Ref": "unauthRoleName"
          }
        ],
        "PolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": ["appsync:GraphQL"],
              "Resource": [
                {
                  "Fn::Sub": [
                    "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/fields/${fieldName}",
                    {
                      "apiId": {
                        "Ref": "AppSyncApiId"
                      },
                      "typeName": "Subscription",
                      "fieldName": "onUpdateRequestPublic"
                    }
                  ]
                },
                {
                  "Fn::Sub": [
                    "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/fields/${fieldName}",
                    {
                      "apiId": {
                        "Ref": "AppSyncApiId"
                      },
                      "typeName": "Subscription",
                      "fieldName": "onCreateRequestPublic"
                    }
                  ]
                },
                {
                  "Fn::Sub": [
                    "arn:aws:appsync:${AWS::Region}:${AWS::AccountId}:apis/${apiId}/types/${typeName}/fields/${fieldName}",
                    {
                      "apiId": {
                        "Ref": "AppSyncApiId"
                      },
                      "typeName": "Subscription",
                      "fieldName": "onCreateRequestBoardPublic"
                    }
                  ]
                }
              ]
            }
          ]
        }
      }
    }
  },
  "Conditions": {
    "HasEnvironmentParameter": {
      "Fn::Not": [
        {
          "Fn::Equals": [
            {
              "Ref": "env"
            },
            "NONE"
          ]
        }
      ]
    },
    "AlwaysFalse": {
      "Fn::Equals": ["true", "false"]
    }
  },
  "Outputs": {
    "EmptyOutput": {
      "Description": "An empty output. You may delete this if you have at least one resource above.",
      "Value": ""
    }
  }
}

あれcondisionsの部分とか何しているかわからないな。こんなのあったっけ。

バグ対策

いくつかバグ対策が必要だったので書いておきます。

グループ追加のlambdaを追加すると、add envして切り替えたときにバグる。

team-provider-info.jsonのfunctionの項目にgroupを追加します

{{project name}}\amplify\team-provider-info.json

{
  "devName": {
    "awscloudformation": {
     ...
    },
    "categories": {
      ...
      "function": {
        "appnamePostConfirmation": {
          "GROUP": "everyone"                  // ここ
        }
      }
    }
  },
}

GraphQLスキーマでフィールドレベル認証できるはずだがよくわからない

GraphQLスキーマでフィールドレベル認証できるはずなんだけど、この挙動がさっぱりわからない。動いている気がしない。

まとめ

基本的にパブリックが必要にならない設計がいいと思います。面倒。

そしてパブリックにするならReadオンリーがいいと思います。Writeを許可しだすとデフォルトで何でもできてしまってそれを制御するのは大変だからです。フィールドレベル認証とかまったく動いている気がしない。