React向けの新しいAuth0 SDKが登場してました

Auth0 React SDKが登場して, Reactで作成したSPAへの組み込みがより簡単になりました.
2020.07.07

認証や認可, ログイン処理はアプリケーション開発者の頭痛のタネであります.
Auth0はそれらに対する解決を提供するプラットフォームであると同時にアプリケーションを簡単にベストプラクティスにそわせてセキュアに設計するためのSDKも提供しています.

様々な種別のSDKを提供しているのですが, SPAでAuth0を利用する場合はAuth0 Single Page App SDKが提供されています. これはどのようなライブラリやフレームワークを利用したSPAであってもうまく動作します.
Auth0 React SDKはReact Custom HooksとHOCを提供しており, これを利用することでセキュアなアプリケーションがSPA SDKより少ないコードでかけます.

SPA SDKとの違い

React SDKは内部的にはSPA SDKを利用しており従来ユーザが設定していたReact Contextの設定やHooksの設定を吸収してよりシンプルにアプリケーションに組み込むこめるようにしています. なのでPKCEのサポートやキャッシュ...などといった今までSPA SDKで設定していた内容は問題なく利用できます.
なので違いと書くと少し語弊があるかもしれませんが, 実際にアプリケーション組み込みで必要な手順を見比べてみましょう.

React Context Provider の設定

SPA SDKであっても, React SDKでもReact Contextで状態を管理します.
ReactのルートコンポーネントをProviderでラップする例で見比べてみましょう.

SPA SDKを利用した場合をまずはみます.
React Context Providerでラップする...前にまずは作成します.

/src/react-auth0-spa.js

import React, { useContext, useEffect, useState  } from "react";
import createAuth0Client from "@auth0/auth0-spa-js";

const DEFAULT_REDIRECT_CALLBACK = () =>
  window.history.replaceState({}, document.title, window.location.pathname);

export const Auth0Context = React.createContext();
export const useAuth0 = () => useContext(Auth0Context);

export const Auth0Provider = ({
  children,
  onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
  ...initOptions
}) => {
  const [isAuthenticated, setIsAuthenticated] = useState();
  const [user, setUser] = useState();
  const [auth0Client, setAuth0] = useState();
  const [loading, setLoading] = useState(true);
  const [popupOpen, setPopupOpen] = useState(false);

  useEffect(() => {
    const initAuth0 = async () => {
      const auth0FromHook = await createAuth0Client(initOptions);
      setAuth0(auth0FromHook);

      if (window.location.search.includes('code=') && window.location.search.includes('state=')) {
        const {appState} = await auth0FromHook.handleRedirectCallback();
        onRedirectCallback(appState);
      }

      const isAuthenticated = await auth0FromHook.isAuthenticated();
      setIsAuthenticated(isAuthenticated);

      if (isAuthenticated) {
        const user = await auth0FromHook.getUser();
        setUser(user);
      }

      setLoading(false);
    };

    initAuth0();
    // eslint-disable-next-line
  }, [])

  /**
   * open '/auth' url and be logged in by user.
   * */
  const loginWithPopup = async (params= {}) => {
    setPopupOpen(true);
    try {
      await auth0Client.loginWithPopup(params);
    } catch (error) {
      console.error(error);
    } finally {
      setPopupOpen(false);
    }

    const user = await auth0Client.getUser();
    setUser(user);
    setIsAuthenticated(true);
  }

  /**
   * handling redirect callback
   */
  const handleRedirectCallback = async () => {
    setLoading(true);
    await auth0Client.handleRedirectCallback();
    const user = await auth0Client.getUser();
    setLoading(false);
    setUser(user);
    setIsAuthenticated(true);
  }

  return (
    <Auth0Context.Provider
      value={{
        isAuthenticated,
        user,
        loading,
        popupOpen,
        loginWithPopup,
        handleRedirectCallback,
        getIdTokenClaims: (...p) => auth0Client.getIdTokenClaims(...p),
        loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
        getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
        getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
        logout: (...p) => auth0Client.logout(...p)
      }}
    >
      {children}
    </Auth0Context.Provider>
  )
}

コードの中身はどのアプリケーションであっても殆ど共通しているのですが記述量はどうしても増えてしまいます.
詳細については書きませんが, 状態をアプリケーションでもつためにContextを設定したりログイン周りの設定, 認証されているかを確認する関数などなどを含めたProviderを作成しています.

次にルータでブラウザの履歴を操作するために, historyを作成します.

/src/utils/history.js

import {createBrowserHistory} from 'history';

export const history = createBrowserHistory();

そしてProviderができたのでアプリケーションをラップします.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

import { Auth0Provider } from './react-auth0-spa';
import config from './auth_config.json';
import { history } from './utils/history';

const onRedirectCallback = appState => {
  history.push(
    appState && appState.targetUrl
      ? appState.targetUrl
      : window.location.pathname
  );
};

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

これで設定が完了しました. 後はコンポーネントでContextを通じて操作を行い, ログインなどを実装できます.
確かにそこまで手間ではないのですが, コード量を減らしたい気持ちも事実です. 自分の気持ちには素直でいたいものですね.

つぎにReact SDKを利用したパターンをみていきます.
はじめにAuth0Providerをimportしてルートコンポーネントラップします.
終わりです. 先ほど書いてきたContext の設定は全て@auth0/auth0-reactが吸収しました.
ついでにリダイレクト時のcallbackの処理も減っています.

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Auth0Provider } from "@auth0/auth0-react";

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

Reactを利用したアプリケーションにAuth0を組み込もうと思いチュートリアルをみた時にこのコードをみてかなり混乱しました.
前はもう少し手順が多かったから何故減ったのか一瞬わかりませんでした.
ですが新しいSDKが波に乗りやってきて, その波でProviderの設定を隠蔽したと理解できました. 新しい時代がきたのですよ.

アプリケーションに組み込んでみる

Reactでアプリケーションができることを前提として書いていきます.
TailwindCSSやSASS, Webpackなど様々導入している環境で検証しているのですが, 本筋ではないので今回は触れません.

まずはライブラリをインストールしましょう.

yarn add @auth0/auth0-react

次にルートコンポーネントをProviderでラップしていきます.
DomainとClientIDは適宜置き換えてください.

import { Auth0Provider } from '@auth0/auth0-react';
import React from 'react';
import ReactDOM from 'react-dom';
import { Header } from './components/header';
import './global.scss';
import { Welcome } from './welcome';

function App() {
  return (
    <div className="w-screen h-screen">
      <Header />
      <Welcome />
    </div>
  );
}

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

ここまでできたらログインしたユーザ情報が見れる, 簡単な機能を実装していきましょう.
Welcomeコンポーネントを編集していきます. 新しいSDKを使うんだという気持ちと, 爽やかな風を感じつつ書いていきましょう.

src/welcome.tsx

import { useAuth0 } from '@auth0/auth0-react';
import React from 'react';
import { Button } from './components/button';
import { Title, SubTitle } from './components/title';

export function Welcome() {
  const { isAuthenticated, loginWithRedirect, user } = useAuth0();
  return (
    <main>
      <div className="container mx-auto flex flex-col items-start py-24">
        <Title>Testing React with Auth0 </Title>
        <div className="mt-3">
          <p>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
            eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
            ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
            aliquip ex ea commodo consequat. Duis aute irure dolor in
            reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
            pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
            culpa qui officia deserunt mollit anim id est laborum.
          </p>
        </div>

        <div className="flex  flex-col mt-10 items-start">
          <SubTitle>User Information</SubTitle>
          {isAuthenticated ? (
            <div className="h-full flex mt-4">
              <img className="w-16 h-16 rounded-full mr-4" src={user.picture} />
              <div className="flex-grow">
                <p className="text-gray-900">{user.nickname}</p>
                <p className="text-gray-500">{user.email}</p>
              </div>
            </div>
          ) : (
            <div className="mt-2">
              <div>
                <Button onClick={() => loginWithRedirect()}>
                  Login to see user info
                </Button>
              </div>
            </div>
          )}
        </div>
      </div>
    </main>
  );
}

自前で定義したコンポーネントについては説明を端折りますが大事なのは下記の部分です.
useAuth0を経由してユーザ認証が終わっている段階なのかの状態, ユーザ情報, ログイン画面への遷移とリダイレクトなどの処理実装を受け取ります.
あとは状態や関数を通じて実際のロジックを記載しているだけです.

export function Welcome() {
  const { isAuthenticated, loginWithRedirect, user } = useAuth0();
  return (
//...
}

私の場合はこの状態でWebpack DevServerを起動してみて, 下記のように表示されました.
ログインしていないので「Login to see user info」と表示されていますね.

img

そしてボタンをクリックするとAuth0のUniversal Loginの画面が表示されます.

img

Universal Loginでの処理が終了すると, ユーザ情報がちゃんと表示されていますね.

img

いったんここまででルーティングとかの処理はしませんが簡単にAuth0の力を授かることが確認できました.

さいごに

Auth0 React SDKはReactアプリケーション開発の負担を今まで以上に大きく減らします.
セキュリティに関する仕様を良く理解していることは重要ですが, 巨人の肩にのり簡単にセキュアなアプリケーションを実現できるのは今までにない体験です. みなさんも是非お試しください.
この記事がお役に立てたら幸いです.

参考資料