🪐

Hasura + Amazon CognitoでJWT認証する手順

2023/09/13に公開

この記事で解説すること

Amazon Cognitoのセットアップ(ユーザプールの作成と各種トリガーの設定)から、Hasuraへの認証情報の連携までの手順を解説します。JWT認証は、CognitoのホストされたUI、又は、Cognito API(amazon-cognito-identity-js)​を使用して実装する2パターンを紹介します。

0. 各種サービスのアカウント準備

この記事で紹介する手順を実施する為、以下のアカウントがご用意ください。

  • AWSアカウント
  • Hasura Cloudアカウント

1. Amazon Cognitoにユーザプールを作成する

Amazon Cognitoのコンソール画面から、ユーザプールを作成します。

https://aws.amazon.com/jp/cognito/

Cognito ユーザープールのサインインオプションには、ユーザー名とEメールを選択しています。

パスワードポリシー多要素認証ユーザーアカウントの復旧は、任意の設定で問題ありませんが、今回は簡単の為多要素認証はMFA なしとしています。

サインアップエクスペリエンスを設定は、全てデフォルトのままで問題ありません。

メッセージ配信を設定は、CognitoでEメールを送信としていますが、ご自身のドメインを使用したい場合などは、Amazon SESでEメールを送信を選択してください。

アプリケーションを統合は、次のように設定しています。

  • ユーザープール名:hasura-cognito-jwt-sample
  • ホストされた認証ページ:Cognito のホストされた UI を使用
  • ドメイン:Cognito ドメインを使用する
  • Cognito ドメインhttps://jwt-sample(任意)
  • 最初のアプリケーションクライアント:パブリッククライアント
  • アプリケーションクライアント名:hasura-cognito-jwt-sample-client(任意)
  • クライアントシークレット:クライアントのシークレットを生成しない
  • 許可されているコールバック URLhttp://localhost:3000




2. Amazon Cognitoのアプリケーションクライアントの設定

ユーザープールの作成が完了したら、アプリケーションクライアントの設定を行います。アプリケーションの統合→アプリケーションクライアントのリストから、先ほど作成したアプリケーションクライアントを選択します。

ホストされたUIから、編集ボタンをクリックします。

OAuth 2.0 許可タイプに、暗黙的な付与を追加してください。

3. AWS Lambdaにトリガー用の関数を作成する

Hasuraとユーザ情報を連携させる為、以下の2つのLambda関数を作成します。

  • サインアップ時にカスタムクレームをJWTに追加(トークン生成前 Lambdaトリガー)
  • ユーザ情報をHasuraのusersテーブルに追加(認証後 Lambdaトリガー)

3-1. トークン生成前 Lambdaトリガー

サインアップしたユーザが、Hasura上でどのロール(x-hasura-user-role)にアサインされるかを決定する為、トークン生成前 Lambdaトリガーを作成します。次のように、Lambda関数を作成してください。

  • 関数名:hasura-cognito-custom-jwt-claims(任意)
  • ランタイム:Node.js 18.x
  • アーキテクチャ:x86_64

関数の作成が完了したら、index.mjsの内容を以下のコードで上書きし、Deployしてください。

index.mjs
export const handler = async (event, context) => {
  event.response = {
    claimsOverrideDetails: {
      claimsToAddOrOverride: {
        "https://hasura.io/jwt/claims": JSON.stringify({
          "x-hasura-user-id": event.request.userAttributes.sub,
          "x-hasura-default-role": "user",
          "x-hasura-allowed-roles": ["user"],
        }),
      },
    },
  };
  return event;
};

この例では、サインアップしたユーザをuserロールにアサインしています。

3-2. 認証後 Lambdaトリガー

ユーザの情報(ID、名前など)をHasuraのテーブル(今回はusersテーブルとします)に登録する処理を実装します。usersテーブルは次のような構造であると想定します。

  • id: ユーザID(TEXT)
  • username: ユーザ名(TEXT)

次のように、Lambda関数を作成してください。

  • 関数名:hasura-cognito-sync-users(任意)
  • ランタイム:Node.js 18.x
  • アーキテクチャ:x86_64

関数の作成が完了したら、index.mjsの内容を以下のコードで上書きし、Deployしてください。

index.mjs
export const handler = async (event, context) => {
  const userId = event.request.userAttributes.sub;
  const userName = event.userName;
  const hasuraAdminSecret = '<your-admin-secret>';
  const hasuraUrl = `https://<your-hasura-project-name>.hasura.app/v1/graphql`;

  const upsertUserQuery = `
    mutation($userId: String!, $userName: String!){
      insert_users(objects: [{ id: $userId, username: $userName }], on_conflict: { constraint: users_pkey, update_columns: [] }) {
        affected_rows
      }
    }`;

  const graphqlReq = {
    query: upsertUserQuery,
    variables: {
      userId,
      userName,
    },
  };

  const fetchOptions = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-hasura-admin-secret': hasuraAdminSecret,
    },
    body: JSON.stringify(graphqlReq),
  };

  try {
    const response = await fetch(hasuraUrl, fetchOptions);
    const data = await response.json();
    console.log(data);
    return event;
  } catch (error) {
    console.error(error);
    throw error;
  }
};

この例では、CognitoのユーザIDをid、ユーザ名をusernameとして、usersテーブルに登録しています。hasuraAdminSecretなどは、適宜環境変数に設定してください。

4. Amazon CognitoのLambdaトリガーの設定

Lambda関数の作成が完了したら、CognitoのLambdaトリガーの設定を行います。Cognitoのコンソール画面から、ユーザープールのプロパティLambda トリガーを選択します。

作成したLambda関数をそれぞれ、次のようにトリガーに設定してください。

認証 -> トークン生成前トリガー:hasura-cognito-custom-jwt-claims

認証 -> 認証後トリガー:hasura-cognito-sync-users

5. Hasuraのセットアップ

テーブルの作成と、認証情報の設定を行います。今回はHasura Cloudを使用します。

5-1. usersテーブルの作成

次のように、idusernameのカラムのみのusersテーブルを作成します。

5-2. userロールへの権限設定

Hasuraのコンソール画面から、DATAusersPermissionsを選択し、ROLEuserを追加してください。

今回は、userロールに対して、SELECT権限のみを付与し、自身のデータのみ(idh-hasura-user-idが一致しているレコード)を取得できるようにしています。

5-3. JWTシークレットの設定

Hasura Cloudのダッシュボードから、Env varsを登録します。Amazon Cognitoと連携する場合、jwks.jsonHASURA_GRAPHQL_JWT_SECRETに設定します。
次のように、作成したCognitoユーザプールのリージョンユーザプールIDを使用して設定値を作成してください。

HASURA_GRAPHQL_JWT_SECRET
{
  "type": "RS256",
  "jwk_url": "https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.json",
  "claims_format": "stringified_json"
}

6. ホストされたUIから動作確認

CognitoのホストされたUIには、以下のようなURLでアクセスできます。

サインアップ

https://<your_domain>.auth.<your_region>.amazoncognito.com/signup?client_id=<your_client_id>&response_type=token&scope=email+openid+phone&redirect_uri=<redirect_url>

実際にユーザ登録、ログインを行い、Hasuraのコンソール画面から、DATAusersBrowseを選択し、ユーザが登録されていることを確認してみましょう。

確認コードを入力し、Confirm accoutをクリックしたら、Cognitoのユーザープールにアカウントが登録されます。
次に、ログインを行なったタイミングで、認証後 トリガーに設定したLambda関数が実行され、Hasuraのusersテーブルにユーザ情報が登録されます。

ログイン

https://<your_domain>.auth.<your_region>.amazoncognito.com/login?client_id=<your_client_id>&response_type=token&scope=email+openid+phone&redirect_uri=<redirect_url>

また、ログイン後にリダイレクトされた際に、URLのid_tokenにJWTが付与されていることを確認してください。次のようなURLになっているはずです。

http://localhost:3000/#id_token=xxxxxxxxxxx&access_token=xxxxxxxxxxx&expires_in=3600&token_type=Bearer

Hasuraのコンソールからも、ユーザ情報が確認できます。

7. CognitoのAPIから動作確認

テンプレート部分のコードは割愛しますいが、以下はNuxt3でCognitoにログインする際の実装例です。amazon-cognito-identity-jsを使用して、CognitoのAPIを呼び出しています。

plugins/cognito.js
import { CognitoUserPool } from 'amazon-cognito-identity-js';
import { defineNuxtPlugin } from '#app';

const poolData = {
  UserPoolId: '<your_user_pool_id>',
  ClientId: '<your_client_id>',
};

const userPool = new CognitoUserPool(poolData);

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use({
    install: (app) => app.provide('userPool', userPool),
  });
});
pages/index.vue
<script setup>
import { CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';

const userPool = inject('userPool');

const username = ref('');
const password = ref('');

const signIn = () => {
  const authenticationData = {
    Username: username.value,
    Password: password.value
  };
  const authenticationDetails = new AuthenticationDetails(authenticationData);

  const userData = {
    Username: username.value,
    Pool: userPool
  };
  const cognitoUser = new CognitoUser(userData);

  cognitoUser.authenticateUser(authenticationDetails, {
    onSuccess: (result) => {
      const accessToken = result.getAccessToken().getJwtToken();
      // 認証成功時の処理
    },
    onFailure: (err) => {
      // 認証失敗時の処理
      console.error('Authentication failed:', err);
    }
  });
};
</script>

ログインに成功すると、次のようにLocalStorageにaccessTokenが自動的に保存されます。

GraphQLのリクエストヘッダにJWTを付与

以下は、@urql/vueを使用してHasuraのGraphQL APIを呼び出す際の実装例です。accessTokenをLocalStorageから取得し、リクエストヘッダに付与しています。

plugins/urql.js
import { Client, cacheExchange, fetchExchange } from '@urql/vue';
import urql from '@urql/vue';
import { defineNuxtPlugin } from "#app";

export default defineNuxtPlugin((nuxtApp) => {
  const runtimeConfig = useRuntimeConfig()
  const ClientId = '<your_client_id>';
  const graphqlClient = new Client({
    url: runtimeConfig.public.endPoint,
    exchanges: [cacheExchange, fetchExchange],
    fetchOptions: () => {
      const userName = localStorage.getItem('CognitoIdentityServiceProvider.'+ClientId+'.LastAuthUser');
      const key = 'CognitoIdentityServiceProvider.'+ClientId+'.'+userName+'.idToken';
      const idToken = localStorage.getItem(key);

      return {
        headers: {
          'Authorization': `Bearer ${idToken}`,
          'x-hasura-user-role': 'user',
        },
      }
    },
  });

  nuxtApp.vueApp.use(urql, graphqlClient);
});
pages/index.vue
<script setup>
import { useQuery } from '@urql/vue';

const result = await useQuery({
  query: gql`
    query {
      users {
        id
        username
      }
    }
  `
});

const users = result.data.value.users;
console.log(users);

</script>

次のようにデータが取得できていれば、JWT認証が成功しています。

8. まとめ

HasuraとAmazon Cognitoを連携することで、HasuraのGraphQL APIを認証付きで使用することができました。IDaaSによって実装方法は異なりますが、Firebase AuthenticationAuth0などでも同様の実装が可能です。

CognitoやLambdaの設定は、AWS CloudFormationなどを使用してテンプレート化しておくと、ユーザ管理が必要なサービスを新規で開発したくなった際に、よりスピーディーに実装を進めることができそうですね。

参考

https://hasura.io/learn/graphql/hasura-authentication/integrations/cognito/

Algorithm's Tech Blog

Discussion