透過 JavaScript 使用 Apple 驗證

您可根據 Firebase SDK 執行端對端 OAuth 2.0 登入流程,讓使用者以 Apple ID 向 Firebase 進行驗證。

事前準備

如要使用 Apple 登入使用者,請先在 Apple 的開發人員網站上設定「使用 Apple 帳戶登入」,然後啟用 Apple 做為 Firebase 專案的登入供應商。

加入 Apple Developer Program

只有 Apple Developer Program 成員才能設定「使用 Apple 登入」。

設定「使用 Apple 登入」功能

Apple Developer 網站上執行下列操作:

  1. 按照「 為網頁版設定使用 Apple 帳戶登入」中第一節的說明,為網站與應用程式建立關聯。系統提示時,請將下列網址註冊為傳回網址:

    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler

    您可以在 Firebase 控制台的設定頁面取得 Firebase 專案 ID。

    完成後,請記下新的服務 ID,您在下一節將會用到。

  2. 建立「使用 Apple 私密金鑰登入」。在下一節中,您將需要新的私密金鑰和金鑰 ID。
  3. 如果您使用的 Firebase 驗證功能會將電子郵件傳送給使用者 (包括電子郵件連結登入、電子郵件地址驗證、帳戶變更撤銷等),請 設定 Apple 私人電子郵件轉發服務並註冊 noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com (或您的自訂電子郵件範本網域),讓 Apple 能將 Firebase 驗證傳送的電子郵件轉發到已去識別化的 Apple 電子郵件地址。

啟用 Apple 做為登入服務供應商

  1. 將 Firebase 新增至您的專案
  2. Firebase 控制台開啟「驗證」專區。在「登入方式」分頁中啟用「Apple」供應商。指定您在上一節建立的服務 ID。此外,請在「OAuth 程式碼流程設定」一節中,指定您的 Apple 團隊 ID,以及您在上一節建立的私密金鑰和金鑰 ID。

遵守 Apple 的去識別化資料規定

「使用 Apple 帳戶登入」可讓使用者在登入時,選擇將自己的資料 (包括電子郵件地址) 去識別化。選擇這個選項的使用者都有 privaterelay.appleid.com 網域的電子郵件地址。在應用程式中使用「使用 Apple 帳戶登入」功能時,您必須遵守 Apple 針對這些匿名化 Apple ID 提供的任何適用的開發人員政策或條款。

包括先取得任何必要的使用者同意聲明,再將任何直接識別個人資訊與去識別化的 Apple ID 建立關聯。使用 Firebase 驗證時,這可能包括下列動作:

  • 將電子郵件地址連結至去識別化的 Apple ID。
  • 將電話號碼與去識別化的 Apple ID 連結
  • 將非匿名社會憑證 (Facebook、Google 等) 連結至去識別化的 Apple ID,反之亦然。

請注意,上方清單僅列出部分示例。請在開發人員帳戶的「會員資格」部分中參閱《Apple 開發人員計畫授權協議》,確保您的應用程式符合 Apple 的規定。

使用 Firebase SDK 處理登入流程

如要建構網頁應用程式,使用 Apple 帳戶透過 Firebase 驗證使用者最簡單的方式,就是透過 Firebase JavaScript SDK 處理整個登入流程。

如要使用 Firebase JavaScript SDK 處理登入流程,請按照下列步驟操作:

  1. 使用對應的供應商 ID apple.com 建立 OAuthProvider 的執行個體。

    網頁模組 API

    import { OAuthProvider } from "firebase/auth";
    
    const provider = new OAuthProvider('apple.com');

    網路命名空間 API

    var provider = new firebase.auth.OAuthProvider('apple.com');
  2. 選用:指定其他 OAuth 2.0 範圍,指定您向驗證供應商要求的預設範圍之外。

    網頁模組 API

    provider.addScope('email');
    provider.addScope('name');

    網路命名空間 API

    provider.addScope('email');
    provider.addScope('name');

    根據預設,當「每個電子郵件地址一個帳戶」啟用時,Firebase 會要求電子郵件和名稱範圍。如果您將這項設定變更為「每個電子郵件地址有多個帳戶」,除非您指定,否則 Firebase 不會向 Apple 要求任何範圍。

  3. 選用:如要使用英文以外的語言顯示 Apple 的登入畫面,請設定 locale 參數。如要瞭解支援的語言代碼,請參閱使用 Apple 帳戶登入說明文件

    網頁模組 API

    provider.setCustomParameters({
      // Localize the Apple authentication screen in French.
      locale: 'fr'
    });

    網路命名空間 API

    provider.setCustomParameters({
      // Localize the Apple authentication screen in French.
      locale: 'fr'
    });
  4. 使用 OAuth 提供者物件向 Firebase 進行驗證。您可以開啟彈出式視窗或重新導向至登入頁面,提示使用者登入 Apple 帳戶。建議在行動裝置上使用重新導向方法。

    • 如要透過彈出式視窗登入,請呼叫 signInWithPopup()

      網頁模組 API

      import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth";
      
      const auth = getAuth();
      signInWithPopup(auth, provider)
        .then((result) => {
          // The signed-in user info.
          const user = result.user;
      
          // Apple credential
          const credential = OAuthProvider.credentialFromResult(result);
          const accessToken = credential.accessToken;
          const idToken = credential.idToken;
      
          // IdP data available using getAdditionalUserInfo(result)
          // ...
        })
        .catch((error) => {
          // Handle Errors here.
          const errorCode = error.code;
          const errorMessage = error.message;
          // The email of the user's account used.
          const email = error.customData.email;
          // The credential that was used.
          const credential = OAuthProvider.credentialFromError(error);
      
          // ...
        });

      網路命名空間 API

      firebase
        .auth()
        .signInWithPopup(provider)
        .then((result) => {
          /** @type {firebase.auth.OAuthCredential} */
          var credential = result.credential;
      
          // The signed-in user info.
          var user = result.user;
      
          // You can also get the Apple OAuth Access and ID Tokens.
          var accessToken = credential.accessToken;
          var idToken = credential.idToken;
      
          // IdP data available using getAdditionalUserInfo(result)
        // ...
        })
        .catch((error) => {
          // Handle Errors here.
          var errorCode = error.code;
          var errorMessage = error.message;
          // The email of the user's account used.
          var email = error.email;
          // The firebase.auth.AuthCredential type that was used.
          var credential = error.credential;
      
          // ...
        });
    • 如要重新導向至登入頁面來登入,請呼叫 signInWithRedirect()

    使用 signInWithRedirectlinkWithRedirectreauthenticateWithRedirect 時,請遵循最佳做法

    網頁模組 API

    import { getAuth, signInWithRedirect } from "firebase/auth";
    
    const auth = getAuth();
    signInWithRedirect(auth, provider);

    網路命名空間 API

    firebase.auth().signInWithRedirect(provider);

    使用者完成登入並返回頁面後,您可以呼叫 getRedirectResult() 來取得登入結果:

    網頁模組 API

    import { getAuth, getRedirectResult, OAuthProvider } from "firebase/auth";
    
    // Result from Redirect auth flow.
    const auth = getAuth();
    getRedirectResult(auth)
      .then((result) => {
        const credential = OAuthProvider.credentialFromResult(result);
        if (credential) {
          // You can also get the Apple OAuth Access and ID Tokens.
          const accessToken = credential.accessToken;
          const idToken = credential.idToken;
        }
        // The signed-in user info.
        const user = result.user;
      })
      .catch((error) => {
        // Handle Errors here.
        const errorCode = error.code;
        const errorMessage = error.message;
        // The email of the user's account used.
        const email = error.customData.email;
        // The credential that was used.
        const credential = OAuthProvider.credentialFromError(error);
    
        // ...
      });

    網路命名空間 API

    // Result from Redirect auth flow.
    firebase
      .auth()
      .getRedirectResult()
      .then((result) => {
        if (result.credential) {
          /** @type {firebase.auth.OAuthCredential} */
          var credential = result.credential;
    
          // You can get the Apple OAuth Access and ID Tokens.
          var accessToken = credential.accessToken;
          var idToken = credential.idToken;
    
          // IdP data available in result.additionalUserInfo.profile.
          // ...
        }
        // The signed-in user info.
        var user = result.user;
      })
      .catch((error) => {
        // Handle Errors here.
        var errorCode = error.code;
        var errorMessage = error.message;
        // The email of the user's account used.
        var email = error.email;
        // The firebase.auth.AuthCredential type that was used.
        var credential = error.credential;
    
        // ...
      });

    您也能在這裡找出並處理錯誤。如需錯誤代碼清單,請參閱 API 參考資料

    不同於 Firebase 驗證支援的其他供應商,Apple 不提供相片網址。

    此外,如果使用者選擇不與應用程式共用電子郵件地址,Apple 會為該使用者佈建專屬電子郵件地址 (採用 xyz@privaterelay.appleid.com 格式),並將該電子郵件地址與應用程式共用。如果您設定了私人電子郵件轉發服務,Apple 會將傳送至匿名電子郵件地址的電子郵件轉寄至使用者的實際電子郵件地址。

    Apple 只會在使用者首次登入時,與應用程式分享使用者資訊,例如顯示名稱。通常 Firebase 會在使用者首次使用 Apple 登入時儲存顯示名稱,您可以前往 firebase.auth().currentUser.displayName 取得這個名稱。不過,如果您先前曾透過 Apple 讓使用者在不使用 Firebase 的情況下登入應用程式,Apple 將不會提供 Firebase 使用者的顯示名稱。

重新驗證和帳戶連結

相同的模式可搭配 reauthenticateWithPopup()reauthenticateWithRedirect() 使用,以針對需要近期登入的敏感作業擷取新憑證:

網頁模組 API

import { getAuth, reauthenticateWithPopup, OAuthProvider } from "firebase/auth";

// Result from Redirect auth flow.
const auth = getAuth();
const provider = new OAuthProvider('apple.com');

reauthenticateWithPopup(auth.currentUser, provider)
  .then((result) => {
    // User is re-authenticated with fresh tokens minted and can perform
    // sensitive operations like account deletion, or updating their email
    // address or password.

    // The signed-in user info.
    const user = result.user;

    // You can also get the Apple OAuth Access and ID Tokens.
    const credential = OAuthProvider.credentialFromResult(result);
    const accessToken = credential.accessToken;
    const idToken = credential.idToken;

    // ...
  })
  .catch((error) => {
    // Handle Errors here.
    const errorCode = error.code;
    const errorMessage = error.message;
    // The email of the user's account used.
    const email = error.customData.email;
    // The credential that was used.
    const credential = OAuthProvider.credentialFromError(error);

    // ...
  });

網路命名空間 API

const provider = new firebase.auth.OAuthProvider('apple.com');

firebase
  .auth()
  .currentUser
  .reauthenticateWithPopup(provider)
  .then((result) => {
    // User is re-authenticated with fresh tokens minted and can perform
    // sensitive operations like account deletion, or updating their email
    // address or password.
    /** @type {firebase.auth.OAuthCredential} */
    var credential = result.credential;

    // The signed-in user info.
    var user = result.user;
     // You can also get the Apple OAuth Access and ID Tokens.
    var accessToken = credential.accessToken;
    var idToken = credential.idToken;

    // IdP data available in result.additionalUserInfo.profile.
      // ...
  })
  .catch((error) => {
    // Handle Errors here.
    var errorCode = error.code;
    var errorMessage = error.message;
    // The email of the user's account used.
    var email = error.email;
    // The firebase.auth.AuthCredential type that was used.
    var credential = error.credential;

    // ...
  });

此外,您也可以使用 linkWithPopup()linkWithRedirect(),將不同的識別資訊提供者連結至現有帳戶。

請注意,Apple 規定您必須先取得使用者的明確同意,才能將 Apple 帳戶連結至其他資料。

例如,如要將 Facebook 帳戶連結至目前的 Firebase 帳戶,請使用使用者登入 Facebook 時取得的存取權杖:

網頁模組 API

import { getAuth, linkWithPopup, FacebookAuthProvider } from "firebase/auth";

const auth = getAuth();
const provider = new FacebookAuthProvider();
provider.addScope('user_birthday');

// Assuming the current user is an Apple user linking a Facebook provider.
linkWithPopup(auth.currentUser, provider)
    .then((result) => {
      // Facebook credential is linked to the current Apple user.
      // ...

      // The user can now sign in to the same account
      // with either Apple or Facebook.
    })
    .catch((error) => {
      // Handle error.
    });

網路命名空間 API

const provider = new firebase.auth.FacebookAuthProvider();
provider.addScope('user_birthday');

// Assuming the current user is an Apple user linking a Facebook provider.
firebase.auth().currentUser.linkWithPopup(provider)
    .then((result) => {
      // Facebook credential is linked to the current Apple user.
      // Facebook additional data available in result.additionalUserInfo.profile,

      // Additional Facebook OAuth access token can also be retrieved.
      // result.credential.accessToken

      // The user can now sign in to the same account
      // with either Apple or Facebook.
    })
    .catch((error) => {
      // Handle error.
    });

在 Chrome 擴充功能中使用 Firebase 驗證

如要建構 Chrome 擴充功能應用程式,請參閱 螢幕外文件指南

請注意,您仍然必須透過 Apple 驗證自訂網域 (與預設 firebaseapp.com 網域類似):

http://auth.custom.example.com/.well-known/apple-developer-domain-association.txt

撤銷權杖

Apple 規定,如果應用程式支援建立帳戶,必須允許使用者在應用程式中自行刪除帳戶,如 App Store 審查指南所述。

為符合這項規定,請實作下列步驟:

  1. 請務必按照設定「使用 Apple 帳戶登入」一節所述,填寫「使用 Apple 登入」設定中的「服務 ID」和「OAuth 程式碼流程設定」部分。

  2. 當使用者透過 Apple 帳戶登入時,Firebase 不會儲存使用者權杖,因此您必須要求使用者重新登入,才能撤銷權杖及刪除帳戶。

    接著,從 OAuthCredential 取得 Apple OAuth 存取權杖,並使用該權杖呼叫 revokeAccessToken(auth, token) 來撤銷 Apple OAuth 存取權杖。

    const provider = new OAuthProvider('apple.com');
    provider.addScope('email');
    provider.addScope('name');
    
    const auth = getAuth();
    signInWithPopup(auth, provider).then(result => {
      // Get the Apple OAuth access token.
      const credential = OAuthProvider.credentialFromResult(result);
      const accessToken = credential.accessToken;
    
      // Revoke the Apple OAuth access token.
      revokeAccessToken(auth, accessToken)
        .then(() => {
          // Token revoked.
    
          // Delete the user account.
          // ...
        })
        .catch(error => {
          // An error happened.
          // ...
        });
    });
    
  3. 最後,刪除使用者帳戶 (及所有相關資料)。

進階:使用 Node.js 以 Firebase 進行驗證

如要在 Node.js 應用程式中使用 Firebase 進行驗證:

  1. 透過 Apple 帳戶登入使用者,並取得使用者的 Apple ID 權杖。您可以透過幾種方式達成此目的。舉例來說,如果您的 Node.js 應用程式有瀏覽器前端:

    1. 在後端產生隨機字串 (「nonce」),並計算其 SHA256 雜湊。Nonce 是一次性的使用值,可用來驗證後端與 Apple 驗證伺服器之間的單程行程。

      網頁模組 API

      const crypto = require("crypto");
      const string_decoder = require("string_decoder");
      
      // Generate a new random string for each sign-in
      const generateNonce = (length) => {
        const decoder = new string_decoder.StringDecoder("ascii");
        const buf = Buffer.alloc(length);
        let nonce = "";
        while (nonce.length < length) {
          crypto.randomFillSync(buf);
          nonce = decoder.write(buf);
        }
        return nonce.slice(0, length);
      };
      
      const unhashedNonce = generateNonce(10);
      
      // SHA256-hashed nonce in hex
      const hashedNonceHex = crypto.createHash('sha256')
        .update(unhashedNonce).digest().toString('hex');

      網路命名空間 API

      const crypto = require("crypto");
      const string_decoder = require("string_decoder");
      
      // Generate a new random string for each sign-in
      const generateNonce = function(length) {
        const decoder = new string_decoder.StringDecoder("ascii");
        const buf = Buffer.alloc(length);
        var nonce = "";
        while (nonce.length < length) {
          crypto.randomFillSync(buf);
          nonce = decoder.write(buf);
        }
        return nonce.slice(0, length);
      };
      
      const unhashedNonce = generateNonce(10);
      
      // SHA256-hashed nonce in hex
      const hashedNonceHex = crypto.createHash('sha256')
        .update(unhashedNonce).digest().toString('hex');
    2. 在登入頁面的「使用 Apple 登入」設定中,指定經過雜湊處理的 Nonce:

      <script src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
      <div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
      <script>
          AppleID.auth.init({
              clientId: YOUR_APPLE_CLIENT_ID,
              scope: 'name email',
              redirectURI: URL_TO_YOUR_REDIRECT_HANDLER,  // See the next step.
              state: '[STATE]',  // Optional value that Apple will send back to you
                                 // so you can return users to the same context after
                                 // they sign in.
              nonce: HASHED_NONCE  // The hashed nonce you generated in the previous step.
          });
      </script>
      
    3. 從 POSTed 驗證回應伺服器端取得 Apple ID 權杖:

      app.post('/redirect', (req, res) => {
        const savedState = req.cookies.__session;
        const code = req.body.code;
        const state = req.body.state;
        const appleIdToken = req.body.id_token;
        if (savedState !== state || !code) {
          res.status(403).send('403: Permission denied');
        } else {
          // Sign in with Firebase using appleIdToken. (See next step).
        }
      });
      

    另請參閱設定使用 Apple 登入的網頁

  2. 取得使用者的 Apple ID 權杖後,請使用這個權杖建構憑證物件,然後使用憑證登入使用者:

    網頁模組 API

    import { getAuth, signInWithCredential, OAuthProvider } from "firebase/auth";
    
    const auth = getAuth();
    
    // Build Firebase credential with the Apple ID token.
    const provider = new OAuthProvider('apple.com');
    const authCredential = provider.credential({
      idToken: appleIdToken,
      rawNonce: unhashedNonce,
    });
    
    // Sign in with credential form the Apple user.
    signInWithCredential(auth, authCredential)
      .then((result) => {
        // User signed in.
      })
      .catch((error) => {
        // An error occurred. If error.code == 'auth/missing-or-invalid-nonce',
        // make sure you're sending the SHA256-hashed nonce as a hex string
        // with your request to Apple.
        console.log(error);
      });

    網路命名空間 API

    // Build Firebase credential with the Apple ID token.
    const provider = new firebase.auth.OAuthProvider('apple.com');
    const authCredential = provider.credential({
      idToken: appleIdToken,
      rawNonce: unhashedNonce,
    });
    
    // Sign in with credential form the Apple user.
    firebase.auth().signInWithCredential(authCredential)
      .then((result) => {
        // User signed in.
      })
      .catch((error) => {
        // An error occurred. If error.code == 'auth/missing-or-invalid-nonce',
        // make sure you're sending the SHA256-hashed nonce as a hex string
        // with your request to Apple.
        console.log(error);
      });

後續步驟

使用者首次登入時,系統會建立新的使用者帳戶,並連結至憑證 (即使用者名稱與密碼、電話號碼或驗證提供者資訊),也就是使用者登入時使用的憑證。這個新帳戶會儲存在您的 Firebase 專案中,可用來識別專案中各個應用程式的使用者 (無論使用者登入方式為何)。

  • 如要在應用程式中瞭解使用者的驗證狀態,建議在 Auth 物件上設定觀察器。接著,您就能從 User 物件取得使用者的基本個人資料資訊。請參閱管理使用者一文。

  • 在 Firebase 即時資料庫和 Cloud Storage 安全性規則中,您可以透過 auth 變數取得登入使用者的專屬 ID,並使用該 ID 控管使用者可存取哪些資料。

您可以將驗證供應商憑證連結至現有的使用者帳戶,讓使用者透過多個驗證服務提供者登入您的應用程式。

如要登出使用者,請呼叫 signOut

網頁模組 API

import { getAuth, signOut } from "firebase/auth";

const auth = getAuth();
signOut(auth).then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});

網路命名空間 API

firebase.auth().signOut().then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});