Cognito認証されているAppSyncをIAM認証で後ろから叩く


Cognito認証されているAppSyncをIAM認証で後ろから叩く

はじめに

以前、サーバーレスWebアプリハンズオン記事として「AppSyncをフロントエンドとバックエンドで利用する」を投稿しました。
ここでは認証モードとして当然のように「API Key」を利用していました。
このAPI Keyでの認証、初学者がまず使ってみる場合や簡易的なプロトタイプとして利用する分には良いのですが、安全性や有効期限が最大365日という更新の手間のことを考えると、なるべく他の認証モードを採用すべきです。

その鍵、見えてますよ、、。
365日後に鍵の更新、絶対忘れますよ、、。

AppSyncの認証モードの種類

AppSyncはAPI Key以外に以下の認証モードをサポートしています。

  • AWS Identity and Access Management (IAM)
  • Amazon Cognito ユーザープール
  • OpenID Connect

IAMとは
AWSリソースへアクセスできる範囲やアクセス方法を制御するための仕組みです。

Cognitoユーザープールとは
アプリユーザー管理(サインアップやサインイン状態)を提供する仕組みです。

OpenID Connectとは
使ったことがないため良く知らないです、、。
(Cognitoと同じように、ログインして発行されたトークンを利用して認証する感じ?)

複数の認証モードを組み合わせて使う

AppSyncは2019年5月に複数の認証モードをサポートしました。
https://aws.amazon.com/jp/about-aws/whats-new/2019/05/aws-appsync-now-supports-configuring-multiple-authorization-type/

これにより、フロントエンド(クライアントサイド)はCognitoで、バックエンド(サーバーサイド)はIAMで認証するということができるようになりました。

IAM認証を追加して、Lambda(Python)で叩く

Amplifyを利用することで、Cognito認証を効かせたAppSyncの構築と、それをフロントエンド(Webアプリ)から利用する実装については、サクッと実現できるようになりました。

この記事ではそこからさらに、
バックエンドで利用するための認証プロバイダーとしてIAMを追加し、
Lambda(Python)から実際に叩く(APIをコールする)ところについて書きたいと思います。

追加の認証プロバイダーにIAMを追加する

AWSコンソール > AWS AppSync > 目的のAPIを選択 > 設定
下の方に「追加の認証プロバイダー」という項目があるので、Newボタンを押下。

「追加の認証プロバイダーを設定する」ポップアップ画面の認証モードコンボボックスから、「AWS Identify and Access Management(IAM)」を選択して「送信」ボタンを押下。

設定の「追加の認証プロバイダー」に「AWS Identify and Access Management(IAM)」が追加されているのを確認し、最下にある「保存」ボタンを押下。(※忘れずに!)

スキーマに認証モードを指定する

複数の認証モードを利用する場合、スキーマへの追記が必要です。
どの認証モードで何ができるのか、Query、Mutation、Subscriptionなどのルート型や、その中にユーザー定義した関数フィールドやオブジェクト型ごとに細かく指定することができます。
(※input(入力型)には指定できません。)

デフォルトでは何も指定されておらず、その場合はプライマリ認証モードのみ許可されている状態ということになり、追加した認証モードは許可されていません。
以下のマーキングをすることで認証モードを指定できます。

  • @aws_api_key
    フィールドが API_KEY で承認されることを指定します。
  • @aws_iam
    フィールドが AWS_IAM で承認されることを指定します。
  • @aws_oidc
    フィールドが OPENID_CONNECT で承認されることを指定します。
  • @aws_cognito_user_pools
    フィールドが AMAZON_COGNITO_USER_POOLS で承認されることを指 定します。

詳細はこちらの開発者ガイドをどうぞ。(というかここに全て書いてある)
https://docs.aws.amazon.com/ja_jp/appsync/latest/devguide/appsync-dg%20.pdf

AWSコンソール > AWS AppSync > 目的のAPIを選択 > スキーマ
ここでgraphqlのスキーマを編集した後、「スキーマを保存」ボタンを押下してください。

一部ですが、編集前(Before.graphql)と編集後(After.graphql)のスキーマの例を載せておきます。

Before.graphql
type Mutation {
    createSampleAppsyncTable(input: CreateSampleAppsyncTableInput!): SampleAppsyncTable
    updateSampleAppsyncTable(input: UpdateSampleAppsyncTableInput!): SampleAppsyncTable
    deleteSampleAppsyncTable(input: DeleteSampleAppsyncTableInput!): SampleAppsyncTable
}

type Query {
    getSampleAppsyncTable(group: String!, path: String!): SampleAppsyncTable
    listSampleAppsyncTables(filter: TableSampleAppsyncTableFilterInput, limit: Int, nextToken: String): SampleAppsyncTableConnection
}

type SampleAppsyncTable {
    group: String!
    path: String!
}
After.graphql
type Mutation {
    createSampleAppsyncTable(input: CreateSampleAppsyncTableInput!): SampleAppsyncTable
        @aws_cognito_user_pools @aws_iam
    updateSampleAppsyncTable(input: UpdateSampleAppsyncTableInput!): SampleAppsyncTable
        @aws_iam
    deleteSampleAppsyncTable(input: DeleteSampleAppsyncTableInput!): SampleAppsyncTable
}

type Query @aws_cognito_user_pools {
    getSampleAppsyncTable(group: String!, path: String!): SampleAppsyncTable
    listSampleAppsyncTables(filter: TableSampleAppsyncTableFilterInput, limit: Int, nextToken: String): SampleAppsyncTableConnection
}

type SampleAppsyncTable @aws_cognito_user_pools @aws_iam {
    group: String!
    path: String!
}

この例では、以下のように指定しています。
Mutation.create : CognitoとIAM
Mutation.update : IAMのみ
Mutation.delete : Cognitoのみ(指定なし=プライマリ)
Query : getもlistもCognitoのみ

ここまで書いて思いましたが、プライマリ認証はCognitoではなくIAMにして、クライアント側で必要なものだけCognito許可を与えるような設計が良さそうですね。

Lambda(Python)で叩く

IAM認証許可を与えたAppSyncを、LambdaのPythonで叩くための実装です。

sample_graphql_with_iam.py
import requests
from requests_aws4auth import AWS4Auth

AWS_REGION = os.environ["AWS_REGION"]
AWS_ACCESS_KEY_ID = os.environ["AWS_ACCESS_KEY_ID"]
AWS_SECRET_ACCESS_KEY = os.environ["AWS_SECRET_ACCESS_KEY"]
AWS_SESSION_TOKEN = os.environ["AWS_SESSION_TOKEN"]
ENDPOINT = "https://{0}.{1}.{2}.amazonaws.com/{3}".format("xxxxxxxxxxxxxxxxxxxxxx", "appsync-api", AWS_REGION, "graphql")
AUTH = AWS4Auth(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, 'appsync', session_token=AWS_SESSION_TOKEN)

def apiCreateTable(group, path):
    try:        
        body_json = {"query": 
            """
            mutation create {{
                createSampleAppsyncTable(input:{{
                group: \"{0}\"
                path: \"{1}\"
              }}){{
                group path
              }}
            }}
            """.format(group, path)
        }
        body = json.dumps(body_json)
        response = requests.request("POST", ENDPOINT, auth=AUTH, data=body, headers={})
    except Exception as e:
        logger.exception(e)
        raise e

IAMロールへポリシーの追加

実行するLambdaのIAMロールへ、AppSyncのポリシーを加えます。
AWSAppSyncAdministratorをアタッチしてあげればOKです。
(※ 本当はAppSync全体ではなく目的のAPIのみ許可するようにインラインポリシーを書いてあげたほうがいいのだと思いますが、書き方が分かりません。誰か教えて下さい!)

あとがき

前から叩いて良し、後ろから叩いて良し。
後ろから叩かれるのを前で待たせておくとイケてる感じになります。(※Subscriptionの話)
前も後ろもCognitoじゃ退屈っていうんで、後ろはIAMにしてるんですよ。(※認証の話)

っていう話をJAWS UG浜松のある方(Amplifyおじさん)とした結果、この記事を書くに思い至りました。

昨年末のLTではこのようなイラストで表現しましたね。

(後ろから叩かれるのを前で監視して待ってるイメージ)

様々な用途に活用できそうで開発のし甲斐がある、そんなAppSyncを今後ますます攻略してゆきたいですね!