Vue.jsアプリにAuth0とGraphQLを使って認証と承認を追加

Takeshi Amano
13 min readApr 1, 2019

--

https://blog.hasura.ioの元の記事のリンク

TL;DR (この記事の概要)

  • Auth0でVue.jsアプリに認証を追加
  • Hasura GraphQLのpermissionを使ったJWT認証
  • ログインしたユーザーが書いた記事だけが読めるサンプルVueアプリ
  • サンプルアプリのソースコード
Vue.js + Auth0 + GraphQL + Hasura

技術スタック

アプリは次のスタックを使用して、動作させるために以下の設定および構成に必要があります。

  • vue-cli-plugin-apolloとvue-routerを使ったVue.js
  • Auth0で認証
  • Hasura GraphQLエンジンでInstant GraphQL API

GraphQL APIを準備するために、Hasuraをpostgresと一緒にデプロイしましょう。

Hasuraのデプロイ

Hasuraは、新規または既存のPostgresデータベース上でリアルタイムのGraphQL APIを提供するオープンソースのエンジンです。カスタムGraphQL APIのステッチおよびデータベース変更時のWebフックのトリガーをサポートしています。

Hasuraをデプロイはドキュメントに従ってください。 Heroku URLがGraphQLエンドポイントになります。後でこれをアプリに設定します。

このセクションの手順に従って移行を適用し、必要なデータベーススキーマと権限を作成します。

これでバックエンドの準備が整いました! Hasura GraphQL APIを使用して即座にクエリを実行できるようになります。エンドポイントは(https://myapp.herokuapp.com/v1alpha1/graphql)のようになります。 Vueアプリの開発中にこれを使います。

Auth0でアプリを作成

  1. Auth0ダッシュボードに進み、Single Page Web Appタイプのアプリケーションを作成します。

2. アプリケーションの設定で、 “Allowed Callback URLs”としてhttp:// localhost:3000/callback、 “Allowed Web Origins”としてhttp://localhost:3000を追加して、アプリのローカル開発を有効にします。

カスタムJWTクレームのルールの追加

Auth0ダッシュボードで、[Rules]に移動します。以下のルールを追加して、カスタムJWTクレームを追加します。

function (user, context, callback) {
const namespace = "https://hasura.io/jwt/claims";
context.idToken[namespace] =
{
'x-hasura-default-role': 'user',
// do some custom logic to decide allowed roles
'x-hasura-allowed-roles': user.email === 'admin@foobar.com' ? ['user', 'admin'] : ['user'],
'x-hasura-user-id': user.user_id
};
callback(null, user, context);
}

JWT証明書の取得

https://hasura.io/jwt-configにアクセスして、Auth0ドメイン用の設定を生成します。

認証アプリケーション用に生成されたJWT Configをコピーします。

HasuraでJWTモードを有効に

上記で生成された設定は、Hasura_GRAPHQL_JWT_SECRET環境変数で使用される必要があります。 JWTモードを有効にするには、HASURA_GRAPHQL_ADMIN_SECRETキーも設定する必要があります。

これを追加すると、GraphQLエンドポイントはAuthorizationヘッダーまたはX-Hasura-Admin-Secretヘッダーがある時だけクエリできます。

Auth0ルールの作成

ユーザーがAuth0にサインアップするたびに、そのユーザーをpostgresデータベースに同期させる必要があります。これはAuth0ルールを使って行われます。別のルールを作成して次のコードを挿入します。

function (user, context, callback) {
const userId = user.user_id;
const nickname = user.nickname;

request.post({
headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': '<your-admin-secret>'},
url: 'http://myapp.herokuapp.com/v1alpha1/graphql',
body: `{\"query\":\"mutation($userId: String!, $nickname: String) {\\n insert_users(\\n objects: [{ auth0_id: $userId, name: $nickname }]\\n on_conflict: {\\n constraint: users_pkey\\n update_columns: [last_seen, name]\\n }\\n ) {\\n affected_rows\\n }\\n }\",\"variables\":{\"userId\":\"${userId}\",\"nickname\":\"${nickname}\"}}`
}, function(error, response, body){
console.log(body);
callback(null, user, context);
});
}

admin secretURLを自分の値で置き換えます。

これで全てのバックエンドと認証設定の準備が整いました。正しいヘッダを使用してGraphQLクエリを作成するようにVue.jsフロントエンドを設定しましょう。

Vue-CLI-Apollo-Pluginの設定

boilerplateコードを使って始めるために、Auth0のサンプルアプリケーションを使用します。

次のコマンドでVueアプリ用のアポロクライアントセットアップを生成します。

vue add apollo

これにより、srcにvue-apollo.jsというファイルが生成されます。このファイルでは、optionsオブジェクトgetAuthを次のように定義して構成します。

getAuth: tokenName => {
// get the authentication token from local storage if it exists
// return the headers to the context so httpLink can read them
const token = localStorage.getItem('apollo-token')
if (token) {
return 'Bearer ' + token
} else {
return ''
}
},

この設定は、ApolloClientが、クエリまたはサブスクリプションを作成するときに、Auth0によって返されたトークンをAuthorizationヘッダーに使用するようにします。

認証されたクエリ

Apollo Clientは上記の設定で正しいheaderが設定されています。それでは、ログインしているユーザーによって書かれた記事のリストを取得するための簡単なクエリを追加しましょう。

export default {
apollo: {
// Simple query that will update the 'article' vue property
article: gql`query {
article {
id
title
}
}`,
},
}

これで、ユーザーがアプリにログインしている場合にのみ表示したいと思います。

Home.vue<template>タグでは、記事を一覧表示するために次のコードスニペットを使用します。

<template>
...
...
<div v-if="isAuthenticated">
<h1 class="mb-4">
Articles written by me
</h1>
<div v-for="a in article" :key="a.id">
{{a.id}}. {{ a.title }}
</div>
</div>
...
...
</template>

このマークアップは、isAuthenticatedがtrueを返す場合にのみレンダリングされる必要があることを確認していることに注意してください。これを実装するために、ログインが成功するたびにイベントを発行します。

src/auth/authService.jsにアクセスして、Auth0ログインとイベント発行の実装の詳細を確認してください。

このファイルでは、ログインが成功するとイベントが発行されます。

this.emit(loginEvent, {
loggedIn: true,
profile: authResult.idTokenPayload,
state: authResult.appState || {}
});

src/pluginsでこのイベントを処理するためのpluginが登録されました。

import authService from "../auth/authService";

export default {
install(Vue) {
Vue.prototype.$auth = authService;

Vue.mixin({
created() {
if (this.handleLoginEvent) {
authService.addListener("loginEvent", this.handleLoginEvent);
}
},

destroyed() {
if (this.handleLoginEvent) {
authService.removeListener("loginEvent", this.handleLoginEvent);
}
}
});
}
};

そのためloginEventが発生すると、handleLoginEventメソッドが呼び出されます。

そして、Home.vueコンポーネントでは、そのメソッドを処理してisAuthenticated値を更新します。デフォルトではfalseであり、ログインが成功するとtrueに更新されます。

methods: {
handleLoginEvent(data) {
this.isAuthenticated = data.loggedIn;
this.isLoading = false;
}
},

上記のGraphQLクエリは、Auth0から返されたトークンヘッダを使用して送信され、これが認証を処理します。

Authorization using JWT

ユーザーはログインしていますが、同じユーザーによって書かれた記事のみを表示したいです。記事を書いたユーザだけがデータを取得できるようにパーミッションが設定されています。

HerokuアプリのURLに移動してHasuraコンソールを開き、Data->article->Permissionsに移動して、userロールに定義されている権限を確認します。

権限チェックは次のようになります:

{ "user_id": {"_eq": "X-Hasura-User-Id"}}

これは、リクエストがクライアントからAuthorization:Bearer <token>で送信されている場合、トークンペイロードからX-Hasura-User-Id値を探し、user_id列をフィルタリングして、ログインしていることだけを確認することを意味します。ユーザーはデータを取得し、自分のデータのみを取得します。ユーザーはすべての列にアクセスする権限を持っています。

Vue Routerを使ってルーティングの保護

Vue Routerを使用しているので、Global Before Guardを使用してナビゲーNavigation Guardsを追加できます。これは、ナビゲーションがトリガされ、ナビゲーションが解決されるまで保留中と見なされるたびに呼び出されます。

src/router.jsでは、解決する前にブール値のauth.isAuthenticated()をチェックするbeforeEachガードを定義しています。

router.beforeEach((to, from, next) => {
if (to.path === "/" || to.path === "/callback" || auth.isAuthenticated()){
return next();
}

auth.login({ target: to.path });
});

この場合、ページが//callbackではない、またはユーザーが認証されていない場合、ユーザーはauth.loginメソッドを使用してログインページにリダイレクトされます。

アプリの実行

Vue.jsアプリでHasura GraphQLエンドポイントを設定する必要があります。 src/vue-apollo.jsに移動して、httpEndpointおよびwsEndpointの値を適切に変更してください。

次のコマンドを実行してサンプルアプリを実行します:

npm install
npm run serve

次のような画面が表示されるはずです:

あなたがすぐに始めることができるように、boilerplateをまとめてみました!

githubのコードも見ておいてくださいね。

そのコードを走らせてみて感想を聞かせてください。質問がある場合、または問題が発生した場合は、Twittergithub、またはdiscordサーバーでお気軽にお問い合わせください。

--

--

Takeshi Amano

広島出身、アムステルダム在住。レガシーシステムをPWA化したり、Jamstackで遊んだりしてます。最近はProduct Managementの勉強してます。