Firebase Authenticationでメールアドレス&パスワード認証に次いで面倒な電話番号認証について。
Firebaseの電話番号認証フロー
電話番号認証を有効にしたりするところは他の認証と変わらないので省略。
以下はUIを自前で実装する場合のもの。
共通
- RecaptchaVerifier インスタンスを作成してreCAPTCHAを表示
電話番号の入力フォームも表示 - 電話番号を入力
- reCAPTCHAを入力
サインイン
- Auth.signInWithPhoneNumberで確認コードを送信
→携帯の番号ではなかったり、bot判定されるなどしたらエラー
→エラーならreCATPCHAリセット - 確認コード入力フォーム表示
- 確認コードを入力
- ①で返されたConfirmationResultオブジェクトのconfirmメソッドで確認コードを検証
- ④が成功すると、ユーザーは正常にログインする
既存アカウントにリンク
- PhoneAuthProvider.verifyPhoneNumber で確認コード送信
→携帯の番号ではなかったり、bot判定されるなどしたらエラー
→エラーならreCATPCHAリセット - 確認コード入力フォーム表示
- 確認コードを入力
- PhoneAuthProvider.credential で資格情報を作成
①成功時に返されるverificationIdと③の確認コードが必要 - User.linkWithCredentialで既存アカウントにリンクする
(Auth.signInWithCredentialでサインインもできる)
reCAPTCHA ウィジェットを使用する
reCAPTCHAは「私はロボットではありません」でおなじみのあれ。
firebaseにはRecaptchaVerifierクラスがあるのでこれを利用する。
入れ物になる要素を用意しておく。
<div id="recaptcha-container"></div>
RecaptchaVerifierクラスのインスタンスを作成する。
オプションは公式のパラメーター一覧を参照で。
const recaptchaVerifier = new firebase.auth.RecaptchaVerifier( "recaptcha-container", { // UIの色 theme: 'light', // UIの大きさ size: 'normal', // タブインデックス tabindex: 0, // ユーザーが正常な応答を送信したときに実行される callback: responseToken => { }, //reCAPTCHA応答が期限切れになり、ユーザーが再検証する必要があるときに実行される "expired-callback": () => { }, // reCAPTCHAでエラー(通常はネットワーク接続)が発生し、接続が復元されるまで続行できない場合に実行される "error-callback": () => { } } );
インスタンスを作ったらrender()を実行すれば表示される。
// async function() const recaptchaWidgetId = await recaptchaVerifier.render();
電話番号に確認コードを送信する
signInWithPhoneNumber
Auth.signInWithPhoneNumberメソッドに、
ユーザーが入力した電話番号と、RecaptchaVerifierのインスタンスを渡す。
これでSMSで確認コードが送信される。
// async function() try { const confirmationResult = await firebase.auth().signInWithPhoneNumber(phoneNumber, recaptchaVerifier) } catch (e) { //reCAPTCHAリセット window.grecaptcha.reset(recaptchaWidgetId); }
送信が成功するとあとで使うConfirmationResultが返される。
verifyPhoneNumber
電話番号の認証プロバイダを作成して、
const provider = new firebase.auth.PhoneAuthProvider();
verifyPhoneNumberメソッドに、
ユーザーが入力した電話番号と、RecaptchaVerifierのインスタンスを渡す。
これでSMSで確認コードが送信される。
// async function() const verificationId = await provider.verifyPhoneNumber( phoneNumber, recaptchaVerifier );
無事送信が完了すると資格情報作成に必要なIDが発行されるので保存しておく。
reCATPCHAの非表示とエラー
確認コード送信したからとreCATPCHAを非表示にした時に
reCATPCHAのDOM要素を削除するのが早いとエラーが出る。
reCAPTCHA client element has been removed
verifyPhoneNumberの結果が戻るまではreCATPCHAのDOMは残しておくこと。
電話番号でサインインする
Auth.signInWithPhoneNumberが返したConfirmationResultのconfirmメソッドに、
ユーザーが入力した確認コードを渡す。
// async function code { user } = await confirmationResult.confirm(verificationCode);
成功するとユーザーの情報とかが帰ってきて、ログイン完了となる。
電話番号認証を既存アカウントにリンクする
資格情報を作成する
PhoneAuthProvider.credentialにverifyPhoneNumberメソッドが返すIDと、ユーザーが入力した確認コードを渡す。
const phoneCredential = firebase.auth.PhoneAuthProvider.credential( verificationId, verificationCode );
返された資格情報をUser.linkWithCredentialに渡す。
// async function const { user } = await firebase .auth() .currentUser.linkWithCredential(phoneCredential);
認証済みのユーザーのアカウントに電話番号認証がリンクされ、サインイン等で利用できるようになる。
電話番号のチェック
Auth.signInWithPhoneNumberやPhoneAuthProvider.verifyPhoneNumberは、送信時にその電話番号がモバイルかどうかを確認してくれる。
モバイルの番号じゃなかったらエラーを返すが、入力時にチェックしたいじゃん…?
あと電話番号は国コード付きじゃないとダメなんだけど、自動で変換したいじゃん…?
そこでawesome-phonenumberモジュールを使う。
これはGoogleのlibphonenumberをプリコンパイルしたもので、TypeScriptにも対応している。
以下はverifyPhoneNumber
に組み込んだサンプル。
const provider = new firebase.auth.PhoneAuthProvider(); const pn = new this.AWPhoneNum(phoneNumber, "JP"); if (!pn.isValid()) { console.log("無効な電話番号です") } if (!pn.isMobile()) { console.log("携帯端末の番号ではありません") } const verificationId = await provider.verifyPhoneNumber( pn.getNumber(), recaptchaVerifier );
FirebaseUIには組み込まれてるので、カスタムUIで電話番号認証したいという場合に必要。
FirebaseUIの利用
説明通りやればいいので…
// async function const firebaseui = await require("firebaseui"); const ui = new firebaseui.auth.AuthUI(firebase.auth()); ui.start('#firebaseui-auth-container', { signInFlow: "popup", callbacks: { signInSuccessWithAuthResult: ( { user, isNewUser, credential }, redirectUrl ) => { // userセットしたりリダイレクトしたりする }, signInFailure: error => { console.error(error); // ログイン失敗したときの処理 }, uiShown: () => { //ローディングを非表示にするなど } }, signInOptions: [ { provider: firebase.auth.PhoneAuthProvider.PROVIDER_ID, defaultCountry: "JP", whitelistedCountries: ["JP", "+81"] } ], tosUrl: "https://domain.name/tos", privacyPolicyUrl: "https://domain.name/privacy" });
FirebaseUIでは電話番号を既存のアカウントにリンクする処理はできない。
ホワイトリストの利用
確認コード送信が10回超えるとreCAPTCHAが厳しくなり、30回超えたくらいでエラーで送信出来なくなる。
実際それだけSMSを受け取れば通信量も取られるわけなので、テスト中はホワイトリストに登録された架空の番号を使うのが良い。
設定手順は公式ドキュメントにある。
多要素認証になり得るのか
正式にはサポートされてないけど、雰囲気は出来るかな…。
- ログインするための認証を限定する
メール、Google、Twitter、Facebook、Github、Microsoft、Yahoo! - ログイン済みのユーザーに電話番号認証を求める
ユーザーが設定済みの認証プロバイダはUser.providerDataに全部あるので、
これに電話番号があるかをチェックする。
// Twitter認証+電話番号認証 const is2FactorAuthed = user.providerData.filter(p => [ firebase.auth.TwitterAuthProvider.PROVIDER_ID, firebase.auth.PhoneAuthProvider.PROVIDER_ID ].includes(p.providerId) ).length === 2
Nuxt.jsのデモアプリで挙動試せます