🐙

Urqlから認証つきAWS AmplifyのGraphQL APIにアクセスする

2021/12/06に公開

はじめに

Urql という GraphQL クライアントから認証付きの Amplify の GraphQL API にアクセスする方法を調査したので共有します。
間違いや改善点などあれば指摘していたけると幸いです。

Urql も AWS Amplify も有名なので既に様々な記事で紹介されていますが、簡単に概要だけします。

AWS Amplify とは

フルスタックアプリ開発に必要なバックエンドを簡単に構築することができる AWS のサービスです。これによって、フロントエンド部分だけに注力することができます。

AWS Amplify は API の種類として REST と GraphQL を提供しており GraphQL を使用する場合は、AWS AppSync というフルマネージドな GraphQL API サーバを立てることになります。

Urql とは

既にいくつかの記事で紹介されていますが簡単に言うと拡張性と高機能性を備えた GraphQL クライアントです。
Exchange と呼ばれるアドオンを用いてネットワーク、キャッシュ、認証など様々な箇所のカスタマイズが可能ですが、基本的な機能は標準の Exchange として提供されています。

公式ドキュメントで Relay や Apollo との比較が記載されています。

Amplify の GraphQL API を使用する上での課題

Amplify の GraphQL を呼び出す方法は下記の 2 つが主流なようです。

いずれの場合も認証機構が組み込まれた状態で使い始めることができるので便利なのですがいくつか不便な点があります。

Amplify の codegen で生成されたクライアントコードを使う場合は、React Hooks に対応したコードは生成されないため自前実装が必要です。[2]
また、キャッシュ機構もないためこれも自前で実装が必要となります。

AppSync 連携用の Apollo Link を使う場合は、graphql-code-generator を使うことで React Hooks なコードが自動生成することはできますが、Apollo のキャッシュ機構は Normalized Cache のみとなっており、Mutation 後の対象となるキャッシュを手動で書き換える必要がある場合があります。[3]

このキャッシュの書き換えには、クエリを再フェッチする方法 (refetchQueries) や キャッシュを直接変更する方法 (writeQuery, writeFragment) などの方法が取られます。(公式サイト)
前者の場合は再度サーバーにアクセスするため効率は悪いですがシンプルです。一方後者はサーバーには再アクセスしないものの扱いが複雑になる傾向があります。
前者は後者に比べるとシンプルですが再フェッチするクエリを指定する必要があります。クエリで変数が必要な場合はそれも指定する必要があります。
どちらの方法もキャッシュをある程度細かく管理したい場合には便利なのですが、 私が扱っているプロジェクトではとりあえず mutation したら関連するキャッシュをなりふり構わずクリアしてもいいぐらいのシンプルさで十分でした。

Urql は上記の課題に対して下記の様な特徴を持っています。

  • graphql-code-generator を使って React Hooks に対応したコードを自動生成する。[4]
  • Urql には Document Caching なるものがあり、Mutation されたタイプに関連するキャッシュを全て消して再度 API にアクセスすることでキャッシュを再構成するということを自動で行ってくれる。

以上から、Urql を使って Amplify の GraphQL API にアクセスしたい、となったわけですが、
GraphQL API に認証がかかっている場合、どのように Urql のクライアントを設定すればよいか、
調べても見当たらなかったため下記ではその手順を説明していきます。

Urql から Amplify の 認証つき GraphQL API にアクセス

ここから本題に入ります。
今回は User Pool による認証を用いているケースを考えます。

アクセストークンの取得

User Pool の場合、認証済みの場合 JWT トークンは下記の様に取得することが出来ます。
ここで注意が必要なのはセッション情報を取得するcurrentSessionは Promise を返すということです。
ですので getJwtToken という関数は async な関数となっています。

import { Auth } from "aws-amplify";

const getJwtToken = async () => {
  const session = await Auth.currentSession(); //現在のセッション情報を取得
  return session.getIdToken().getJwtToken();
};

リクエストヘッダでの JWT トークンの指定方法

次にこの JWT トークンをリクエストにどのように指定するかです。
ここで先程登場したAppsync 連携用の Apollo Linkがどのように認証をしているのかをソースコードを見て確認して見ました。

User Pool に関連する箇所を見てみると Authorization ヘッダーに JWT トークンを渡していることがわかります。ですので認証に必要な情報はリクエストヘッダーで Authorization: <JWTトークン> のように指定すれば良いことがわかりました。

case AUTH_TYPE.AMAZON_COGNITO_USER_POOLS:
case AUTH_TYPE.OPENID_CONNECT:
    const { jwtToken = '' } = auth;
    promise = headerBasedAuth({ header: 'Authorization', value: jwtToken }, operation, forward);
    break;

Amplify の REST API に関するドキュメントには User Pool であれば Bearer を付けても良いと書かれているので、GraphQL API の場合も Bearer ありでも問題ないのかもしれません。

Urql での認証ロジック実装

認証のロジックを組み込むために authExchange という Exchange を使います。
実は createClient のオプションに fetchOptions なるものがありそこでヘッダーを指定することができるのですが、async にヘッダーを生成することが出来ません。同期的にヘッダーが生成できる場合は fetchOptions だけでも対応ができそうです。Urql のレポジトリでもこの件はイシュー化されていますが、authExchange が実装されたことによりクローズされています。
少なくとも User Pool による認証では currentSession が async な関数であることから authExchange を使って実装してきます。
その前に、authExchange は@urql/coreurqlとは別パッケージ (@urql/exchange-auth)なためインストールする必要があります。

React を用いたサンプルコードを GitHub に置いていますので興味がある方は見てみてください。

authExchange のオプション

authExchange は下記のオプションを取ります。

  • addAuthToOperation
    認証状態をリクエストに適用する
  • getAuth
    認証情報を取得する
  • didAuthError
    レスポンスを元に何を認証エラーとするかを定義する。認証エラーになった場合 getAuth が再度実行される。
  • willAuthError
    リクエストが送信される前に認証エラーとなるかどうかを判断するもの。
    サーバーにリクエストを送らなくても認証エラーとなるのが明らかな場合に無駄なリクエストを排除できる。

getAuth の実装

認証状態 (authState)によらず JWT トークンを取得するようにします。
トークンリフレッシュを自前実装する場合は初回アクセス時に何らかのストアから JWT トークンを取得し、それ以降はリフレッシュトークンを元に再度 JWT トークンを取得するみたいな条件分岐が必要ですが Amplify の場合は内部的にリフレッシュを実施してくれているので不要なはずです。

const getAuth: AuthConfig<AuthToken>["getAuth"] = async () => {
  try {
    const session = await Auth.currentSession();
    const token = session.getIdToken().getJwtToken();
    return {
      token
    };
  } catch {
    return null;
  }
};

addAuthToOperation の実装

getAuth で取得した認証情報を Authorization ヘッダーに組み込んだリクエストを生成します。

const addAuthToOperation: AuthConfig<AuthToken>["addAuthToOperation"] = ({
  authState,
  operation
}) => {
  if (!authState || !authState.token) {
    return operation;
  }

  const fetchOptions =
    typeof operation.context.fetchOptions === "function"
      ? operation.context.fetchOptions()
      : operation.context.fetchOptions || {};

  return makeOperation(operation.kind, operation, {
    ...operation.context,
    fetchOptions: {
      ...fetchOptions,
      headers: {
        ...fetchOptions.headers,
        Authorization: authState.token
      }
    }
  });
};

didAuthError の実装

AppSync は認証エラーの場合、ステータスコードが 401 を返すので下記のように書きます。
こうすることでトークンの有効期限が切れるなどでアクセスができなくなったときに getAuth が再度呼ばれトークンの更新が行われます。
amplify がトークンリフレッシュなどを担ってくれているのなら getAuth でなく addAuthToOperation で毎回トークンを取得するのも有りだと思ったのですが、残念ながら addAuthToOperation 内では非同期処理ができないようです。

const didAuthError: AuthConfig<AuthToken>["didAuthError"] = ({ error }) => {
  return error.response.status === 401;
};

クライアントの生成

あとは上で実装したgetAuthaddAuthToOperationを authExchange のオプションとして指定した上で、クライアントを生成すれば良いです。

export const client = createClient({
  url: `${appSyncConfig.aws_appsync_graphqlEndpoint}`,
  exchanges: [
    dedupExchange,
    cacheExchange,
    authExchange({
      addAuthToOperation,
      getAuth,
      didAuthError
    }),
    fetchExchange
  ]
});

まとめ

本記事では Urql から認証つき Amplify GraphQL API を呼ぶ方法を紹介しました。
subscription の対応は調査しきれていないのでまた調査してみようと思います。
最後までお読みいただきありがとうございました。

脚注
  1. Apollo v3 は offline 対応が無いようなので注意が必要です。 ↩︎

  2. 3rd party のライブラリはあるようなのですがメンテされてないようです。 ↩︎

  3. リストの操作など ↩︎

  4. Urql 専用のプラグインを入れる必要があります。 ↩︎

Discussion