ReactでAzure AD B2Cのアクセストークンを付与して送信する【axios】

ReactでAzure AD B2Cのアクセストークンを付与して送信する【axios】
◆ Live配信スケジュール ◆
サイオステクノロジーでは、Microsoft MVPの武井による「わかりみの深いシリーズ」など、定期的なLive配信を行っています。
⇒ 詳細スケジュールはこちらから
⇒ 見逃してしまった方はYoutubeチャンネルをご覧ください
【5/21開催】Azure OpenAI ServiceによるRAG実装ガイドを公開しました
生成AIを活用したユースケースで最も一番熱いと言われているRAGの実装ガイドを公開しました。そのガイドの紹介をおこなうイベントです!!
https://tech-lab.connpass.com/event/315703/

今回は、失敗から学んだ方法について解説していきます。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の通信時には認証済みの状態であると仮定して話を進めていきます。

実装に必要な知識としては、以下になります。

それでは実際のコード解説に入っていきます。

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との対話を大切にする必要を感じました。ひとまず問題解決できたことに、胸をなでおろしました。これからも問題を一つ一つ潰してよいエンジニアを目指していきたいです。

それではまた~

 

 

アバター画像
About 龍:Ryu 107 Articles
2022年入社で主にフロントエンドの業務でTailwindと遊ぶ日々。お酒とうまいご飯が好きで、運動がちょっと嫌いなエンジニアです。しゃべれるエンジニアを目指しておしゃべりとブログ執筆に注力中(業務もね)//
ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。


ご覧いただきありがとうございます。
ブログの最新情報はSNSでも発信しております。
ぜひTwitterのフォロー&Facebookページにいいねをお願い致します!



>> 雑誌等の執筆依頼を受付しております。
   ご希望の方はお気軽にお問い合わせください!

Be the first to comment

Leave a Reply

Your email address will not be published.


*


質問はこちら 閉じる