ReactおよびGraphQLアプリに認証とルート保護を適用する方法
序章
メール、Facebook、Google、Twitter、Githubはすべて、ウェブアプリでユーザーを認証するための可能なオプションです。 ReactとGraphQLで構築されたアプリは、そのような認証の候補でもあります。
この記事では、以下を使用してさまざまな認証プロバイダーをGraphQLアプリに追加する方法を学習します。
- GraphQL :APIのクエリ言語
- Graphcool :サービスとしてのGraphQLバックエンド
- Auth0 :サービスとしての認証
- React :ユーザーインターフェースを構築するためのJavaScriptライブラリ
また、ユーザーが認証サーバーによる認証に失敗した場合にReactルートがアクセスされないように保護する方法についても学習します。
ステップ1—プロジェクトの準備
計画は、Reactプロジェクトをセットアップすることです。 Reactプロジェクトが設定されているのと同じディレクトリで、Graphcoolサーバーを構成し、サーバーの動作をプロジェクトに指示できます。 create-react-app
およびgraphcool-framework
CLIツールのインストールから始めます。
npm install -g create-react-app graphcool-framework
次に、ReactCLIツールを使用して新しいReactアプリの足場を作成します。
create-react-app do-auth
作成したReactアプリに移動し、Graphcool initコマンドを実行して、Graphcoolサーバーを作成します。
cd do-auth
graphcool-framework init server
ステップ2—ReactルートとUIを作成する
パブリックルートと保護ルートの両方が必要です。
- ホームページ(公開)
- プロフィールページ(保護)
- 管理者ページ(保護され、管理者のみが利用可能)
- アバウトページ(公開)
containers
フォルダーを作成し、home.js
、profile.js
、admin.js
、およびabout.js
をファイルとして追加してこれらの各ルートを表します。
Home.js
import React from 'react'; import Hero from '../components/hero'; const Home = (props) => ( <div> <Hero page="Home"></Hero> <h2>Home page</h2> </div> ) export default Home;
About.js
import React from 'react'; import Hero from '../components/hero'; const About = (props) => ( <div> <Hero page="About"></Hero> <h2>About page</h2> </div> ) export default About;
Profile.js
import React from 'react'; import Hero from '../components/hero'; const Profile = props => ( <div> <Hero page="Profile"></Hero> <h2>Profile page</h2> </div> ); export default Profile;
Admin.js
import React from 'react'; import Hero from '../components/hero'; const Admin = (props) => ( <div> <Hero></Hero> <Hero page="Admin"></Hero> </div> ) export default Admin;
各ページはHero
をインポートして使用し、着陸ヒーローメッセージを表示します。 components
フォルダーを作成し、次のファイルを使用してHero.js
ファイルを作成します。
import React from 'react'; import Nav from './nav' import './hero.css' const Hero = ({ page }) => ( <section className="hero is-large is-dark"> <div className="hero-body"> <Nav></Nav> <div className="container"> <h1 className="title">DO Auth</h1> <h2 className="subtitle">Welcome to the Auth {page}</h2> </div> </div> </section> ); export default Hero;
コンポーネントフォルダにnav.js
としてナビゲーションコンポーネントを追加することもできます。 その前に、Reactアプリのルーティングを設定し、ページをルートとして公開する必要があります。
ReactRouterライブラリのインストールから始めます。
yarn add react-router-dom
次に、index.js
エントリファイルを介してルーターをアプリに提供します。
//... import { BrowserRouter } from 'react-router-dom'; import App from './App'; //... ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ); //...
次に、アプリコンポーネントでルートを構成します。
import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; import Profile from './containers/profile'; import About from './containers/about'; import Admin from './containers/admin'; import Home from './containers/home'; class App extends Component { render() { return ( <div className="App"> <Switch> <Route exact path="/" component={Home} /> <Route exact path="/about" component={About} /> <Route exact path="/profile" component={Profile} /> <Route exact path="/admin" component={Admin} /> </Switch> </div> ); } } export default App;
ナビゲーションコンポーネント(nav.js
)に戻り、react-router-dom
のLink
コンポーネントを使用してナビゲーションをプロビジョニングします。
import React from 'react'; import { Link } from 'react-router-dom' import './nav.css' const Nav = () => { return ( <nav className="navbar"> <div className="navbar-brand"> <Link className="navbar-item" to="/"> <strong>Auth Page</strong> </Link> </div> <div className="navbar-menu"> <div className="navbar-end"> <Link to="/about" className="navbar-item"> About </Link> <Link to="/profile" className="navbar-item"> Profile </Link> <div className="navbar-item join"> Join </div> </div> </div> </nav> ); }; export default Nav;
次に、Graphcoolサーバーを作成します
graphcool-framework deploy
ステップ3—サーバー認証用のAuth0の構成
クライアントアプリから少し離れて、前に作成したGraphcoolサーバーに戻りましょう。 Graphcoolには、サーバーの機能を拡張できるサーバーレス機能の概念があります。 この機能は、認証を含む多くのサードパーティ統合を実現するために使用できます。
このような統合の一部の関数は事前にパッケージ化されているため、最初から作成する必要はありません。 テンプレートをインストールし、いくつかの構成とタイプのコメントを外してから、必要に応じてコードを更新または微調整するだけです。
Auth0テンプレートを追加しましょう。 server
フォルダーにいることを確認し、次を実行します。
graphcool-framework add-template graphcool/templates/auth/auth0
これにより、server/src
にauth0
フォルダーが作成されます。 このフォルダーには、関数ロジック、タイプ、およびこの関数をトリガーするミューテーション定義の両方が含まれています。
次に、Auth0 APIを作成してから、APIの構成をサーバーに追加する必要があります。 最初にアカウントを作成してから、APIダッシュボードから新しいAPIを作成します。 APIには好きな名前を付けることができます。 既存のすべてのAPIに固有の識別子を提供します。
server/src/graphcool.yml
のテンプレート構成のコメントを解除し、次のように更新します。
authenticate: handler: code: src: ./src/auth0/auth0Authentication.js environment: AUTH0_DOMAIN: [YOUR AUTH0 DOMAIN] AUTH0_API_IDENTIFIER: [YOUR AUTH0 IDENTIFIER] type: resolver schema: ./src/auth0/auth0Authentication.graphql
AUTH0_DOMAIN
とAUTH0_API_IDENTIFIER
は、環境変数として関数のprocess.env
に公開されます。
templateコマンドは、server/src/types.graphql
のタイプも生成します。 デフォルトではコメントアウトされています。 次のコメントを解除する必要があります。
type User @model { # Required system field: id: ID! @isUnique # read-only (managed by Graphcool) # Optional system fields (remove if not needed): createdAt: DateTime! # read-only (managed by Graphcool) updatedAt: DateTime! # read-only (managed by Graphcool) email: String auth0UserId: String @isUnique }
サーバーの作成時に生成されたUser
タイプを削除して、このauthUser
タイプで置き換える必要があります。
次に、認証ロジックを微調整する必要があります。 次のコードブロックを見つけます。
jwt.verify( token, signingKey, { algorithms: ['RS256'], audience: process.env.AUTH0_API_IDENTIFIER, ignoreExpiration: false, issuer: `https://${process.env.AUTH0_DOMAIN}/` }, (err, decoded) => { if (err) throw new Error(err) return resolve(decoded) } )
そして、verify
メソッドのaudience
プロパティをaud
に更新します。
jwt.verify( token, signingKey, { algorithms: ['RS256'], aud: process.env.AUTH0_API_IDENTIFIER, ignoreExpiration: false, issuer: `https://${process.env.AUTH0_DOMAIN}/` }, (err, decoded) => { if (err) throw new Error(err) return resolve(decoded) } )
最後に、認証トークンは常に電子メールをエンコードするため、電子メールを取得するために次のことを行う必要はありません。
let email = null if (decodedToken.scope.includes('email')) { email = await fetchAuth0Email(accessToken) }
decodedToken
からすぐにメールを受け取ることができます。
const email = decodedToken.email
これにより、fetchAuth0Email
機能が使用できなくなるため、削除できます。
次のコマンドを実行して、サーバーをGraphcoolにデプロイします。
graphcool-framework
Graphcoolを初めて使用する場合は、Graphcoolアカウントを作成するためのページに移動する必要があります。
ステップ4—クライアント認証用のAuth0の構成
サーバーは、認証用のトークンを受信するように設定されています。 次のコマンドを実行して、Graphcoolプレイグラウンドでこれをテストできます。
graphcool-framework playground
ミューテーションにAuth0トークンを提供し、Graphcoolからノードトークンを取得します。 Auth0からトークンを取得する方法を見てみましょう。
APIを作成するのと同じように、プロジェクトのクライアントも作成する必要があります。 クライアントは、ブラウザからの認証をトリガーするために使用されます。 Auth0ダッシュボードナビゲーションで、クライアントをクリックして、新しいクライアントを作成します。
アプリケーションタイプは、シングルページWebアプリケーションに設定する必要があります。これは、ルーティングされたReactアプリです。
authが開始されると、Auth0ドメインにリダイレクトされてユーザーを確認します。 ユーザーが確認されると、ユーザーをアプリにリダイレクトする必要があります。 コールバックURLは、リダイレクト後に戻る場所です。 作成したクライアントの設定タブに移動し、コールバックURLを設定します。
これで、Auth0ダッシュボードでのクライアント構成のセットアップは完了です。 次に実行したいのは、Reactアプリでいくつかのメソッドを公開するサービスを作成することです。 これらのメソッドは、認証のトリガー、Auth0からの応答の処理、ログアウトなどのユーティリティタスクを処理します。
まず、Auth0JSライブラリをインストールします。
yarn add auth0-js
次に、src
にservices
フォルダーを作成します。 次の内容のauth.js
を新しいフォルダーに追加します。
import auth0 from 'auth0-js'; export default class Auth { auth0 = new auth0.WebAuth({ domain: '[Auth0 Domain]', clientID: '[Auth0 Client ID]', redirectUri: 'http://localhost:3000/callback', audience: '[Auth0 Client Audience]', responseType: 'token id_token', scope: 'openid profile email' }); handleAuthentication(cb) { this.auth0.parseHash({hash: window.location.hash}, (err, authResult) => { if (authResult && authResult.accessToken && authResult.idToken) { this.auth0.client.userInfo(authResult.accessToken, (err, profile) => { this.storeAuth0Cred(authResult, profile); cb(false, {...authResult, ...profile}) }); } else if (err) { console.log(err); cb(true, err) } }); } storeAuth0Cred(authResult, profile) { // Set the time that the access token will expire at let expiresAt = JSON.stringify( authResult.expiresIn * 1000 + new Date().getTime() ); localStorage.setItem('do_auth_access_token', authResult.accessToken); localStorage.setItem('do_auth_id_token', authResult.idToken); localStorage.setItem('do_auth_expires_at', expiresAt); localStorage.setItem('do_auth_profile', JSON.stringify(profile)); } storeGraphCoolCred(authResult) { localStorage.setItem('do_auth_gcool_token', authResult.token); localStorage.setItem('do_auth_gcool_id', authResult.id); } login() { this.auth0.authorize(); } logout(history) { // Clear access token and ID token from local storage localStorage.removeItem('do_auth_access_token'); localStorage.removeItem('do_auth_id_token'); localStorage.removeItem('do_auth_expires_at'); localStorage.removeItem('do_auth_profile'); localStorage.removeItem('do_auth_gcool_token'); localStorage.removeItem('do_auth_gcool_id'); // navigate to the home route history.replace('/'); } isAuthenticated() { // Check whether the current time is past the // access token's expiry time const expiresAt = JSON.parse(localStorage.getItem('do_auth_expires_at')); return new Date().getTime() < expiresAt; } getProfile() { return JSON.parse(localStorage.getItem('do_auth_profile')); } }
このコードの機能は次のとおりです。
- まず、これによりAuth0 SDKのインスタンスが作成され、Auth0クライアントの資格情報を使用して構成されます。 このインスタンスは、インスタンス変数
auth0
に格納されます。 handleAuthentication
は、認証が完了すると、コンポーネントの1つによって呼び出されます。 Auth0は、URLハッシュを介してトークンを返します。 このメソッドは、このハッシュを読み取って渡します。storeAuth0Cred
およびstoreGraphCoolCred
は、将来の使用のために資格情報をlocalStorageに保持します。isAuthenticated
を呼び出して、localStorageに保存されているトークンがまだ有効かどうかを確認できます。getProfile
は、ユーザーのプロファイルのJSONペイロードを返します。
また、ユーザーが認証されている場合はユーザーをプロファイルページにリダイレクトし、認証されていない場合はユーザーをホームページ(デフォルトページ)に送り返します。 コールバックページはこれに最適な候補です。 まず、App.js
のルートに別のルートを追加します。
//... import Home from './containers/home'; import Callback from './containers/callback' class App extends Component { render() { return ( <div className="App"> <Switch> <Route exact path="/" component={Home} /> {/* Callback route */} <Route exact path="/callback" component={Callback} /> ... </Switch> </div> ); } } export default App;
/callback
は、src/container/Callback.js
で作成する必要のあるCallback
コンポーネントを使用します。
import React from 'react'; import { graphql } from 'react-apollo'; import gql from 'graphql-tag'; import Auth from '../services/auth' const auth = new Auth(); class Callback extends React.Component { componentDidMount() { auth.handleAuthentication(async (err, authResult) => { // Failed. Send back home if (err) this.props.history.push('/'); // Send mutation to Graphcool with idToken // as the accessToken const result = await this.props.authMutation({ variables: { accessToken: authResult.idToken } }); // Save response to localStorage auth.storeGraphCoolCred(result.data.authenticateUser); // Redirect to profile page this.props.history.push('/profile'); }); } render() { // Show a loading text while the app validates the user return <div>Loading...</div>; } } // Mutation query const AUTH_MUTATION = gql` mutation authMutation($accessToken: String!) { authenticateUser(accessToken: $accessToken) { id token } } `;
これは、Auth
によって公開されているhandleAuthentication
メソッドを使用して、Graphcoolサーバーにミューテーションを送信します。 サーバーへの接続はまだ設定されていませんが、設定してユーザーの認証を試みると、このコールバックページは書き込みミューテーションをGraphcoolサーバーに送信して、ユーザーが存在し、リソースへのアクセスが許可されていることをサーバーに通知します。
ステップ5—Apolloのセットアップとサーバーへの接続
コールバックコンポーネントでは、graphql
(まだインストールされていません)を使用して、コンポーネントをミューテーションに接続しました。 これは、Graphcoolサーバーへの接続がまだあることを意味するものではありません。 Apolloを使用してこの接続を設定してから、アプリのトップレベルでApolloインスタンスを提供する必要があります。
必要な依存関係のインストールから始めます。
yarn add apollo-client-preset react-apollo graphql-tag graphql
src/index.js
エントリファイルを更新します。
import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import './index.css'; import App from './App'; import registerServiceWorker from './registerServiceWorker'; // Import modules import { ApolloProvider } from 'react-apollo'; import { ApolloClient } from 'apollo-client'; import { HttpLink } from 'apollo-link-http'; import { InMemoryCache } from 'apollo-cache-inmemory'; // Create connection link const httpLink = new HttpLink({ uri: '[SIMPLE API URL]' }); // Configure client with link const client = new ApolloClient({ link: httpLink, cache: new InMemoryCache() }); // Render App component with Apollo provider ReactDOM.render( <BrowserRouter> <ApolloProvider client={client}> <App /> </ApolloProvider> </BrowserRouter>, document.getElementById('root') ); registerServiceWorker();
まず、これはすべての依存関係をインポートします。 次に、HttpLink
を使用してリンクを作成しました。 渡される引数は、URIを持つオブジェクトです。 サーバーフォルダで次のコマンドを実行すると、サーバーのURIを取得できます。
graphcool-framework list
シンプルURIを使用して、上記のコードのプレースホルダーを置き換えます。
次に、このリンクとキャッシュを使用してApolloクライアントインスタンスを作成および構成しました。 この作成されたクライアントインスタンスは、App
コンポーネントをラップするApolloプロバイダーにpropとして渡されます。
ステップ6—認証フローのテスト
すべてが無傷であるため、ナビゲーションバーのボタンにイベントを追加して、認証プロセスをトリガーできます。
//... import Auth from '../services/auth' const auth = new Auth(); const Nav = () => { return ( <nav className="navbar"> ... <div className="navbar-menu"> <div className="navbar-end"> ... <div className="navbar-item join" onClick={() => {auth.login()}}> Join </div> </div> </div> </nav> ); }; export default Nav;
[参加]ボタンをクリックすると、auth.login
メソッドがトリガーされ、認証のためにAuth0ドメインにリダイレクトされます。
ユーザーがログインした後、Auth0はユーザーが自分のプロファイル情報にアクセスするかどうかを尋ねます。
認証後、認証が成功した場合は、ページが/callback
および/profile
に戻るのを確認してください。
Graphcoolダッシュボードのデータビューに移動し、Usersテーブルを開くことで、ユーザーが作成されたことを確認することもできます。
ステップ7—条件付きボタンの追加
次に、ユーザーが認証されたときにlogin
ボタンを非表示にし、代わりにログアウトボタンを表示します。 nav.js
に戻り、Join
ボタン要素を次の条件付きロジックに置き換えます。
{auth.isAuthenticated() ? ( <div className="navbar-item join" onClick={() => { auth.logout(); }} > Logout </div> ) : ( <div className="navbar-item join" onClick={() => { auth.login(); }} > Join </div> )}
Auth
サービスは、isAuthenticated
というメソッドを公開して、トークンがlocalStorageに格納されており、有効期限が切れていないかどうかを確認します。 その後、ユーザーがログインします。
ステップ8—ユーザーのプロファイルを表示する
Authサービスを使用して、ログインしているユーザープロファイルを取得することもできます。 このプロファイルは、localStorageですでに利用可能です。
//... import Auth from '../services/auth'; const auth = new Auth(); const Profile = props => ( <div> //... <h2 className="title">Nickname: {auth.getProfile().nickname}</h2> </div> ); export default Profile;
ニックネームはブラウザに出力されます。
ステップ9—Graphcoolエンドポイントの保護
現時点では、トークンについてサーバーに通知していないため、認証されたユーザーは制限されたバックエンドに引き続きアクセスできます。
リクエストのヘッダーでBearer
トークンとしてトークンを送信できます。 index.js
を次のように更新します。
//... import { ApolloLink } from 'apollo-client-preset' const httpLink = new HttpLink({ uri: '[SIMPLE URL]' }); const middlewareAuthLink = new ApolloLink((operation, forward) => { const token = localStorage.getItem('do_auth_gcool_token') const authorizationHeader = token ? `Bearer ${token}` : null operation.setContext({ headers: { authorization: authorizationHeader } }) return forward(operation) }) const httpLinkWithAuthToken = middlewareAuthLink.concat(httpLink)
HTTPリンクだけでApolloクライアントを作成する代わりに、作成したミドルウェアを使用するように更新します。これにより、作成したすべてのサーバー要求にトークンが追加されます。
const client = new ApolloClient({ link: httpLinkWithAuthToken, cache: new InMemoryCache() })
その後、サーバー側のトークンを使用してリクエストを検証できます。
ステップ10—ルートを保護する
プロジェクトの最も重要な部分であるサーバーとそのデータを保護することで素晴らしい仕事をした限り、コンテンツがないルートにユーザーをぶら下げたままにしておくのは意味がありません。 /profile
ルートは、ユーザーが認証されていないときにアクセスされないように保護する必要があります。
App.js
を更新して、ユーザーがプロファイルにアクセスしたが認証されていない場合にホームページにリダイレクトするようにします。
//... import { Switch, Route, Redirect } from 'react-router-dom'; //... import Auth from './services/auth'; const auth = new Auth(); class App extends Component { render() { return ( <div className="App"> <Switch> <Route exact path="/" component={Home} /> <Route exact path="/callback" component={Callback} /> <Route exact path="/about" component={About} /> <Route exact path="/profile" render={props => auth.isAuthenticated() ? ( <Profile /> ) : ( <Redirect to={{ pathname: '/' }} /> ) } /> </Switch> </div> ); } } export default App;
まだauth.isAuthenticated
を使用して認証を確認しています。 trueを返す場合は、/profile
が優先され、そうでない場合は、/
が優先されます。
結論
このチュートリアルでは、GraphQLプロジェクトでAuth0を使用してユーザーを認証しました。 できることは、Auth0ダッシュボードに移動し、Twitterなどのソーシャル認証オプションをさらにいくつか追加することです。 Twitter開発者のWebサイトから取得できるTwitter開発者の資格情報の入力を求められます。