🐯
firebase v9のパスワードリセットとメールアドレス変更
v9系のパスワード変更とメールアドレス変更に詰まったので記事にする
環境
- firebase v9.x
- パスワード認証
- react-hook-form v7.x
- zod v3.x
- chakra v1.x
該当部分
パスワード変更
メールアドレス変更
ユーザー再認証
公式が丁寧だったり突き放されたりするのでつらい
何が問題なのか
アカウントの削除、メインのメールアドレスの設定、パスワードの変更といったセキュリティ上重要な操作を行うには、ユーザーが最近ログインしている必要があります。
重要: ユーザーのメールアドレスを設定するには、ユーザーが最近ログインしている必要があります。ユーザーの再認証をご覧ください。
重要: ユーザーのパスワードを設定するには、ユーザーが最近ログインしている必要があります。ユーザーの再認証をご覧ください。
!?
つまりパスワードやメールアドレスを変更するにはまず再認証する必要があるらしい
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を取得する
stackoverflowにv9でEmailAuthProviderからcredentialを取得する記事を発見😎
const credential = EmailAuthProvider.credential(
auth.currentUser.email,
userProvidedPassword
)
つまり下記の構造にしてその後処理をさせれば良さそう
- 現在のユーザーを取得する
(getAuth().currentUser)
- 現在のユーザーからメールアドレスを取得しセットする
(user?.email ?? '')
- パスワードは入力してもらう
(password)
- credentialを取得する
(EmailAuthProvider.credential)
- 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>
)
}
Discussion
"formとコアな部分だけ実装する"の、パスワード変更とメールアドレス変更の1個目のコードが、逆になっているかもしれません。