Movatterモバイル変換


[0]ホーム

URL:


LoginSignup
140

Go to list of users who liked

120

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

認証付きGraphQL APIサーバーを爆速で立てる。 Hasura + Firebase Authentication

Last updated atPosted at 2020-01-14

HasuraはPostgreSQLからGraphQL APIサーバーを爆速で構築できるものの、認証については外部の認証基盤を使う必要があります。
今回は、認証基盤としてFirebase AuthenticationのJWT認証を使った例を紹介します。

Hasuraの認証について

Hasuraの認証はWebhook方式と、JWT方式があり、今回はJWT方式を使います。
JWTは属性情報をJSONデータ構造で表現したトークンを使い認証を行う方法で、Firebase Authenticationにて採用されています。
Hasuraの認証でFirebase Authenticationを使う場合は以下のような流れとなります。

  1. クライアントアプリでFirebase SDKを利用して認証サーバーにアクセスしてトークンを発行(ここでカスタムクレームにHasuraの属性情報も持たせておく)
  2. リクエストヘッダーのAuthorizationにBearerスキームで、トークンを埋め込む
  3. Hasuraから検証サーバーにアクセスしてトークンの検証を行う。

無題のプレゼンテーション.jpg

Hasuraの設定

まずGraphQL APIサーバーの構築を行います。

Hasura環境構築

以下のボタンをクリックしてHerokuにHasuraをデプロイします。

image.png

アプリ名は適当に入力してDeploy App。

スクリーンショット 2020-01-12 11.41.51.png

完了したらView appからHasuraのコンソールへ。

スクリーンショット 2020-01-12 11.42.36.png

これでHasuraの環境構築は完了です。
上部に表示されているアクセスポイントでHasuraのGraphQL APIが使用できます。

スクリーンショット 2020-01-12 11.44.45.png

アクセス制限、JWT検証サーバーの設定

Hasuraのコンソール・APIは今のままだと、URLさえ知れば誰でもアクセスできる状態です。それを防ぐためADMIN_SECRETの設定をします。

Herokuのコンソールより、Hasura用に作成したAPPを選択してSettingsのConfig Varsから以下を設定してください。

変数名
HASURA_GRAPHQL_ADMIN_SECRETAdmin権限でアクセスする際に必要なパスワード(任意の値)
HASURA_GRAPHQL_JWT_SECRETJWTのモード、検証サーバーの設定(こちらより生成)

スクリーンショット 2020-01-12 23.43.18.png

HASURA_GRAPHQL_JWT_SECRETは、https://hasura.io/jwt-config よりFirebase、Project IDを入力することで生成できます。

スクリーンショット 2020-01-12 23.43.18.png

これでHasura側の設定は完了です。
以降、Hasuraのコンソールにログインする際、パスワードを求められるようになるはずです。
HASURA_GRAPHQL_ADMIN_SECRETで設定したパスワードを入力してログインしてください。

スクリーンショット 2020-01-12 23.53.17.png

テーブル構築

今回は簡易なメモアプリを作成します。テーブルはユーザー情報を保存するusersと、メモを保存するmemosの2つとします。
以下Hasuraのコンソール上でテーブルを作成します。
上部メニューDATA > Create tableより、まずusersテーブルの作成。

カラム名構造属性
idTextPrimary key
nameText
create_atTimeStampdefault now()
スクリーンショット 2020-01-12 12.00.20.png

次にmemosテーブルの作成。

カラム名構造属性
idInteger(Auto-increment)Primary key
user_idTextForeign key user.id
contentText
created_atTimestampdefault now()
スクリーンショット 2020-01-13 20.35.51.png

Foreign Keysでuser_idの関連キーとしてusersテーブルのidを指定します。

スクリーンショット 2020-01-13 20.35.35.png

これでテーブルの作成は完了です。

認可の設定

次にテーブルの操作・カラム単位での認可の設定をします。
DATA > サイドメニュー todos > Permissionsタブから新しいロールuserを追加します。
そしてuserロールでは自分のuser_idと関連するメモしかinsert, select, update, delete出来ないようにします。

ここで設定するX-Hasura-User-Idは後ほどFirebase Authenticationのカスタムクレームで設定します。

まずinsertの設定。
Allow role user to insert rowsで、With custom checkを選択肢し、user_id eq X-Hasura-User-Idを指定します。
その他、画像のように設定します。

スクリーンショット 2020-01-13 20.37.06.png

次にselectの設定。
Allow role user to insert rowsは、With same custom check insertを選択します。
これでinsertと同じくuser_idがX-Hasura-User-Idと同様のものしかselect出来ないようになります。
ほかは画像のとおりです。

スクリーンショ<img width=

次にupdateの設定。
同じくAllow role user to insert rowsは、With same custom check insertを選択肢します。
ほかは画像のとおりです。

スクリーンショット 2020-01-13 20.38.01.png

最後にdeleteの設定。
同じくAllow role user to insert rowsは、With same custom check insertを選択肢します。

スクリーンショット 2020-01-13 20.38.12.png

これで認可の設定は完了です。

Firebaseの設定

続いて認証基盤として使うFirebaseの設定です。

プロジェクトの作成

任意のディレクトリでFirebaseプロジェクトを作成してください。使うリソースはCloud FunctionsとFirestoreです。
拙著ですが、TypeScriptでESLint+Prettierを使う場合の設定例はこちらをどうぞ。

firebase init

課金プランの設定

Cloud FunctionsからHasuraサーバーへユーザー作成のリクエストを投げため、外部サービスへのアクセスを有効にする必要があります。
initで設定したFirebaseプロジェクトのコンソールにログインして、課金プランをBlazeプランに変更してください。

スクリーンショット 2020-01-13 7.11.57.png

Firebase Authenticationの設定

Firebase Authenticationを有効化します。
コンソールのサイドメニューよりAuthenticationを選択して、任意のログイン方法を有効化してください(デモだとGoogleとEmailログイン)。

スクリーンショット 2020-01-13 7.20.34.png

Cloud Functionsの設定

最後にFirebase Cloud Functionsです。
Firebase Authenticationでのユーザー追加をフックに起動するFunctionsを設定します。
firebase initしたディレクトリのfunctions以下で依存モジュールを追加します。

npm i apollo-boost graphql graphql-tag node-fetch @types/node-fetch

次にコード内で参照する環境変数として、HasuraのエンドポイントURLとadmin_secreteを設定します。

firebase functions:config:set hasura.url="herokuのHasuraエンドポイントURL" hasura.admin_secret="HerokuのConfig varsで指定したHASURA_GRAPHQL_ADMIN_SECRETの値"

今回functionsのAuthenticationユーザー追加のトリガーにて以下を行います。

  1. Hasura認証用のカスタムクレームの設定
  2. Hasuraサーバーへのユーザー作成リクエストの送信
  3. tokenリフレッシュのフック用にFirestoreへのmetaデータ追加

functions/index.tsに以下を記載してください。

functions/index.ts
import*asfunctionsfrom"firebase-functions";import*asadminfrom"firebase-admin";importApolloClientfrom"apollo-boost";importfetchfrom"node-fetch";importgqlfrom"graphql-tag";admin.initializeApp();constclient=newApolloClient({uri:functions.config().hasura.url,fetch:fetchasany,request:(operation):void=>{operation.setContext({headers:{"x-hasura-admin-secret":functions.config().hasura.admin_secret}});}});exportconstsetCustomClaims=functions.auth.user().onCreate(asyncuser=>{ // Hasuraの検証用のカスタムクレーム(属性情報)constcustomClaims={"https://hasura.io/jwt/claims":{"x-hasura-default-role":"user","x-hasura-allowed-roles":["user"],"x-hasura-user-id":user.uid}};try{// カスタムクレームの設定awaitadmin.auth().setCustomUserClaims(user.uid,customClaims);// Hasuraサーバーへのユーザーデータの作成リクエストawaitclient.mutate({variables:{id:user.uid,name:user.displayName||"unknown"},mutation:gql`        mutation InsertUsers($id: String, $name: String) {          insert_users(objects: { id: $id, name: $name }) {            returning {              id              name              created_at            }          }        }      `});// 初回ログインの際にユーザー作成と、カスタムクレームの設定には遅延があるため、// tokenリフレッシュのフック用にFirestoreへのmetaデータ追加を行うawaitadmin.firestore().collection("user_meta").doc(user.uid).create({refreshTime:admin.firestore.FieldValue.serverTimestamp()});}catch(e){console.log(e);}});

これでdeployを実行するとfunctionsが作成されます。

npm run deploy

deploy完了後、コンソールにてFunctionsを確認できればOKです。

スクリーンショット 2020-01-14 5.36.43.png

クライアントの実装

最後にVue.jsでのクライントの実装を紹介します。
今回は認証がメインなので、細かい環境構築等は省き関連コードのみ紹介します。
以下で紹介するメモアプリの動作コードはこちらにあります。

Firebase Authenticationのログインフックの設定

main.tsにて、Vueの初期化の前にFirebase Authenticationのログインフックの設定を行っています。
ログイン後Hasuraのカスタムクレームがない場合は、FirestoreのonSnapshotにてuser_metaの変更を待ち、変更後再度tokenの取得、ログイン処理を行っている点がポイントです。

main.ts
importVuefrom"vue";importAppfrom"./App.vue";importrouterfrom"./router";importvuetifyfrom"./plugins/vuetify";import{apolloProvider,onLogin,onLogout}from"@/plugins/apollo";importVueApollofrom"vue-apollo";import{auth,db}from"@/plugins/firebase";constHASURA_TOKEN_KEY="https://hasura.io/jwt/claims";Vue.use(VueApollo);Vue.config.productionTip=false;letvue:Vue;// firebaseの初期化が終わったあとにVueを初期化するようにするauth.onAuthStateChanged(asyncuser=>{if(!vue){newVue({vuetify,apolloProvider,router,render:h=>h(App)}).$mount("#app");}if(user){consttoken=awaituser.getIdToken(true);constidTokenResult=awaituser.getIdTokenResult();consthasuraClaims=idTokenResult.claims[HASURA_TOKEN_KEY];if(hasuraClaims){awaitonLogin(token);}else{// Tokenのリフレッシュを検知するためにコールバックを設定するconstuserRef=db.collection("user_meta").doc(user.uid);userRef.onSnapshot(async()=>{consttoken=awaituser.getIdToken(true);awaitonLogin(token);});}}else{awaitonLogout();}});

Apolloクライアントの設定、ログイン・ログアウト処理

ApolloクライアントではBearerスキームで使うtokenをLocalStorage経由で設定しています。
ログイン、ログアウト処理では、tokenのLocalStorageへの追加・削除と、Apolloクライアントのリフレッシュを行っています。

plugin/apollo.ts
importApolloClientfrom"apollo-boost";importVueApollofrom"vue-apollo";constAUTH_TOKEN="hasura-auth-token";constclient=newApolloClient({uri:process.env.VUE_APP_GRPHQL_HTTP,request:operation=>{operation.setContext({headers:{Authorization:`Bearer${localStorage.getItem(AUTH_TOKEN)}`}});}});// ログイン処理exportasyncfunctiononLogin(token:string){if(localStorage.getItem(AUTH_TOKEN)!==token){localStorage.setItem(AUTH_TOKEN,token);}try{awaitclient.resetStore();}catch(e){// eslint-disable-next-lineconsole.error(`Login Failed.${e}`);}}// ログアウト処理exportasyncfunctiononLogout(){if(typeoflocalStorage!=="undefined"){localStorage.removeItem(AUTH_TOKEN);}try{awaitclient.resetStore();}catch(e){// eslint-disable-next-lineconsole.error(`Logout Failed.${e}`);}}exportconstapolloProvider=newVueApollo({defaultClient:client});

Vue routerでの遷移制御

beforeEachにて、各routeへの遷移前にのmeta情報の判定とcurrent_userの有無で遷移制御を行っています。

router/index.ts
importVuefrom"vue";importVueRouterfrom"vue-router";importHomefrom"@/views/Home.vue";importLoginfrom"@/views/Login.vue";import{auth}from"@/plugins/firebase";Vue.use(VueRouter);constroutes=[{path:"/",name:"home",component:Home,meta:{requireAuth:true}},{path:"/login",name:"login",component:Login}];constrouter=newVueRouter({mode:"history",base:process.env.BASE_URL,routes});router.beforeEach((to,_from,next)=>{constrequireAuth=to.matched.some(record=>record.meta.requireAuth);constcurrentUser=auth.currentUser;if(!requireAuth||currentUser){next();return;}next({path:"/login",query:{redirect:to.fullPath}});});exportdefaultrouter;

Firebase UIでのログインページ

ログインページではFirebase UIを利用してログインフォームを構築しています。

view/Login.vue
<template><divclass="about"><h2class="text-center">Please login.</h2><divid="firebaseui-auth-container"></div></div></template><script>import{auth}from"@/plugins/firebase";importfirebasefrom"firebase";import*asfirebaseuifrom"firebaseui";import"firebaseui/dist/firebaseui.css";exportdefault{name:"login",beforeRouteEnter(to,from,next){next(()=>{constuiConfig={signInSuccessUrl:"/",signInFlow:"popup",signInOptions:[firebase.auth.GoogleAuthProvider.PROVIDER_ID,firebase.auth.EmailAuthProvider.PROVIDER_ID]};constui=firebaseui.auth.AuthUI.getInstance()||newfirebaseui.auth.AuthUI(auth);ui.start("#firebaseui-auth-container",uiConfig);});}};</script>

memoの取得・削除

アプリのメインのmemo追加・削除部分の実装はこちらです。
vue-apolloにて通信を行っています。

view/Home.vue
<template><divclass="home"><v-cardclass="mb-5"><v-card-text><v-textareaoutlinedlabel="Memo"single-linev-model="content"/><v-btnblock@click="addMemo"color="primary">add Memo</v-btn></v-card-text></v-card><templatev-if="memos && memos.length > 0"><v-cardv-for="memo in memos":key="memo.id"class="mb-1"><v-card-text>{{memo.content}}</v-card-text><v-card-actions><v-spacer/><v-icon@click="deleteMemo(memo.id)">mdi-delete</v-icon></v-card-actions></v-card></template></div></template><scriptlang="ts">importVuefrom"vue";import{FETCH_MEMOS}from"@/graphql/queries";import{ADD_MEMO,DELETE_MEMO}from"@/graphql/mutations";import{auth}from"@/plugins/firebase";typeMemo={id:number;content:string;created_at:string;};typeData={content:string;memos:Memo[];};exportdefaultVue.extend({name:"home",data():Data{return{content:"",memos:[]};},methods:{asyncaddMemo(){constres=awaitthis.$apollo.mutate({mutation:ADD_MEMO,variables:{content:this.content,userId:(auth.currentUserasfirebase.User).uid}});constinsertResult=res.data.insert_memos.returning[0];this.memos.push({id:insertResult.id,content:insertResult.content,created_at:insertResult.created_at});this.clearField();},asyncdeleteMemo(id:Number){awaitthis.$apollo.mutate({mutation:DELETE_MEMO,variables:{id}});this.memos=this.memos.filter(memo=>memo.id!==id);},clearField(){this.content="";}},apollo:{memos:{query:FETCH_MEMOS}}});</script>

最後に

以上、 「認証付きGraphQL APIサーバーを爆速で立てる。 Hasura + Firebase Authentication」でした。
Firebase AuthenticationをFirebaseのリソース以外の認証基盤として使うのは初めてだったので、JWTの認証方法などとても勉強になりました。Hasuraの日本語情報あまりなく、私もまだまだ勉強中です。もし誤表記や訂正などあればお気軽にコメントにて指摘願いします。

参考URL

140

Go to list of users who liked

120
0

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
140

Go to list of users who liked

120

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?


[8]ページ先頭

©2009-2025 Movatter.jp