初めに
どもども!お疲れ様です。最近は業務で取り扱ったことをブログに投げつける日々な龍ちゃんです。このブログは前回の記事「Azure AD B2C 認証ページを作ってみる【React】」を拡充する内容になります。ReactでAzure AD B2Cを利用して認証する場合には、ポップアップ認証・リダイレクト認証・サイレントSSO認証などありますが、リダイレクト認証における挙動について触れています。
今回のブログでは、実際に僕が直面した問題をユーザーシナリオ(要望 or 問題)に落とし込んで紹介しています。取り扱うシナリオとしては以下を想定しています。
- 認証後にリロードしたら認証前の画面に戻る
- 認証後に特定の画面に遷移したい
それでは順に初めて行きます。
前提条件
説明に入る前に環境について説明を入れておきます。キーワードだけ欲しい人は、各項目の最後にリンクが置いてあるのでそちらを参照してください。ほとんどが英語なので翻訳してみてください。
今回想定しているルーティングは以下になります。「/azure」にはログイン・ログアウトのボタンがあり、「/azure/mypage」には認証を突破しないと入れないようにルーティングを構築しています。詳しい説明はこちらの記事を読んでもらえれば助かります。
このイメージでコーディングでのルーティング定義は以下の構成にしています。
import { Outlet, Route, Routes } from 'react-router';
import { BrowserRouter } from 'react-router-dom';
import { LinkPage } from '@/components/layout/LinksPage';
import { AzureADB2CPage } from '@/components/pages/AADB2C/AzureADPage';
import { MyPage as MypageAADB2C } from '@/components/pages/AADB2C/MyPage';
import {
RouterAuthenticatedCheck,
RouterHasAuthenticated,
} from './RouterAuthenticateConfigAADB2C';
export const RouterConfig = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LinkPage />}>
<Route index element={<>とりま動いたから良しとしようや</>} />
<Route
path="/azure"
element={<RouterAuthenticatedCheck component={<Outlet />} />}
>
<Route index element={<AzureADB2CPage />} />
<Route
path="mypage"
element={<RouterHasAuthenticated component={<MypageAADB2C />} />}
/>
</Route>
</Route>
</Routes>
</BrowserRouter>
);
};
内部ルーティングコンポーネントは以下の構成です。
import { InteractionStatus } from '@azure/msal-browser';
import { useAADB2CAuth } from '@/hooks/useAADB2CAuth';
import { RedirectComponent } from '@/utilities/RedirectComponent';
type Props = {
component: React.ReactNode;
};
export const RouterAuthenticatedCheck = (props: Props) => {
const { component } = props;
const { inProgress } = useAADB2CAuth();
if (inProgress != InteractionStatus.None)
return (
<div className="flex justify-center">
<div className="h-10 w-10 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" />
</div>
);
return <>{component}</>;
};
export const RouterHasAuthenticated = (props: Props) => {
const { component } = props;
const { isAuthenticated } = useAADB2CAuth();
if (!isAuthenticated) return <RedirectComponent redirect="/azure" />;
return <>{component}</>;
};
「RouterAuthenticatedCheck.tsx」では認証状態の監視のみを行ってルーティング判定は行っていません。「RouterHasAuthenticated.tsx」では認証状態の場合ではpropsで渡されたコンポーネントを表示しますが、未認証状態では「/azure」のパスにリダイレクトを行います。
認証後にリロードしたら認証前の画面に戻る
この問題の原因としては、「認証判定に使用している変数の変更にラグがある」が考えられます。Azureが提供している認証ライブラリであるmsal-reactでは、認証状態によって表示を切り替えることができるコンポーネントがあります( AuthenticatedTemplate・UnauthenticatedTemplate)。こちらを使用することで、ページ単位での画面のちらつきは抑制することができます。
ルーティングレベルで認証判定を行う際は、「useIsAuthenticated」を使用して以下のようなコードを使用するかと思います。
import { useIsAuthenticated} from '@azure/msal-react'; const isAuthenticated = useIsAuthenticated();
こちらもとても便利です。ですが、認証が通っている場合でもリロードしてしまうと、一度FALSEになりその後TRUEとなります。これがちらつきの原因です。こちらを解消する方法としては、「inProgress」を使用する方法が挙げられます。
import { useMsal } from '@azure/msal-react';
const { inProgress } = useMsal();
こちらのパラメータはアプリ認証の挙動を監視しています。試しに「inProgress」と「isAuthenticated」をuseEffectで調査してみました。認証済み状態でリロードを押した場合、以下のタイミングで値が変動します。
アプリが立ち上がってすぐの場合では「inProgres:startup」となっています。「inProgress:startup」中に認証チェックが終了し「isAuthenticated :true」となり、その後「inProgress:none」の値を取っています。
ログイン処理中では、以下のタイミングで値が変動します。
先ほどのリロード時に加えて「inProgress:handleRedirect」が追加されています。この場合では、「inProgress」と「isAuthenticated 」の判定にラグがあることがわかります。
とりあえずこの調査から、「inProgress」の値を監視して特定の値を取っている間は、ルーティング判定を止める必要があることがわかります。以降が改訂版のコードになります。
import { useEffect } from 'react';
import { InteractionStatus } from '@azure/msal-browser';
import { useAADB2CAuth } from '@/hooks/useAADB2CAuth';
import { RedirectComponent } from '@/utilities/RedirectComponent';
type Props = {
component: React.ReactNode;
};
export const RouterAuthenticatedCheck = (props: Props) => {
const { component } = props;
const { inProgress } = useAADB2CAuth();
if (
inProgress == InteractionStatus.Startup ||
inProgress == InteractionStatus.HandleRedirect
)
return <>認証判定中</>;
return <>{component}</>;
};
export const RouterHasAuthenticated = (props: Props) => {
const { component } = props;
const { isAuthenticated } = useAADB2CAuth();
if (!isAuthenticated) return <RedirectComponent redirect="/azure" />;
return <>{component}</>;
};
ちなみにReact SPAでAzure AD B2Cの認証を使用した場合だと、Login後に処理を実行したページに自動で戻ってきます。こちらの対応については次章の「認証後に特定の画面に遷移したい」で説明したいと思います。
この内容は、公式ではサラッと流れていたのでパラメータの参考になるサイトと使い方の参考になったサイトを置いておきます。
認証後に特定の画面に遷移したい
まず前提としては、React SPAでAzure AD B2Cの認証を使用した場合だと、Login後に処理を実行したページに自動で戻ってきます。ログインページとサービスページが一致している場合では問題ありません。ですが、ランディングページとサービスページの構成の場合だと、自動でサービスページに飛んでもらわないとUX的によろしくないですね。
これの対応として、Azure portal上で設定する「redirectURL」があります。ですが、僕が確認したところでは、Loginの処理を実行した場所に自動でリダイレクトされています。こちらを自分で設定したいですよね。その方法について説明していきます。
ざっくりとコードから行きます。設定するパラメータとしては「RedirectRequest」内の「redirectStartPage」と「onRedirectNavigate」になります。
import { RedirectRequest } from '@azure/msal-browser';
const { instance } = useMsal();
const loginAzure = useCallback(async () => {
const loginRequest: RedirectRequest = {
redirectStartPage: window.location.origin + '/azure/mypage',
onRedirectNavigate: () => true,
scopes: ['openid'],
};
instance.loginRedirect(loginRequest);
}, [instance]);
「redirectStartPage」は行きたいサイトのURLを渡します。「onRedirectNavigate」に関してはURLの変更許可を出すプロパティです(正直あまりわかっていません)。こちらの変数を渡すことでとりあえずページの移動は可能になります。
もしページにクエリパラメータを入れたい場合は、受け皿作って動的に生成できるようにしましょう。
const loginAzure = useCallback(async (file:string) => {
const loginRequest: RedirectRequest = {
redirectStartPage: window.location.origin + `/azure/mypage?file=${file}`,
onRedirectNavigate: () => true,
scopes: ['openid'],
};
instance.loginRedirect(loginRequest);
}, [instance]);
ただ、挙動としてはリダイレクトで帰ってきた後にさらにリダイレクトが走るという挙動になります。この辺りは海外でも言及があったようです。
参考にしたサイトをぶら下げておきます。
- Redirect to home page after React Azure AD login
- [Help wanted] Redirect flow + react-router
- MSAL double redirect #2514
終わりに
お疲れ様です。今回は、ReactでのAzure AD B2Cのリダイレクト認証後の挙動に対しての対処法についてまとめました。この辺りの情報は英語圏の情報しか出てこなかったです。久々にしっかりissueをにらめっこしてました。やはり情報はGitに落ちてますね。あとは、公式のリファレンスとVScodeのサジェストといろいろ見るところはありましたが、想定していた動きを作成できたのでよかったです。
このブログで誰かの疑問の助けになればうれしいです。
ではまた~