Closed29

Firebase Authentication メモ

mythosilmythosil

Firebaseの初期化。
スタートガイドを読みつつ進める。

import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: ...,
  authDomain: ...,
  projectId: ...,
  storageBucket: ...,
  messagingSenderId: ...,
  appId: ...,
};

initializeApp(firebaseConfig);
mythosilmythosil

メアド/パスワードでユーザー作成。
作成したユーザーはFirebase Console上でみれる。

const auth = getAuth();
await createUserWithEmailAndPassword(auth, email, password);
mythosilmythosil

ユーザー情報を表示する。

function useFirebase() {
  const [user, setUser] = useState<User | null>(null);
  useEffect(() => {
    const auth = getAuth();
    auth.onAuthStateChanged(setUser);
  }, []);
  return {
    user,
  };
}

function User() {
  const { user } = useFirebase();
  if (!user) {
    return null;
  }
  return (
    <div>
      <pre>
        <code>{JSON.stringify(user.toJSON(), null, 2)}</code>
      </pre>
    </div>
  );
}

auth.currentUser でもユーザー情報にアクセスできるが、ログイン済みなのに null が入っているケースがあることに注意が必要。onAuthStateChanged を使うことで問題を回避できる。
https://firebase.google.com/docs/auth/web/manage-users#get_the_currently_signed-in_user

Note: currentUser might also be null because the auth object has not finished initializing.

mythosilmythosil

onAuthStateChanged のコールバックが呼ばれるタイミングについて。
FirebaseがGoogle Cloud Identity Platformのaccounts.lookup APIをコールしていて、そのレスポンスが返ってくるまではコールバックが呼ばれないように見える。
accounts.lookup APIのレスポンスタイムが400-500msかかってる。これは要注意。

mythosilmythosil

サインアウトする。

const auth = getAuth();
await signOut(auth);
mythosilmythosil

メアド/パスワードでサインイン。

const auth = getAuth();
await signInWithEmailAndPassword(auth, email, password);
mythosilmythosil

Facebookでサインイン。

const auth = getAuth();
const provider = new FacebookAuthProvider();
signInWithRedirect(auth, provider);

なお、Facebookでサインインする場合に、ユーザーが未作成の場合は自動で作成される。AppleやGoogleでサインインした場合も同様。

また、リダイレクト方式ではなくポップアップ方式でFacebookサインインすることもできる。
signInWithPopupを使う。
一部のブラウザではポップアップが禁止されており、signInWithPopup が期待通りに動かない場合がある。アプリ内ブラウザでよく起きる。signInWithRedirect を使っておくのが無難そう。

mythosilmythosil

Facebookサインイン時、リダイレクトで戻ってきた時に結果を取得することが可能。
getRedirectResultを使う。
新規ユーザーとして作成されたかどうか等の情報を得ることができる。

mythosilmythosil

メアドでもFacebookでも、どちらでユーザー作成しても emailVerified はfalseになる。
trueを求めるかどうかは要件次第だが、trueにするには実際にメールを送ってみる手段をとる。

const auth = getAuth();
if (!auth.currentUser) {
  return;
}
await sendEmailVerification(auth.currentUser);

なお、sendEmailVerificationは同一ユーザーへの連続送信に制限がかかる。前回送信から1分経過していない場合は auth/too-many-requests エラーとなる。

mythosilmythosil

いったんクライアント側を離れて、Admin SDKを使ってユーザー情報を取得するやり方を試してみる。firebase-adminパッケージを使う。調べたい対象のユーザーのUIDかメアドさえ分かれば情報を取得できる。

const { initializeApp, cert, getApp } = require('firebase-admin/app');
const { getAuth } = require('firebase-admin/auth');
const serviceAccount = require('./xxxxx-firebase-adminsdk-xxxxx.json');

initializeApp({
  credential: cert(serviceAccount)
});

const app = getApp();
const auth = getAuth(app);

async function main() {
  const uid = '...';
  const user = await auth.getUser(uid);
  console.log(user);
}

main()
  .catch(console.error);
mythosilmythosil

メアド変更。即時に完了し、 emailVerified はfalseになる。

const auth = getAuth();
if (!auth.currentUser) {
  return;
}
await updateEmail(auth.currentUser, email);

変更前のメアドに対して注意喚起メールが送信される。「本当にメアド変更するのか?他人が勝手にやったりしてないか?」という主旨のもの。
メール文面内にあるURLを開くとメアド変更が取り消されて変更前のメアドに戻る。 emailVerified はtrueになる。

Your sign-in email for project-xxxxx was changed to new@example.com.

If you didn’t ask to change your email, follow this link to reset your sign-in email.

<メアド変更を取り消すためのURL>

Thanks,

Your project-xxxxx team

メアド変更を取り消すと、画面に "change your password right away" というリンクが表示される。
そのリンクをクリックすると、パスワードリセットのためのメールが送られてくる。

Follow this link to reset your project-xxxxx password for your hoge@example.com account.

<パスワードリセットのためのURL>

If you didn’t ask to reset your password, you can ignore this email.

Thanks,

Your project-xxxxx team

このパスワードリセットがかなり曲者。
パスワードリセットを実行してしまうと、プロバイダが password に強制的に切り替わる。
Facebookログインで使っていたのにFacebookログインできなくなる、ということが発生する。

mythosilmythosil

updateEmail 呼び出し時に auth/requires-recent-login のエラーが出る場合がある。その場合は再ログインが必要。
https://firebase.google.com/docs/reference/js/auth#updateemail

Important: this is a security sensitive operation that requires the user to have recently signed in. If this requirement isn't met, ask the user to authenticate again and then call reauthenticateWithCredential().

mythosilmythosil

パスワードリセットでプロバイダが password に切り替わる挙動について。
Stackoverflowに背景の説明があった。
https://stackoverflow.com/a/44694017/3808025

ただ、この説明がイマイチしっくりこない。

This behavior allows the user to recover an account in case it was hijacked and modified by another unverified user.

ここでいう "hijacked" がどういう状態か分からないし、どうやって "recover" するのかも分からない。

以下、推測。
"hijacked" はメールボックスの乗っ取りなど、パスワードリセットメールを盗聴できる状態を指している。
Aliceが email=M1, provider=facebook.com なアカウントを持っているとして、MalloryがメアドM1宛にパスワードリセットメールを送信する。Malloryはそのメールを盗聴できるのでパスワードリセットを実行でき、Aliceのアカウントにログインできるようになる。
もしunlinkが自動で行われないとすると、Aliceはアカウント乗っ取りに気づけない。unlinkが自動で行われていれば気づくことができ、何らかの措置を取れる。

これだと "recover" の説明がイマイチ。やっぱりよく分からない。

mythosilmythosil

Facebookログインのアカウントに password プロバイダをリンクする。
参考:https://firebase.google.com/docs/auth/web/account-linking

const auth = getAuth();
if (!auth.currentUser) {
  return null;
}
const credential = EmailAuthProvider.credential(email, password);
await linkWithCredential(auth.currentUser, credential);

リンク先のアカウントと異なるメアドを指定した場合、変更前のメアドに対して注意喚起メールが送信される。メアド変更と同様の振る舞い。

Your sign-in email for project-xxxxx was changed to new@example.com.

If you didn’t ask to change your email, follow this link to reset your sign-in email.

<メアド変更を取り消すためのURL>

メアド変更取り消しのURLを開くと、元のメアドに戻る。リンクされた状態は維持。
メアド変更を取り消すと、画面に "change your password right away" というリンクが表示される。これもメアド変更の時と同様の振る舞い。そして、パスワードリセットするとFacebookログインできなくなるのも同じ。厄介。

mythosilmythosil

password プロバイダのアカウントに facebook.com プロバイダをリンクする。

const auth = getAuth();
if (!auth.currentUser) {
  return;
}
const provider = new FacebookAuthProvider();
linkWithRedirect(auth.currentUser, provider);

linkWithCredential とは異なり、メアドが違っても変更されない。シンプルに facebook.com プロバイダがリンクされるだけ。

mythosilmythosil

パスワード変更。

const auth = getAuth();
if (!auth.currentUser) {
  return;
}
await updatePassword(auth.currentUser, password);

facebook.com プロバイダのみのユーザーがパスワード変更すると、password プロバイダが自動でリンクされる。

mythosilmythosil

Admin SDKでパスワード変更。Client SDKと同様に、facebook.com プロバイダのみのユーザーのパスワード変更をすると、password プロバイダが自動でリンクされる。

await auth.updateUser(uid, {
  password,
});
mythosilmythosil

Admin SDKで password プロバイダをリンクしようとしてもうまくいかない。
https://github.com/firebase/firebase-admin-node/issues/1592

password プロバイダをリンクする」というのはつまり「パスワードを設定する」ことであり、シンプルにパスワード変更すれば良いということ。password プロバイダがリンクされていなければ自動でリンクされる。

mythosilmythosil

facebook.com プロバイダのユーザーがFacebook側のメアドを変更するとどうなるか。
Firebase Auth側のメアドは古いまま。まあ当然か。

mythosilmythosil

Admin SDKでメアド変更。

await auth.updateUser(uid, {
  email,
});

Client SDKでメアド変更すると、変更前のメアドに対して通知メールが送信される。Admin SDKの場合はそれがない。
また、Client SDKでのメアド変更では、emailVerified がfalseに戻るが、Admin SDKの場合は元の値のまま。falseに戻す必要があれば updateUser メソッドの第2引数のプロパティに emailVerified: false を追加すればよい。

なお、Admin SDKでメアド変更やパスワード変更を行うと、ユーザーはログアウトされる。
ログイン状態を維持する方法はあるんだろうか?下記Issueを見る限りなさそうに思える。
https://github.com/firebase/firebase-admin-node/issues/882

mythosilmythosil

メアドがないFacebookアカウントでログインするとどうなるか。
ユーザー作成・ログインは問題なくできる。emailがnullになる。
Firebase Consoleの画面上はIDが - として表示される。

mythosilmythosil

Facebookアカウントでログインする際に、メアドの提供を許可しなかったらどうなるか。
ユーザー作成・ログインは問題なくできる。emailがnullになる。
Firebase Consoleの画面上はIDが - として表示される。

mythosilmythosil

任意のメアドのログイン方法(=プロバイダ)を知る方法。未ログイン状態でも使える。

const auth = getAuth();
const result = await fetchSignInMethodsForEmail(auth, email);

この関数の利用シーンはパスワードリセット時。
パスワードリセット画面でメアド入力された時に、パスワードリセットしても良いかどうかの判定に使える。
Facebookログインなのにパスワードリセットすると、password プロバイダに切り替わってしまい、それ以降はFacebookログインできなくなってしまう。
Facebookログインの場合はパスワードリセットが不要であるから、fetchSignInMethodsForEmail の返り値が ['facebook.com'] だった場合は「Facebookでのサインインをお試しください」的なメッセージを出せばいい。

そのメアドがFacebookログインであることがわかってしまうのは良いのか、という観点も考慮する必要あり。
個人的には、上述のようなメッセージを出すことではリスクは変わらないと考えている。悪意を持った人間はConsoleで直接コードを打ち込めばいいだけ。

mythosilmythosil

メアドがないFacebookアカウントにパスワードを設定したらどうなるか。
Client SDKでパスワードを設定しても何も変わらない。password providerはリンクされない。
Admin SDKでも同様だが、強制的にログアウトされる。

mythosilmythosil

ユーザーをFirebase Authenticationに取り込む方法(Admin SDK)。Firebase Authenticationがサポートしているハッシュアルゴリズムであれば可能。
https://firebase.google.com/docs/auth/admin/import-users

パスワードがbcryptでハッシュ化されたユーザーを取り込む例:

const passwordHash = await bcrypt.hash(passwordRaw, hashRounds);
await auth.importUsers(
  [
    {
      uid,
      email,
      passwordHash: Buffer.from(passwordHash),
    }
  ],
  {
    hash: {
      algorithm: 'BCRYPT',
    },
  },
);

uid の値はなんでも良い。他のユーザーと衝突しない限り。
Firebaseのuid生成アルゴリズムは公開されていないが、168bitのランダムなバイナリをBase64エンコードしたものっぽく見える。

mythosilmythosil

複数ドメインでID Tokenを使い回せるかどうか。
ID Tokenを別ドメインに持っていってsignInWithCustomTokenでログイン済にできるかと思ったが、auth/invalid-custom-token エラーが発生してしまった。
ドキュメント通りにちゃんとCustomTokenを作らないとダメそう。

このスクラップは2023/08/24にクローズされました