📝

React + Authentication で OpenIDConnect 認証を行う

2023/02/02に公開

こんにちは。安政です。

弊社では、ユーザ認証に Firebase の Authentication のパスワード認証を用いています。

OpenIDConnect を用いて、Auth0 を用いた SSO を実装する機会がありましたので、簡単にですが、共有したいと思います。

Firebase Authentication の設定

Authentication にて OpenIDConnect を有効にするための方法を記します。

  1. Authentication の 「Sign-in Method」を開く

  2. 「新しいプロバイダを追加」をクリック

  3. カスタムプロバイダ上の「OpenID Connect」を選択
    a. 初回は Authentication のアップグレードを行う必要がある

  4. OIDC プロバイダ情報を定義する
    a. 画像のような形で定義を行います。クライアント ID, 発行元, クライアントシークレットは認可サーバから定義されたものを適宜指定してください。

    b. プロバイダ ID は任意の値を設定できます。React 側で利用する必要があるので、メモしておきましょう。
    c. コールバック URL が発行されるので、認可サーバの方に設定します。

※ Auth0 を認可サーバとして用いる場合、設定した Application 内の以下の項目に設定を行う必要があります。

  • Allowed Callback URLs: 4-c で発行されたコールバック URL を記載します
  • Allowed Web Origins: 認証を行うシステムの URL を記載します

React で OIDC を用いたログイン処理を行う

今回は React のカスタムフックの形で、ログイン処理を実装しました。

サンプルコードとしてはこちらです。

import {
  getAuth,
  OAuthProvider,
  signInWithPopup,
  signOut as signOutFirebase,
  AuthError
} from 'firebase/auth'

export default function useOidcSignin(): [Result, () => Promise<Viewer>] {
  // 弊社のログイン処理
  const client = useApolloClient()
  const { setViewer } = useViewerContext()

  // Authentication の設定で指定したプロバイダID を指定
  const oidcProviderId = 'oidc.test-provider-id' 

  // OIDC を利用したプロバイダーの定義
  const provider = new OAuthProvider(oidcProviderId)
  const auth = getAuth()

  const [
    { loading, error, value: viewer },
    auth0Siginin
  ] = useAsyncFn(async (): Promise<Viewer> => {
    try {
            // このタイミングで、OpenIDConnect 認可先(Auth0)の認証ページが開く
      const result = await signInWithPopup(auth, provider)

            // Firebase での認証情報を取得
      const accessToken = await result.user.getIdToken()
      const userInfo = jwt_decode(accessToken)

	        // 外部からのデータ取得なので、TypeGuard を用いて意図したデータが取得されているかの判定
      if (!dataIsUserInfoData(userInfo)) {
        setErrorMessage('ユーザ情報の取得に失敗しました')
        throw new TypeError('ユーザ情報の取得に失敗しました')
      }

      // 認証後に何らかの処理を行う場合、ここに記載する
			
    } catch (e) {
      // ログイン処理等で失敗した場合、Firebase からログアウトする
      await signOutFirebase(auth)

      // 認可先(auth0)でエラーが発生した想定
      const error = e as AuthError
      if (error.code == 'auth/invalid-credential') {
        const responseRe = new RegExp(
          'response: (.*)(auth/invalid-credential).',
          'g'
        )
        const response = responseRe.exec(error.message)
        if (response) {
          window.location.href =
            process.env.ERROR_PAGE + '?' + response[1]
        }
      }

      throw new Error('問題が発生しました。しばらく時間を置いて再度お試しください。')
    }

    // ログインして viewer の状態が変化したので更新
    // KANNA の通常のログイン処理
    const viewer = await loadViewer(client)
    setViewer && setViewer(viewer)

    return viewer
  }, [])

  return [{ loading, error, viewer }, auth0Siginin]
}

// auth0 より受信したデータをパースしたデータを保持する
interface UserInfoResponse {
  email: string
  firebase: {
    // 認可先(Auth0)で指定されたデータを取得できる
    sign_in_attributes: {
      app_metadata: {
        organization_data: {
          id: string
          metadata: {
            company_name: string
          }
        }
      }
      user_metadata: {
        first_name: string
        last_name: strin
      }
    }
  }
}

// 必要なデータが取得できているかの TypeGuard
const dataIsUserInfoData = (data: unknown): data is UserInfoResponse => {
  const userInfo = data as UserInfoResponse

  if (userInfo.email === undefined) return false
  if (userInfo.firebase === undefined) return false
  if (userInfo.firebase.sign_in_attributes === undefined) return false
    
    // 以下は Auth0 向けの処理
  if (userInfo.firebase.sign_in_attributes.user_metadata === undefined)
    return false
  if (userInfo.firebase.sign_in_attributes.app_metadata === undefined)
    return false
  if (
    userInfo.firebase.sign_in_attributes.app_metadata.organization_data ===
    undefined
  )
    return false

  return true
}

大事な点としては、

// Authentication の設定で指定した プロバイダID を指定
const oidcProviderId = 'oidc.test-provider-id' 

// OIDC を利用したプロバイダーの定義
const provider = new OAuthProvider(oidcProviderId)
const auth = getAuth()

// このタイミングで、OpenIDConnect 認可先(Auth0)の認証ページが開く
const result = await signInWithPopup(auth, provider)

この辺りとなります。

Firebase による認証を素直に利用している場合は、getAuth の利用のみで済むかと思います。

弊社では、auth の変化を onAuthStateChanged を用いて監視することで、ログイン状態の管理を行なっています。

最後に

とても簡単にですが、サンプルコードをベースに参考になりましたら、幸いです。

アプリ(React-Native)での Authentication を用いた OIDC の処理はもう少し複雑であったり、ライブラリにバグがあったり(2022/11 時点)で苦戦しました。

また機会がありましたら、コード中心になりますが、共有できればと思います。

アルダグラム Tech Blog

Discussion