序章

メール、Facebook、Google、Twitter、Githubはすべて、ウェブアプリでユーザーを認証するための可能なオプションです。 ReactとGraphQLで構築されたアプリは、そのような認証の候補でもあります。

この記事では、以下を使用してさまざまな認証プロバイダーをGraphQLアプリに追加する方法を学習します。

  • GraphQL :APIのクエリ言語
  • Graphcool :サービスとしてのGraphQLバックエンド
  • Auth0 :サービスとしての認証
  • React :ユーザーインターフェースを構築するためのJavaScriptライブラリ

また、ユーザーが認証サーバーによる認証に失敗した場合にReactルートがアクセスされないように保護する方法についても学習します。

ステップ1—プロジェクトの準備

計画は、Reactプロジェクトをセットアップすることです。 Reactプロジェクトが設定されているのと同じディレクトリで、Graphcoolサーバーを構成し、サーバーの動作をプロジェクトに指示できます。 インストールから始めます create-react-appgraphcool-framework CLIツール:

  1. npm install -g create-react-app graphcool-framework

次に、ReactCLIツールを使用して新しいReactアプリの足場を作成します。

  1. create-react-app do-auth

作成したReactアプリに移動し、Graphcool initコマンドを実行して、Graphcoolサーバーを作成します。

  1. cd do-auth
  1. 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ライブラリのインストールから始めます。

  1. 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)、使用したい Link からのコンポーネント react-router-dom ナビゲーションをプロビジョニングするには:

    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サーバーを作成します

  1. graphcool-framework deploy

ステップ3—サーバー認証用のAuth0の構成

クライアントアプリから少し離れて、前に作成したGraphcoolサーバーに戻りましょう。 Graphcoolには、サーバーの機能を拡張できるサーバーレス機能の概念があります。 この機能は、認証を含む多くのサードパーティ統合を実現するために使用できます。

このような統合の一部の関数は事前にパッケージ化されているため、最初から作成する必要はありません。 テンプレートをインストールし、いくつかの構成とタイプのコメントを外してから、必要に応じてコードを更新または微調整するだけです。

Auth0テンプレートを追加しましょう。 あなたがにいることを確認してください server フォルダを作成し、次を実行します。

  1. graphcool-framework add-template graphcool/templates/auth/auth0

これにより、 auth0 のフォルダ server/src. このフォルダーには、関数ロジック、タイプ、およびこの関数をトリガーするミューテーション定義の両方が含まれています。

次に、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_DOMAINAUTH0_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 この認証が行われるようにサーバーが作成されたときに生成されたタイプ User タイプはそれを置き換えることができます。

次に、認証ロジックを微調整する必要があります。 次のコードブロックを見つけます。

    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)
            }
          )

そして、更新します audience のプロパティ verify 方法 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にデプロイします。

  1. graphcool-framework

Graphcoolを初めて使用する場合は、Graphcoolアカウントを作成するためのページに移動する必要があります。

ステップ4—クライアント認証用のAuth0の構成

サーバーは、認証用のトークンを受信するように設定されています。 次のコマンドを実行して、Graphcoolプレイグラウンドでこれをテストできます。

  1. graphcool-framework playground

ミューテーションにAuth0トークンを提供し、Graphcoolからノードトークンを取得します。 Auth0からトークンを取得する方法を見てみましょう。

APIを作成するのと同じように、プロジェクトのクライアントも作成する必要があります。 クライアントは、ブラウザからの認証をトリガーするために使用されます。 Auth0ダッシュボードナビゲーションで、クライアントをクリックして、新しいクライアントを作成します。

アプリケーションタイプは、シングルページWebアプリケーションに設定する必要があります。これは、ルーティングされたReactアプリです。

authが開始されると、Auth0ドメインにリダイレクトされてユーザーを確認します。 ユーザーが確認されると、ユーザーをアプリにリダイレクトする必要があります。 コールバックURLは、リダイレクト後に戻る場所です。 作成したクライアントの設定タブに移動し、コールバックURLを設定します。

これで、Auth0ダッシュボードでのクライアント構成のセットアップは完了です。 次に実行したいのは、Reactアプリでいくつかのメソッドを公開するサービスを作成することです。 これらのメソッドは、認証のトリガー、Auth0からの応答の処理、ログアウトなどのユーティリティタスクを処理します。

まず、Auth0JSライブラリをインストールします。

  1. yarn add auth0-js

次に、を作成します services のフォルダ src. 追加します 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ハッシュを介してトークンを返します。 このメソッドは、このハッシュを読み取って渡します。
  • storeAuth0CredstoreGraphCoolCred 将来の使用のために、資格情報を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 を使用します Callback 作成する必要のあるコンポーネント src/container/Callback.js:

    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
        }
      }
    `;

これは handleAuthentication によって公開されたメソッド Auth Graphcoolサーバーにミューテーションを送信します。 サーバーへの接続はまだ設定されていませんが、設定してユーザーの認証を試みると、このコールバックページは書き込みミューテーションをGraphcoolサーバーに送信して、ユーザーが存在し、リソースへのアクセスが許可されていることをサーバーに通知します。

ステップ5—Apolloのセットアップとサーバーへの接続

コールバックコンポーネントでは、 graphql (まだインストールしていません)コンポーネントをミューテーションに接続します。 これは、Graphcoolサーバーへの接続がまだあることを意味するものではありません。 Apolloを使用してこの接続を設定してから、アプリのトップレベルでApolloインスタンスを提供する必要があります。

必要な依存関係のインストールから始めます。

  1. 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を取得できます。

  1. graphcool-framework list

シンプルURIを使用して、上記のコードのプレースホルダーを置き換えます。

次に、このリンクとキャッシュを使用してApolloクライアントインスタンスを作成および構成しました。 この作成されたクライアントインスタンスは、プロップとしてApolloプロバイダーに渡されます。Apolloプロバイダーは App 成分。

ステップ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>
      )}

The Auth servicesは、と呼ばれるメソッドを公開します 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—ルートを保護する

プロジェクトの最も重要な部分であるサーバーとそのデータを保護することで素晴らしい仕事をした限り、コンテンツがないルートにユーザーをぶら下げたままにしておくのは意味がありません。 The /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開発者の資格情報の入力を求められます。