Auth0 React SDKで保護されたルートを実現する

2020.07.08

Auth0 React SDKが登場して, Reactを利用した開発に大きな風が吹き荒れています. 主に私の中でです.
Auth0 React SDKの紹介は以前書きましたが, ルーティングについて触れていなかったので今回は保護されたルートの実現を試していきます.
ReactでSPA作っていることを前提として書いていきますので, Reactについてはあまり触れません.

TL;DL

結論だけをみたい方はここを参照にコードを書いていただければ幸いです.
この記事では保護されたルートはAuth0 React SDKが提供するwithAuthenticationRequiredとReact Router v5で実現していきます.
まず, 保護されたルートを下記のように定義します.

/src/protected.tsx

import { withAuthenticationRequired } from '@auth0/auth0-react';
import React from 'react';
import { Route } from 'react-router-dom';

export function ProtectedRoute({ component, ...args }) {
  return (
    <Route component={withAuthenticationRequired(component, {})} {...args} />
  );
}

後はRouterとSwitchで保護されたルートを作成します.
また匿名ユーザが保護されたルートにアクセスした場合に, Universal Login画面にリダイレクトされて認証が終わったら戻ってくる...といった実装のために「onRedirectCallback」を定義してAuth0Providerにわたします.

/src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { Auth0Provide } from '@auth0/auth0-react';
import { createBrowserHistory } from 'history';
import { Route, Router, Switch } from 'react-router-dom';

import { ProtectedRoute } from './protected';

export const history = createBrowserHistory();

const onRedirectCallback = async appState => {
  history.replace({
    pathname: appState?.returnTo || window.location.pathname,
    search: '',
  });
};

function App() {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={ROOT_COMPONENT_NAME} />
        <ProtectedRoute path="/secure" component={PROTECTED_COMPONENT_NAME} />
      </Switch>
    </Router>
  );
};

ReactDOM.render(
  <Auth0Provider
    domain="YOUR_DOMAIN"
    clientId="YOUR_CLIENT_ID"
    redirectUri={window.location.origin}
  >
    <App />
  </Auth0Provider>,
  document.querySelector('#root')
);

初期設定

React Router v5でルーティングをするので依存関係をインストールしていきます.

yarn add history react-router-dom @auth0/auth0-react
yarn add -D @types/history @types/react-router-dom

次にルートコンポーネントをRouterで囲みます.
特段特別なことはしていませんが, HeaderとWelcomeコンポーネントをすでに作成している状態で進めていきます.
コンポーネントについて少し説明します. ログインしていない状態であれば下記のような画面が表示されます.

img

ログインした状態であればユーザ情報が表示されます.

img

そしてコードの全容は下記のようになります.

/src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { Auth0Provider, useAuth0 } from '@auth0/auth0-react';
import { createBrowserHistory } from 'history';
import { Route, Router, Switch } from 'react-router-dom';

import { Header } from './components/header';
import { Welcome } from './welcome';
import './global.scss';

export const history = createBrowserHistory();

function App() {
  const { isLoading } = useAuth0();

  if (isLoading) {
    return <p></p>;
  }
  return (
    <Router history={history}>
      <div className="w-screen h-screen">
        <Header />
        <Switch>
          <Route path="/" exact component={Welcome} />
        </Switch>
      </div>
    </Router>
  );
};

ReactDOM.render(
  <Auth0Provider
    domain="YOUR_DOMAIN"
    clientId="YOUR_CLIENT_ID"
    redirectUri={window.location.origin}
  >
    <App />
  </Auth0Provider>,
  document.querySelector('#root')
);

ルーティングの部分を確認していきましょう.
ルートを保護していないので「/」には誰でもアクセスできます.

function App() {
  // ...
  return (
    <Router history={history}>
      <div className="w-screen h-screen">
        <Header />
        <Switch>
          <Route path="/" exact component={Welcome} />
        </Switch>
      </div>
    </Router>
  );
};

保護されたルートを作成する

次にUniversal Login画面でログインした後にのみアクセスできる保護されたルートを作成していきます.
Auth0 React SDKが提供するwithAuthenticationRequiredを利用して簡単にルートを保護します.
withAuthenticationRequiredはHOCを作成するものであり, 保護された対象コンポーネントにアクセスにアクセスしたときに認証ずみのユーザであればアクセスを許可し, そうでなければログイン画面にリダイレクトさせます.

/src/protected.tsx

import { withAuthenticationRequired } from '@auth0/auth0-react';
import React from 'react';
import { Route } from 'react-router-dom';

export function ProtectedRoute({ component, ...args }) {
  return (
    <Route component={withAuthenticationRequired(component, {})} {...args} />
  );
}

これでコンポーネントの保護を作成できるので, ルートに組み込みます.
Secureというコンポーネントを新たに追加してこのコンポーネントを保護していきます. まずはコードの全容を見てから, 細かい点を確認していきましょう.

/src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import { Auth0Provider, useAuth0 } from '@auth0/auth0-react';
import { createBrowserHistory } from 'history';
import { Route, Router, Switch } from 'react-router-dom';

import { ProtectedRoute } from './protected';

import { Header } from './components/header';
import { Secure } from './secure';
import { Welcome } from './welcome';
import './global.scss';

export const history = createBrowserHistory();

const onRedirectCallback = async appState => {
  history.replace({
    pathname: appState?.returnTo || window.location.pathname,
    search: '',
  });
};

function App() {
  const { isLoading } = useAuth0();

  if (isLoading) {
    return <p></p>;
  }
  return (
    <Router history={history}>
      <div className="w-screen h-screen">
        <Header />
        <Switch>
          <Route path="/" exact component={Welcome} />
          <ProtectedRoute path="/secure" component={Secure} />
        </Switch>
      </div>
    </Router>
  );
};

ReactDOM.render(
  <Auth0Provider
    domain="YOUR_DOMAIN"
    clientId="YOUR_CLIENT_ID"
    redirectUri={window.location.origin}
  >
    <App />
  </Auth0Provider>,
  document.querySelector('#root')
);

まずはonRedirectCallbackについてです.
カスタムルータを利用する場合, つまりReact Routerなどを利用してルーティングをする場合は自前で実装してAuth0Providerに渡します.

const onRedirectCallback = async appState => {
  history.replace({
    pathname: appState?.returnTo || window.location.pathname,
    search: '',
  });
};

例えば「/secure」に認証されていないユーザがアクセスした場合にログイン画面に遷移します.
認証が無事終わると「/secure」にアクセスできるようになります.
この時ログイン画面からのリダイレクト先が「/secure」にできるようにonRedirectCallbackを指定するのがイメージです.

なので実装的には下記の内容で用件は満たしていますが, リダイレクトされた後にURLパラメータにcodeとstateが残ってしまいます.
ユーザにcodeやstateが見える状態になるので不便です.

const onRedirectCallback = async appState => {
  history.replace(pathname: appState?.returnTo || window.location.pathname);
};

なのでhistory.replaceの引数をいい感じに変更して調整します.

const onRedirectCallback = async appState => {
  history.replace({
    pathname: appState?.returnTo || window.location.pathname,
    search: '',
  });
};

ルーティングの部分を見ていきましょう.
Switch配下に公開するコンポーネントは通常のRouteで, 保護したいコンポーネントはProtectedRouteを利用してRouteを定義していきます.

function App() {
  //...
  return (
    <Router history={history}>
      <div className="w-screen h-screen">
        <Header />
        <Switch>
          <Route path="/" exact component={Welcome} />
          <ProtectedRoute path="/secure" component={Secure} />
        </Switch>
      </div>
    </Router>
  );
}

これで実装が完了しました. 簡単ですね.

アクセスしてみる

リダイレクトの動作なので静止画を並べても何も分からないとは思いますが...表示していきましょう. ログインしていない状態でまずは/secureにアクセスしてみます.

img

保護されたルートに匿名ユーザでアクセスしたのでUniversal Loginの画面に遷移されます.
この時はもクライアントからstateなどが発行しており, 渡しています.

img

認証が終わり, /secureにリダイレクトされます.
この時何もクエリストリングに対する処理を入れていないとcodeやstateが露見しますが, 処理をいれているので/secureのみ表示されます.

img

Auth0 React SDKで簡単にルートを保護できました. 便利ですね.

さいごに

Auth0 React SDKが本当に便利で, 開発に大きなインパクトを与えると思っています.
この記事が役に立ちましたら幸いです.

参考