AWS AppSync
AWS AppSync 開発者ガイド

認証のユースケース

セキュリティ」セクションでは、API を保護するためのさまざまな認証モードについて説明し、きめ細かな認証メカニズムの概念と流れについて紹介します。AWS AppSync では、GraphQL リゾルバーのマッピングテンプレートを使用して、ユーザーがデータに対して完全なオペレーションを実行できるため、ユーザー ID、条件、およびデータインジェクションを組み合わせて使用して非常に柔軟な方法で、データの読み取り時または書き込み時にデータを保護できます。

AWS AppSync のリゾルバーの編集に慣れていない場合は、プログラミングガイドを参照してください。

概要

システム内のデータへのアクセス権限の付与は、行 (リソース) と列 (ユーザーまたはロール) の交点が付与されるアクセス権限であるアクセスコントロールマトリックスを通じて行われています。

AWS AppSync では、お客様自身のアカウント内のリソースを使用し、ID (ユーザーまたはロール) 情報を GraphQL のリクエストおよびレスポンスにコンテキストオブジェクトとして渡すため、それをリゾルバーで使用できます。つまり、リゾルバーのロジックに基づいて、書き込みまたは読み取りのオペレーションに対して適切にアクセス権限を付与できます。たとえば、そのロジックがリソースレベルである場合は、特定の名前のユーザーまたはグループのみが、「認証メタデータ」が保存される必要がある特定のデータベース行に対して読み取りおよび書き込みできます。AWS AppSync ではデータは一切保存されないため、ユーザーは、アクセス権限を計算できるようにリソースを使用してその認証メタデータを保存する必要があります。認証メタデータは通常、DynamoDB テーブル内の属性 (列) であり、所有者、ユーザーまたはグループのリストなどです。たとえば、ReadersWriters 属性があります。

ハイレベルでは、データソースから個々の項目を読み取っている場合、リゾルバーがデータソースから読み取った後に、レスポンステンプレートで条件ステートメント #if () ... #end を実行します。そのチェックでは通常、読み取りオペレーションから返された認証メタデータに対するメンバーシップ確認のために、$context.identity の user または group の値が使用されます。複数のレコードがある (テーブルの ScanQuery で返されるリストなど) 場合は、同様の user または group の値を使用して、データソースに対するオペレーションの一部として条件チェックを送信します。

同様に、データを書き込む場合は、アクション (ミューテーションを作成するユーザーやグループにアクセス権限があるかどうかを確認する PutItemUpdateItem など) に対して条件ステートメントを適用します。この条件チェックでは、$context.identity 内の値を使用して何度も行われ、リソースの認証メタデータと比較されます。リクエストテンプレートとレスポンステンプレートの両方で、クライアントからのカスタムヘッダーを使用して検証チェックを実行することもできます。

データの読み取り

前述のように、チェックを実行するための認証メタデータは、リソースに保存されているかまたは GraphQL リクエスト (ID、ヘッダーなど) で渡される必要があります。その例を示すために、次の DynamoDB テーブルがあるとします。

プライマリキーは id であり、アクセスするデータは Data です。その他の列は、認証のために実行できるチェックの例です。「リゾルバーの DynamoDB 用のマッピングテンプレートリファレンス」で説明しているように、OwnerString であり、PeopleCanAccessGroupsCanAccessString Sets です。

リゾルバーのマッピングテンプレートの概要」の図に示しているように、レスポンステンプレートには、コンテキストオブジェクトだけでなくデータソースからの結果も含まれています。個々の項目に対する GraphQL クエリでは、レスポンステンプレートを使用して、そのユーザーが結果を確認することを許可されているかどうかをチェックし、そうでない場合は認証エラーメッセージを返すことができます。これは、「認証フィルタ」と呼ばれることもあります。Scan または Query を使用してリストを返す GraphQL クエリでは、リクエストテンプレートでチェックを実行し、認証条件が満たされている場合にのみデータを返すのが、より効率的です。次のように実装します。

  1. GetItem - 個々のレコードに対する認証チェック。#if() ... #end ステートメントを使用して行われます。

  2. Scan/Query オペレーション - 認証チェックは "filter":{"expression":...} ステートメントです。よく使用されるチェックは、等価チェック (attribute = :input)、または値がリストあるかどうかのチェック (contains(attribute, :input)) です。

上記の 2 で両方のステートメントにある attribute は、テーブル内のレコードの列名 (上記の例では Owner など) を表しています。そのエイリアスとして # 記号と "expressionNames":{...} を使用できますが、必須ではありません。:input は、データベース属性と比較する値への参照であり、"expressionValues":{...} で定義します。その例を以下に示します。

ユースケース: 所有者が読み取り可能

上記のテーブルを使用して、個々の読み取りオペレーション (Owner == Nadia) で GetItem であるときにのみデータを返す場合、テンプレートは次のようになります。

#if($context.result["Owner"] == $context.identity.username) $utils.toJson($context.result); #else $utils.unauthorized() #end

ここで、以降のセクションで再利用する点について説明しておきます。まず、チェックで使用される $context.identity.username は、Amazon Cognito ユーザープールが使用されている場合は分かりやすいユーザーサインアップ名であり、AWS IAM (Amazon Cognito フェデレーテッドアイデンティティも含む) が使用されている場合はユーザー ID です。所有者に対して保存されるその他の値として、複数のロケーションからフェデレーテッドログインする場合に便利な一意の「Amazon Cognito ID」値などがあり、「リゾルバーのマッピングテンプレートのコンテキストリファレンス」で、利用可能なオプションを確認しておく必要があります。

次に、$util.unauthorized() に対応する条件付き else チェックは、完全に省略可能ですが、GraphQL API を設計する際のベストプラクティスとして、指定することお勧めします。

ユースケース: 特定のアクセス権のハードコード

// This checks if the user is part of the Admin group and makes the call #foreach($group in $context.identity.claims.get("cognito:groups")) #if($group == "Admin") #set($inCognitoGroup = true) #end #end #if($inCognitoGroup) { "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : { "S" : "${context.argument.id}" } }, "attributeValues" : { "owner" : {"S" : "${context.identity.username}"} #foreach( $entry in $context.arguments.entrySet() ) ,"${entry.key}" : { "S" : "${entry.value}" } #end } } #else $utils.unauthorized() #end

ユースケース: 結果リストのフィルタリング

前の例では単一の項目が返されるため、$context.result に対してチェックを直接実行することもできましたが、スキャンなどの一部のオペレーションでは $context.result.items で複数の項目が返されるため、認証フィルタを実行して、そのユーザーが確認を許可されている結果のみを返す必要があります。たとえば、今度はレコードに設定されている Amazon Cognito IdentityID が Owner フィールドにあるとすると、次のレスポンスマッピングテンプレートを使用して、そのユーザーが所有しているレコードのみを示すようにフィルタリングできます。

#set($myResults = []) #foreach($item in $context.result.items) ##For userpools use $context.identity.username instead #if($item.Owner == $context.identity.cognitoIdentityId) #set($added = $myResults.add($item)) #end #end $utils.toJson($myResults)

ユースケース: 複数のユーザーが読み取り可能

もう 1 つのよくある認証オプションは、ユーザーのグループがデータを読み取ることができるように許可することです。次の例の "filter":{"expression":...} では、GraphQL クエリを実行しているユーザーが PeopleCanAccess のセットにリストされている場合にのみ、テーブルスキャンからの値が返されます。

{ "version" : "2017-02-28", "operation" : "Scan", "limit": #if(${context.arguments.count}) "${context.arguments.count}" #else 20 #end, "nextToken": #if(${context.arguments.nextToken}) "${context.arguments.nextToken}" #else null #end, "filter":{ "expression": "contains(#peopleCanAccess, :value)", "expressionNames": { "#peopleCanAccess": "peopleCanAccess" }, "expressionValues": { ":value": { "S" : "${context.identity.username}" } } } }

ユースケース: グループが読み取り可能

直前のユースケースと同様に、1 つまたは複数のグループに属するユーザーのみが、データベース内の特定の項目を読み取る権限を持っているとします。"expression": "contains()" オペレーションを使用するのは同じですが、設定されているメンバーシップに属している必要があるすべてのグループの論理 OR です。この例では、ユーザーが属している各グループに対して次の $expression ステートメントを作成し、それをフィルタに渡します。

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "Scan", "limit": #if(${context.arguments.count}) "${context.arguments.count}" #else 20 #end, "nextToken": #if(${context.arguments.nextToken}) "${context.arguments.nextToken}" #else null #end, "filter":{ "expression": "$expression", "expressionValues": $utils.toJson($expressionValues) } }

データの書き込み

ミューテーションでのデータの書き込みは、常にリクエストマッピングテンプレートで制御されます。DynamoDB のデータソースの場合、key は、そのテーブル内の認証メタデータに対して検証を実行する適切な "condition":{"expression"...}" が使用されます。「セキュリティ」には、テーブル内の Author フィールドのチェックに役立つ例があります。このセクションでは、その他のユースケースを示します。

ユースケース: 複数の所有者

以前の例のテーブル図を使用した、PeopleCanAccess リストがあるとします。

{ "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : { "S" : "${context.arguments.id}" } }, "update" : { "expression" : "SET meta = :meta", "expressionValues": { ":meta" : { "S": "${context.arguments.meta}" } } }, "condition" : { "expression" : "contains(Owner,:expectedOwner)", "expressionValues" : { ":expectedOwner" : { "S" : "${context.identity.username}" } } } }

ユースケース: グループが新規レコードを作成可能

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "PutItem", "key" : { ## If your table's hash key is not named 'id', update it here. ** "id" : { "S" : "$context.arguments.id" } ## If your table has a sort key, add it as an item here. ** }, "attributeValues" : { ## Add an item for each field you would like to store to Amazon DynamoDB. ** "title" : { "S" : "${context.arguments.title}" }, "content": { "S" : "${context.arguments.content}" }, "owner": {"S": "${context.identity.username}" } }, "condition" : { "expression": "attribute_not_exists(id) AND $expression", "expressionValues": $utils.toJson($expressionValues) } }

ユースケース: グループが既存レコードを更新可能

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : { "S" : "${context.arguments.id}" } }, "update":{ "expression" : "SET title = :title, content = :content", "expressionValues": { ":title" : { "S": "${context.arguments.title}" }, ":content" : { "S": "${context.arguments.content}" } } }, "condition" : { "expression": "$expression", "expressionValues": $utils.toJson($expressionValues) } }

パブリックレコードとプライベートレコード

条件フィルターを使用すると、データをプライベート、パブリック、またはその他のブール型チェックとしてマークすることもできます。それを認証フィルタの一部としてレスポンステンプレート内に組み込むことができます。このチェックを使用すると、グループメンバーシップを制御することなく、データを一時的に隠したり、ビューから除外したりできます。

たとえば、DynamoDB テーブル内の各項目に、yes または no のいずれかの値を持つ public という属性を追加するとします。次のレスポンステンプレートを GetItem 呼び出しで使用すると、アクセス権があるグループにユーザーが属していて、かつ、そのデータがパブリックとマークされている場合にのみ、データを表示できます。

#set($permissions = $context.result.GroupsCanAccess) #set($claimPermissions = $context.identity.claims.get("cognito:groups")) #foreach($per in $permissions) #foreach($cgroups in $claimPermissions) #if($cgroups == $per) #set($hasPermission = true) #end #end #end #if($hasPermission && $context.result.public == 'yes') $utils.toJson($context.result) #else $utils.unauthorized() #end

また、上記のコードで論理 OR (||) を使用すると、ユーザーにレコードへのアクセス許可があるか、または、レコードがパブリックである場合に、そのユーザーに読み取りを許可できます。

#if($hasPermission || $context.result.public == 'yes') $utils.toJson($context.result) #else $utils.unauthorized() #end

通常、認証チェックを実行する際に、標準的な演算子 ==!=&&、および || が役立ちます。

リアルタイムデータ

クライアントがサブスクリプションを作成したときに、このドキュメントで前述したのと同じ手法で、きめ細かなアクセス制御コントロールを GraphQL のサブスクリプションに適用できます。サブスクリプションフィールドにリゾルバーをアタッチすると、そのポイントで、データソースからデータをクエリし、リクエストまたはレスポンスのいずれかのマッピングテンプレートで条件ロジックを実行できます。そのデータ構造が、GraphQL サブスクリプションで返される型と一致している限り、追加のデータ (サブスクリプションからの初期結果など) をクライアントに返すこともできます。

ユースケース: ユーザーが特定の対話のみのサブスクライブ可能

GraphQL サブスクリプションを使用したリアルタイムデータのよくあるユースケースは、メッセージングやプライベートチャットのアプリケーションを構築することです。複数のユーザーに対応したチャットアプリケーションを作成する場合、2 人または複数ユーザーの間で対話が行われます。ユーザーは、プライベートまたはパブリックの「ルーム」にグループ化されます。したがって、ユーザーにアクセス権が付与されている対話 (1 対 1 またはグループ間の) をサブスクライブする 1 人のユーザーのみを認証します。デモの目的で、以下の単純な例では、1 人のユーザーが別のユーザーにプライベートのメッセージを送信するユースケースを示します。次の 2 つの Amazon DynamoDB テーブルをセットアップします。

  • Messages テーブル: (プライマリキー) toUser、(ソートキー) id

  • Permissions テーブル: (プライマリキー) username

Messages テーブルには、GraphQL ミューテーション経由で送信される実際のメッセージが保存されます。Permissions テーブルは、クライアントの接続時に認証のために GraphQL サブスクリプションによってチェックされます。以下の例では、次の GraphQL スキーマを使用していることを前提としています。

input CreateUserPermissionsInput { user: String! isAuthorizedForSubscriptions: Boolean } type Message { id: ID toUser: String fromUser: String content: String } type MessageConnection { items: [Message] nextToken: String } type Mutation { sendMessage(toUser: String!, content: String!): Message createUserPermissions(input: CreateUserPermissionsInput!): UserPermissions updateUserPermissions(input: UpdateUserPermissionInput!): UserPermissions } type Query { getMyMessages(first: Int, after: String): MessageConnection getUserPermissions(user: String!): UserPermissions } type Subscription { newMessage(toUser: String!): Message @aws_subscribe(mutations: ["sendMessage"]) } input UpdateUserPermissionInput { user: String! isAuthorizedForSubscriptions: Boolean } type UserPermissions { user: String isAuthorizedForSubscriptions: Boolean } schema { query: Query mutation: Mutation subscription: Subscription }

以下では、サブスクリプションリゾルバーを示すために、一部の標準オペレーション (createUserPermissions() など) は取り上げていませんが、DynamoDB リゾルバーで標準実装されています。代わりに、リゾルバーでのサブスクリプションの認証フローを中心に説明します。ユーザー間でメッセージを送信するには、sendMessage() フィールドにリゾルバーをアタッチし、次のリクエストテンプレートを使用して Messages テーブルデータソースを選択します。

{ "version" : "2017-02-28", "operation" : "PutItem", "key" : { "toUser" : { "S" : "${ctx.args.toUser}" }, "id" : { "S" : "${util.autoId()}" } }, "attributeValues" : { "fromUser" : { "S" : "${context.identity.username}" }, "content" : { "S" : "${ctx.args.content}" }, } }

この例では、$context.identity.username を使用します。これは、AWS Identity and Access Management ユーザーまたは Amazon Cognito ユーザーのユーザー情報を返します。レスポンステンプレートは、$util.toJson($ctx.result) の単純なパススルーです。保存してスキーマページに戻ります。次に、Permissions テーブルをデータソースとして使用し、次のリクエストマッピングテンプレートを使用して、newMessage() サブスクリプションにリゾルバーをアタッチします。

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

次のレスポンスマッピングテンプレートを使用し、Permissions テーブルからのデータを使用して認証チェックを実行します。

#if(${context.identity.username} != ${context.arguments.toUser}) $utils.unauthorized() #elseif(! ${context.result.isAuthorizedForSubscriptions}) $utils.unauthorized() #else ##User is authorized, but we return null to continue null #end

この例の場合、2 つの認証チェックを実行しています。最初のチェックでは、そのユーザーが別のユーザーに対するメッセージをサブスクライブしていないことを確認しています。2 番目のチェックでは、BOOL として保存されている isAuthorizedForSubscriptions の DynamoDB 属性をチェックすることで、そのユーザーが任意のフィールドへのサブスクライブを許可されていることを確認しています。

テストするには、Amazon Cognito ユーザープールと「Nadia」というユーザー名を使用して AWS AppSync コンソールにサインインし、次の GraphQL サブスクリプションを実行します。

subscription AuthorizedSubscription { newMessage(toUser: "Nadia") { id toUser fromUser content } }

Permissions テーブルに、Nadiausername キー属性に対して isAuthorizedForSubscriptionstrue に設定されているレコードがある場合は、正常なレスポンスが表示されます。上記の username クエリで別の newMessage() を試行すると、エラーが返されます。