AWS AppSyncのリゾルバで、AWS CognitoのグループやHTTPリクエストヘッダを使った認可処理を書いてみた

前回、Schemaで @aws_auth@ aws_cognito_user_pools などを使って認可処理を書いてみました。
AWS AppSyncのSchemaで、認証・認可系ディレクティブの @aws_auth や @aws_cognito_user_pools などを試してみた - メモ的な思考的な

ただ、複数認証時に @aws_auth が使えなかったため、Schemaだけでは複数認証時にAWS Cognitoのグループによる認可処理ができなさそうでした。

 
他の方法を探してみたところ、Schemaに紐づくリゾルバ内で $context の値を参照することで、AWS CognitoのグループやHTTPリクエストヘッダを使った認可処理ができそうでしたので、メモを残します。

 
目次

 

環境

  • AppSync
  • aws-amplify 1.1.30
  • @aws-amplify/cli 1.8.1

 

ゾルバの $context について

ゾルバとは、Schema (QueryやMutation、Subscription) とデータソースを紐付ける関数のようなものです。

ゾルバ内で参照できる変数 $context とは

$context 変数は、リゾルバー呼び出しのすべてのコンテキスト情報が保持されるマップです。この変数の構造は次のとおりです。

{
   "arguments" : { ... },
   "source" : { ... },
   "result" : { ... },
   "identity" : { ... },
   "request" : { ... }
}

です。
リゾルバーのマッピングテンプレートのコンテキストリファレンス - AWS AppSync

 
最初に、 $context にはどんな値が設定されているかを確認してみます。

スキーマとして、以下を用意します。また、データソースは None

type Context {
    source: String
    identity: String
    claims: String
    sourceIp: String
    request: String
    headers: String
}

type Query {
    getContext: Context
}

 
次にデータソースを指定します。今回は $context の値を表示するだけなので、データソースには None を指定します。
None データソースのリゾルバーマッピングテンプレートリファレンス - AWS AppSync

 
最後にリゾルバを作成します。リクエストテンプレートでは、中身を見たい項目を指定しておきます。

{
    "version": "2017-02-28",
    "payload": {
        "source": $util.toJson($context.source),
        "identity": $util.toJson($context.identity),
        "claims": $context.identity.claims,
        "sourceIp": $util.toJson($context.identity.sourceIp),
        "request": $util.toJson($context.request),
        "headers": $util.toJson($context.request.headers)
   }
}

 
レスポンステンプレートでは、リクエストテンプレートの中身をそのまま表示します。

$utils.toJson($context.result)

 
あとはAmplify frameworkを使って

Amplify.configure(awsconfig);

// サインイン
Auth.signIn(username, '11111111')

// AppSync APIを呼ぶ
await API.graphql(
  {
    query: queries.getContext,
    authMode: 'AMAZON_COGNITO_USER_POOLS'
  }
).then((event) => {
  console.log('ok');
  console.log(event.data.getContext);
}).catch((e) => {
  console.log('error!');
  console.log(e);
});

として、レスポンスをログ出力してみます。

 
結果は以下のような感じでした。

# claims:
"{sub=xxx,
  cognito:groups=[member],  <= groupが設定されている場合のみ存在するキー
  event_id=xxx,
  token_use=access,
  scope=aws.cognito.signin.user.admin,
  auth_time=nnn,
  iss=https://cognito-idp.<region>.amazonaws.com/<region>_xxx,
  exp=xxx,
  iat=xxx,
  jti=xxx,
  client_id=xxx,
  username=foo}"

# headers:
"{x-forwarded-for=xxx.xxx.xxx.xxx, yyy.yyy.yyy.yyy,
  cloudfront-is-tablet-viewer=false,
  cloudfront-viewer-country=JP,
  pragma=no-cache,
  via=2.0 xxx.cloudfront.net (CloudFront),
  cloudfront-forwarded-proto=https,
  origin=http://localhost:8080,
  content-length=149,
  cache-control=no-cache,
  host=<host>.appsync-api.<region>.amazonaws.com,
  x-forwarded-proto=https,
  accept-language=ja,en-US;q=0.9,en;q=0.8,
  user-agent=Mozilla/5.0 ...,
  cloudfront-is-mobile-viewer=false,
  accept=application/json, text/plain, */*,
  cloudfront-is-smarttv-viewer=false,
  accept-encoding=gzip, deflate, br,
  referer=http://localhost:8080/,
  content-type=application/json; charset=UTF-8,
  x-amz-cf-id=xxx,
  x-amzn-trace-id=Root=xxx,
  authorization=xxx,
  cloudfront-is-desktop-viewer=true,
  x-forwarded-port=443}"

# identity:
"{sub=xxx,
  issuer=https://cognito-idp.<region>.amazonaws.com/<region>_xxx,
  username=foo,
  claims={...},
  sourceIp=[...],
  defaultAuthStrategy=ALLOW,
  groups=null}"

# request:
  実質headersしか入っていなかったので略

 
これより、

  • cognito:groups にて、AWS Cognitoのグループ
  • request.headers にて、HTTPリクエストヘッダ

などを使った認可処理が書けそうでした。

 

AWS Cognitoのグループを使った認可制御

AWS Cognitoで認証されたユーザーかつユーザーがグループに所属している場合、 $context.identity.claims にキー cognito:groups が追加されます。

これを使い、リクエストテンプレートの先頭にて、VTL(Apache Velocity Template Language)を使って実装します。VTLの書き方はAppSyncのドキュメントの他、Javaまわりでも確認できます。
リゾルバーのマッピングテンプレートプログラミングガイド - AWS AppSync

なお、認可できない時は、 ユーティリティヘルパーの $utils.unauthorized() を使ってエラーを返します。
リゾルバーのマッピングテンプレートのユーティリティリファレンス - AWS AppSync

全体はこんな感じです。

## Cognitoのユーザープールのユーザーのうち、Groupが "admin" の場合のみ、データを取得可能にする
#set( $groups = $context.identity.claims.get("cognito:groups") )

## ユーザーにグループが設定されていない場合は、キー自体が存在しないことに注意
#if( $util.isNull($groups) )
    $util.unauthorized()
#end

#foreach( $group in $groups )
    #if( $group != "admin" )
        $util.unauthorized()
    #end
#end

## 以降は、前述通りなので略
{
    "version": "2017-02-28",
    ...
}

参考:AppSync + Cognitoによる認可制御 | Takumon Blog

 

HTTPリクエストヘッダを使った認可制御

こちらも同様に、リクエストテンプレートに記載します。

HTTPリクエストヘッダを見るとCloudFrontのヘッダーが追加されていたため*1、今回はそれを用いるようにしました。
参考:https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-device

## また、モバイルの時は、データ取得不可
## CloudFrontのヘッダが取得できているので、それを使用(ふつうのUserAgentも取得できる)
#if( $context.request.headers.get("cloudfront-is-mobile-viewer") == "true")
    $util.unauthorized()
#end

 

schema.graphql でのmulti-auth directivesは、今のところ未対応 (PR #1524)

前回の記事の通り、AWS Amplify Consoleでは @aws_cognito_user_pools などが使えたため、Amplifyでも使えるのかと思いましたが、現在は対応中のようです。
[WIP] Add support for new multi-auth AppSync directives by attilah · Pull Request #1524 · aws-amplify/amplify-cli

そのため、今のところは、AppSync Consoleにて手書きで @aws_cognito_user_pools などを付けるしかなさそうです。

 

ソースコード

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

 

環境構築ログ

今回試した時の環境構築ログです。

初期化
$ amplify init

Note: It is recommended to run this command from the root of your app directory
? Enter a name for the project resolver_auth
? 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を追加

Cognitoは新規作成します。

$ amplify add auth
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 only (Best used with a cloud API only)
 Please provide a friendly name for your resource that will be used to label this category in the project: ResolverAuthResource
 Please provide a name for your user pool: ResolverAuthUserPool
 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? Yes
 Enter the minimum password length for this User Pool: 8
 Select the password character requirements for your userpool: (Press <space> to select, <a> to toggle all, <i> to invert selection)
 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

 
pushして、Cognitoを生成します。

$ amplify push

Current Environment: dev

| Category | Resource name        | Operation | Provider plugin   |
| -------- | -------------------- | --------- | ----------------- |
| Auth     | ResolverAuthResource | Create    | awscloudformation |
? Are you sure you want to continue? Yes

 
Cognito作成後、ユーザーとグループを作成します。

 

APIを追加
$ amplify add api

? Please select from one of the below mentioned services GraphQL
? Provide API name: ResolverAuthAPI
? Choose an authorization type for the API Amazon Cognito User Pool
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: (Use arrow keys)
❯ Single object with fields (e.g., “Todo” with ID, name, description) 
  One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”) 
ShinanoDolce:resolver_auth kamijoshinya$ amplify add api
? Please select from one of the below mentioned services GraphQL
? Provide API name: ResolverAuthAPI
? Choose an authorization type for the API Amazon Cognito User Pool
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? No
? Provide a custom type name MyType

 
ただ、自動生成されたスキーマだと @model ディレクティブが付与されているため、DynamoDBを作ってしまいます。

そのため、resolver_auth/amplify/backend/api/ResolverAuthAPI/schema.graphql を修正し、データソース None に対応できるようにします。

type Context {
    source: String
    identity: String
    claims: String
    sourceIp: String
    request: String
    headers: String
}

type Query {
    getContext: Context
}

 
また、APIで必要な

  • データソースやリゾル
    • resolver_auth/amplify/backend/api/ResolverAuthAPI/stacks/CustomResources.json
  • ゾルバのテンプレート
    • resolver_auth/amplify/backend/api/ResolverAuthAPI/resolvers/Query.getContext.req.vtl
    • resolver_auth/amplify/backend/api/ResolverAuthAPI/stacks/Query.getContext.res.vtl

なども作成しておきます。

 
一通り終わったら、pushしてAPIを作成します。

その際、 ? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes としておくと、schema.graphql の内容に基づいて、 resolver_auth/src/graphql/queries.js が生成されます。

今回はQueryだけ用意しましたので、 queries.js のみが生成されます。

$ amplify push

Current Environment: dev

| Category | Resource name        | Operation | Provider plugin   |
| -------- | -------------------- | --------- | ----------------- |
| Api      | ResolverAuthAPI      | Create    | awscloudformation |
| Auth     | ResolverAuthResource | No Change | awscloudformation |
? Are you sure you want to continue? Yes

GraphQL schema compiled successfully.
Edit your schema at path/to/resolver_auth/amplify/backend/api/ResolverAuthAPI/schema.graphql or place .graphql files in a directory at path/to/resolver_auth/amplify/backend/api/ResolverAuthAPI/schema
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes

 

アプリの作成と起動
# 作成して実装
$ touch package.json index.html webpack.config.js src/app.js

# 必要なものをインストール
$ npm install

# amplifyもインストール
$ npm install --save aws-amplify

# 起動して動作確認
$ npm start

*1:AppSyncの前段にCloudFrontがいるのかなと思いましたが、それらしき資料は見つけられず...