今回は、失敗から学んだ方法について解説していきます。axiosのインターセプタ―でAzure AD B2Cのアクセストークンを取得して、ヘッダーに挿入する方法についてです。useEffect内で非同期的にアクセストークンを取得ができなかったため、その対処を実装しました。Reactでの処理周りが難しいところです。
挨拶
ども!先々週から先週にかけて学びが多い週だったので、今週はアウトプットの週にしていきたい龍ちゃんです。約一年携わっていたプロジェクトが終了して、次期案件までの助走期間なのでアウトプットが捗りそうです。それでは本題に入っていきたいと思います。
この議題を始める起因となった記事は「フロントとバックエンドの接続時にアクセストークンを使用してセキュアに運用する」になります。一言で表すと「ログイン後のユーザーであれば、成り済まし可能なシステム」というちょっとしびれる状況だったのです。
これを解消する方法としては、以下の構成を実現する必要があります。
フロントエンドの対応としては、「Azure AD B2C」からアクセストークンを取得し、「APIの通信」時にHeaderにアクセストークンを付与するというところまでになります。
そもそもAzure AD B2Cでアクセストークンを取得できない人はこちらを参照してください。
それでは、本題に入っていこうと思います。
実装
まずは、前提として「Azure AD B2C」の認証を作成する必要があります。公式のページの手順に沿えば、さくっとインストールすることができます。
SPAを用いてAzure AD B2Cのログイン状態の管理方法は、「msal-react」を使用するのを公式もお勧めしています。ログインをさせる方法は、3パターンもあり用途によって選択する必要があります。私は、MsalAuthenticationTemplate
をよく利用します。何はともあれ、APIの通信時には認証済みの状態であると仮定して話を進めていきます。
実装に必要な知識としては、以下になります。
- Azure AD B2Cでログインしてなければ自動でログイン処理を実装する:MsalAuthenticationTemplate
- axiosを用いたAPI通信に共通処理を挟み込む:interceptor
- Azure AD B2Cからアクセストークンを取得する:acquireTokenSilent
それでは実際のコード解説に入っていきます。
Msal.js【React】を使用するための共通設定
【Msal.js】を利用するにあたって、絶対作成しなければならないコードが以下になります。接続先を示すファイルを設定ファイルとして切り出しています。ここの設定ファイルに関しては、以下のブログで解説しています。公式のガイドもわかりやすいのでおいておきます。
PublicClientApplication
を構築するため以外にも、アクセストークン取得用の情報も定義してあります。
import { Configuration } from '@azure/msal-browser';
export const AzureClientId: string = import.meta.env.VITE_AZURE_CLIENT_ID || '';
export const AzureScopes: string[] = [import.meta.env.VITE_AZURE_SCOPE || ''];
const AzureAuthority: string = import.meta.env.VITE_AZURE_AUTHORITY || '';
const AzureKnownAuthorities: string[] = [import.meta.env.VITE_AZURE_KNOWN_AUTHORITY || ''];
export const msalConfig: Configuration = {
auth: {
clientId: AzureClientId,
authority: AzureAuthority,
knownAuthorities: AzureKnownAuthorities,
redirectUri: window.location.origin,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
};
以下は、MsalAuthenticationTemplate
のログインの処理とaxiosのインターセプタ―定義を差し込む設定になります。MsalAuthenticationTemplate
の内部にaxiosのインターセプタ―定義があるため、axiosのインターセプタ―定義が行われる際には、ログイン済みの状態を担保することができます。Reactのレンダリング関係でエラーハンドリングは実装する必要があります。
import { InteractionType, PublicClientApplication } from '@azure/msal-browser';
import { MsalAuthenticationTemplate, MsalProvider } from '@azure/msal-react';
import { AzureScopes, msalConfig } from '@/constants/authAzure';
import { NormalErrorPage } from '@/pages/Error/NormalErrorPage';
import { LoadingComponent } from '@/pages/LoadingPage';
import { AxiosErrorHandlingComponent } from '@/utilities/AxiosConfig';
export const AzureConfigComponent = (props: { children: React.ReactNode }) => {
const { children } = props;
const pca = new PublicClientApplication(msalConfig);
return (
<MsalProvider instance={pca}>
<MsalAuthenticationTemplate
interactionType={InteractionType.Redirect}
errorComponent={NormalErrorPage}
authenticationRequest={{
scopes: AzureScopes,
}}
loadingComponent={LoadingComponent}
>
<AxiosErrorHandlingComponent>{children}</AxiosErrorHandlingComponent>
</MsalAuthenticationTemplate>
</MsalProvider>
);
};
Azure AD B2Cからアクセストークンを取得するHooks
ここでは、useMsal
を用いてアクセストークンを取得するHooksについて解説しています。
import { useCallback } from 'react';
import { useAccount, useMsal } from '@azure/msal-react';
import { AzureScopes } from '@/constants/authAzure';
export const useAzureAuth = () => {
const { instance, accounts } = useMsal();
const account = useAccount(accounts[0]) ?? undefined;
const authenticationResult = useCallback(async () => {
const result = await instance.acquireTokenSilent({
scopes: AzureScopes,
account: account,
});
return result;
}, [instance, account]);
return {
authenticationResult
};
};
acquireTokenSilent
はログイン済みの状態であれば、トークン情報を取得することができます。未ログイン状態の場合では、失敗してエラーが吐かれます。
axiosのヘッダーにアクセストークンを付与する
今までの設定を含めて、ヘッダーにアクセストークンを付与する方法について紹介していきます。リクエストのインターセプタ―は、非同期でアクセストークンを付与しています。
import { useEffect } from 'react';
import { useErrorBoundary } from 'react-error-boundary';
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { useAzureAuth } from '@/hooks/useAzureAuth';
export const axiosClient = axios.create({
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
type Props = {
children: React.ReactNode;
};
export const AxiosErrorHandlingComponent = (props: Props) => {
const { children } = props;
const { authenticationResult } = useAzureAuth();
const requestInterceptor = axiosClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
try {
const result = await authenticationResult();
config.headers.Authorization = 'Bearer ' + result.accessToken;
return config;
} catch (e) {
return config;
}
});
const responseInterceptor = axiosClient.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
useEffect(() => {
return () => {
axiosClient.interceptors.response.eject(responseInterceptor);
axiosClient.interceptors.request.eject(requestInterceptor);
};
}, [requestInterceptor, responseInterceptor]);
return <>{children}</>;
};
機能的な部分として、useEffect
のクリーンナップでaxiosのインターセプタ―をeject
しています。これは、認証時に再レンダリングが実行されて古いインターセプタ―が機能する可能性を防止するためです。また、未ログイン時のエラーを防止するためにtry~catch
を挟み込んで、エラーハンドリングを行っています。
以降は失敗談になります。
useEffect
内で、非同期的にinterceptorの注入を行っていたました。そうすると、レンダリングの関係で、処理がこける問題が発生しました。useEffect
内で非同期で挟み込むことはリスクだと再認識されました。この更新とレンダリング更新がわかっていなかったことが原因であると思います。
今回は、AxiosErrorHandlingComponent
がレンダリングされるタイミングでは、認証が通っているという担保をMsalAuthenticationTemplate
にやってもらっている認識です。ですが、初期状態では認証が通っていない状態でもレンダリングが走っていました。そのため、クリーンナップ関数でinterceptorの開放を行っています。実際に処理は動きました。
でも、この内容がミスってたら教えてください。
終わりに
さて、今回は失敗から学んだ方法と対処について書いておきました。解決策はひとまず出たのですが、もっとReactとの対話を大切にする必要を感じました。ひとまず問題解決できたことに、胸をなでおろしました。これからも問題を一つ一つ潰してよいエンジニアを目指していきたいです。
それではまた~