Hasura + Amazon CognitoでJWT認証する手順
この記事で解説すること
Amazon Cognitoのセットアップ(ユーザプールの作成と各種トリガーの設定)から、Hasuraへの認証情報の連携までの手順を解説します。JWT認証は、CognitoのホストされたUI、又は、Cognito API(amazon-cognito-identity-js)を使用して実装する2パターンを紹介します。
0. 各種サービスのアカウント準備
この記事で紹介する手順を実施する為、以下のアカウントがご用意ください。
- AWSアカウント
- Hasura Cloudアカウント
1. Amazon Cognitoにユーザプールを作成する
Amazon 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(任意)
- クライアントシークレット:クライアントのシークレットを生成しない
- 許可されているコールバック URL:http://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してください。
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してください。
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テーブルの作成
次のように、id
とusername
のカラムのみのusers
テーブルを作成します。
5-2. userロールへの権限設定
Hasuraのコンソール画面から、DATA→users→Permissionsを選択し、ROLEにuser
を追加してください。
今回は、user
ロールに対して、SELECT
権限のみを付与し、自身のデータのみ(id
とh-hasura-user-id
が一致しているレコード)を取得できるようにしています。
5-3. JWTシークレットの設定
Hasura Cloudのダッシュボードから、Env varsを登録します。Amazon Cognitoと連携する場合、jwks.jsonをHASURA_GRAPHQL_JWT_SECRET
に設定します。
次のように、作成したCognitoユーザプールのリージョンとユーザプールIDを使用して設定値を作成してください。
{
"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のコンソール画面から、DATA→users→Browseを選択し、ユーザが登録されていることを確認してみましょう。
確認コードを入力し、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を呼び出しています。
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),
});
});
<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から取得し、リクエストヘッダに付与しています。
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);
});
<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 AuthenticationやAuth0などでも同様の実装が可能です。
CognitoやLambdaの設定は、AWS CloudFormationなどを使用してテンプレート化しておくと、ユーザ管理が必要なサービスを新規で開発したくなった際に、よりスピーディーに実装を進めることができそうですね。
参考
Discussion