firebase v9のパスワードリセットとメールアドレス変更


v9系のパスワード変更とメールアドレス変更に詰まったので記事にする

環境

  • firebase v9.x
    • パスワード認証
  • react-hook-form v7.x
  • zod v3.x
  • chakra v1.x

該当部分

パスワード変更

https://firebase.google.com/docs/auth/web/manage-users?hl=ja#set_a_users_password

メールアドレス変更

https://firebase.google.com/docs/auth/web/manage-users?hl=ja#set_a_users_email_address

ユーザー再認証

https://firebase.google.com/docs/auth/web/manage-users?hl=ja#re-authenticate_a_user

公式が丁寧だったり突き放されたりするのでつらい

何が問題なのか

アカウントの削除、メインのメールアドレスの設定、パスワードの変更といったセキュリティ上重要な操作を行うには、ユーザーが最近ログインしている必要があります。

重要: ユーザーのメールアドレスを設定するには、ユーザーが最近ログインしている必要があります。ユーザーの再認証をご覧ください。

重要: ユーザーのパスワードを設定するには、ユーザーが最近ログインしている必要があります。ユーザーの再認証をご覧ください。



!?

つまりパスワードやメールアドレスを変更するにはまず再認証する必要があるらしい

re-authenticate_a_user
import { getAuth, reauthenticateWithCredential } from "firebase/auth";

const auth = getAuth();
const user = auth.currentUser;

// TODO(you): prompt the user to re-provide their sign-in credentials
const credential = promptForCredentials();

reauthenticateWithCredential(user, credential).then(() => {
  // User re-authenticated.
}).catch((error) => {
  // An error ocurred
  // ...
});



!?

TODOに書いてあるpromptForCredentials()が突如呼ばれてcredentialをどこから取得すればいいのかわからない😂

reauthenticateWithCredentialに投げるcredentialを取得する

https://stackoverflow.com/questions/37811684/how-to-create-credential-object-needed-by-firebase-web-user-reauthenticatewith
stackoverflowにv9でEmailAuthProviderからcredentialを取得する記事を発見😎

const credential = EmailAuthProvider.credential(
    auth.currentUser.email,
    userProvidedPassword
)

つまり下記の構造にしてその後処理をさせれば良さそう

  1. 現在のユーザーを取得する(getAuth().currentUser)
  2. 現在のユーザーからメールアドレスを取得しセットする(user?.email ?? '')
  3. パスワードは入力してもらう(password)
  4. credentialを取得する(EmailAuthProvider.credential)
  5. 1と4で取得したユーザーとcredentialを使用し再認証させる(reauthenticateWithCredential)
import { FirebaseError } from '@firebase/util'
import {
  EmailAuthProvider,
  getAuth,
  reauthenticateWithCredential,
} from 'firebase/auth'

(async () => {
  const user = getAuth().currentUser
  try {
    const credential = await EmailAuthProvider.credential(
      user?.email ?? '', 
      password
    )
    user && (await reauthenticateWithCredential(user, credential))
    //メールアドレス、パスワードリセットの処理
  } catch (e) {
    if (e instanceof FirebaseError) {
      console.error(e)
    }
  }
})()

ということで

formとコアな部分だけ実装する

パスワード変更

import { FirebaseError } from '@firebase/util'
import { zodResolver } from '@hookform/resolvers/zod'
import {
  EmailAuthProvider,
  getAuth,
  reauthenticateWithCredential,
  updateEmail,
} from 'firebase/auth'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

/**
 * 簡易バリデーション
 */
const schema = z.object({
  newEmail: z.string().email(),
  password: z.string().min(6),
})

type UpdateEmailInput = z.infer<typeof schema>

/**
 * @see https://firebase.google.com/docs/auth/web/manage-users?hl=ja#set_a_users_email_address
 * @see https://firebase.google.com/docs/auth/web/manage-users?hl=ja#re-authenticate_a_user
 * @see https://stackoverflow.com/questions/37811684/how-to-create-credential-object-needed-by-firebase-web-user-reauthenticatewith
 * firebaseのメールアドレスを変更をする関数
 * 成功
 * 1. EmailAuthProvider.credential(firebaseにログインさせcredentialを取得する)
 * 2. reauthenticateWithCredential(credentialを使いfirebaseを再認証させる)
 * 3. updateEmail(パスワードを再設定する)
 * 失敗
 * 1. firebaseのエラーを吐く
 */
export const useUpdateEmail = () => {
  const form = useForm<UpdateEmailInput>({
    defaultValues: {
      newEmail: '',
      password: '',
    },
    resolver: zodResolver(schema),
  })

  const onUpdateEmail = form.handleSubmit(async ({ newEmail, password }) => {
    const user = getAuth().currentUser
    try {
      const credential = await EmailAuthProvider.credential(
        user?.email ?? '',
        password
      )
      user && (await reauthenticateWithCredential(user, credential))
      user && (await updateEmail(user, newEmail))
      form.setValue('newEmail', '')
      form.setValue('password', '')
    } catch (e) {
      form.setValue('password', '')
      if (e instanceof FirebaseError) {
        console.error(e)
      }
    }
  })
  return {
    form,
    onUpdateEmail,
  }
}
const UpdatePassword = () => {
  const {
    form: { register },
    onUpdatePassword,
  } = useUpdatePassword()

  return (
    <Box borderWidth={1} p={4}>
      <Heading size={'md'}>パスワード変更</Heading>
      <FormControl>
        <Input placeholder={'今のPW'} {...register('password')} />
      </FormControl>
      <FormControl >
        <Input placeholder={'新しいPW'} {...register('newPassword')} />
      </FormControl>
      <FormControl >
        <Input placeholder={'新しいPW確認'} {...register('newPasswordConfirm')} />
      </FormControl>
      <Button onClick={onUpdatePassword}>
        パスワード変更
      </Button>
    </Box>
  )
}

メールアドレス変更

import { FirebaseError } from '@firebase/util'
import { zodResolver } from '@hookform/resolvers/zod'
import {
  EmailAuthProvider,
  getAuth,
  reauthenticateWithCredential,
  updatePassword,
} from 'firebase/auth'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

/**
 * 簡易バリデーション
 */
const schema = z
  .object({
    newPassword: z.string().min(6),
    newPasswordConfirm: z.string().min(6),
    password: z.string().min(6),
  })
  .refine((data) => data.newPassword === data.newPasswordConfirm, {
    message: "Passwords don't match",
    path: ['newPasswordConfirm'],
  })

type UpdatePasswordInput = z.infer<typeof schema>

/**
 * @see https://firebase.google.com/docs/auth/web/manage-users?hl=ja#set_a_users_password
 * @see https://firebase.google.com/docs/auth/web/manage-users?hl=ja#re-authenticate_a_user
 * @see https://stackoverflow.com/questions/37811684/how-to-create-credential-object-needed-by-firebase-web-user-reauthenticatewith
 * firebaseのパスワードを変更をする関数
 * 成功
 * 1. EmailAuthProvider.credential(firebaseにログインさせcredentialを取得する)
 * 2. reauthenticateWithCredential(credentialを使いfirebaseを再認証させる)
 * 3. updatePassword(パスワードを再設定する)
 * 失敗
 * 1. firebaseのエラーを吐く
 */
export const useUpdatePassword = () => {
  const form = useForm<UpdatePasswordInput>({
    defaultValues: {
      newPassword: '',
      newPasswordConfirm: '',
      password: '',
    },
    resolver: zodResolver(schema),
  })

  const onUpdatePassword = form.handleSubmit(
    async ({ newPassword, password }) => {
      const user = getAuth().currentUser
      try {
        const credential = await EmailAuthProvider.credential(
          user?.email ?? '',
          password
        )
        user && (await reauthenticateWithCredential(user, credential))
        user && (await updatePassword(user, newPassword))
      } catch (e) {
        if (e instanceof FirebaseError) {
          console.error(e)
        }
      } finally {
        form.setValue('password', '')
        form.setValue('newPassword', '')
        form.setValue('newPasswordConfirm', '')
      }
    }
  )
  return {
    form,
    onUpdatePassword,
  }
}
const UpdateEmail = () => {
  const {
    form: { register },
    onUpdateEmail,
  } = useUpdateEmail()
  return (
    <Box borderWidth={1} p={4}>
      <Heading size={'md'}>メールアドレス変更</Heading>
      <FormControl>
        <Input placeholder={'今のPW'} {...register('password')} />
      </FormControl>
      <FormControl>
        <Input placeholder={'新しいメアド'} type={'email'} {...register('newEmail')} />
      </FormControl>
      <Button onClick={onUpdateEmail}>
        メールアドレス変更
      </Button>
    </Box>
  )
}

最後に

validation、エラーハンドリング、loadingなどをお忘れなく!!

おまけ

パスワードリセット

import { FirebaseError } from '@firebase/util'
import { zodResolver } from '@hookform/resolvers/zod'
import { getAuth, sendPasswordResetEmail } from 'firebase/auth'
import { useForm } from 'react-hook-form'
import { z } from 'zod'

/**
 * 簡易バリデーション
 */
const schema = z.object({
  email: z.string().email(),
})

type SendPasswordResetEmailInput = z.infer<typeof schema>


/**
 * @see https://firebase.google.com/docs/auth/web/manage-users?hl=ja#send_a_password_reset_email
 * firebaseのパスワードリセットをする関数
 * 成功
 * 1. 指定したメールアドレスにパスワードをリセットするメールを送信
 * 失敗
 * 1. firebaseのエラーを吐く
 */
export const useSendPasswordResetEmail = () => {
  const form = useForm<SendPasswordResetEmailInput>({
    defaultValues: {
      email: '',
    },
    resolver: zodResolver(schema),
  })

  const onSendPasswordResetEmail = form.handleSubmit(async ({ email }) => {
    try {
      await sendPasswordResetEmail(getAuth(), email)
      form.setValue('email', '')
    } catch (e) {
      if (e instanceof FirebaseError) {
        console.error(e)
      }
    }
  })
  return {
    form,
    onSendPasswordResetEmail,
  }
}
const SendPasswordResetEmail = () => {
  const {
    form: { register },
    onSendPasswordResetEmail,
  } = useSendPasswordResetEmail()
  return (
    <Box borderWidth={1} p={4}>
      <Heading size={'md'}>パスワードリセット</Heading>
      <FormControl>
        <Input placeholder={'リセットするユーザーのメアド'} {...register('email')} />
      </FormControl>
      <Button onClick={onSendPasswordResetEmail}>
        リセット
      </Button>
    </Box>
  )
}