透過 JavaScript 使用 Apple 驗證

您可以使用 Firebase SDK 執行端對端 OAuth 2.0 登入流程,讓使用者透過 Apple ID 向 Firebase 驗證身分。

事前準備

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

加入 Apple 開發人員計畫

只有 Apple 開發人員計畫的成員可以設定「使用 Apple 帳戶登入」。

設定「使用 Apple 登入」功能

Apple 開發人員網站上執行下列操作:

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

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

    您可以在 Firebase 控制台的「 設定」>「一般」分頁標籤中,找到 Firebase 專案 ID。

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

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

啟用 Apple 做為登入供應商

  1. 將 Firebase 新增至專案
  2. Firebase 控制台中,依序前往「安全性」>「驗證」
  3. 在「Sign-in method」(登入方式) 分頁中,啟用「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 處理登入流程

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

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

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

    Web

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

    Web

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

    Web

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

    Web

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

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

  3. 選用:如要以英文以外的語言顯示 Apple 登入畫面,請設定 locale 參數。如需支援的語言代碼,請參閱「透過 Apple 登入」文件。

    Web

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

    Web

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

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

      Web

      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);
      
          // ...
        });

      Web

      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 時,請遵循最佳做法

    Web

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

    Web

    firebase.auth().signInWithRedirect(provider);

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

    Web

    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);
    
        // ...
      });

    Web

    // 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 Auth 支援的其他供應商不同,Apple 不提供相片網址。

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

    Apple 只會在使用者首次登入時,與應用程式分享顯示名稱等使用者資訊。通常,Firebase 會在使用者首次透過 Apple 登入時儲存顯示名稱,您可以透過 firebase.auth().currentUser.displayName 取得該名稱。不過,如果您先前未使用 Firebase,而是透過 Apple 登入應用程式,Apple 就不會向 Firebase 提供使用者的顯示名稱。

重新驗證及連結帳戶

您也可以將相同模式用於 reauthenticateWithPopup()reauthenticateWithRedirect(),以便擷取新憑證,用於需要最近登入的敏感作業:

Web

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);

    // ...
  });

Web

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 的使用者取得的存取權杖:

Web

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.
    });

Web

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

權杖撤銷

根據 App Store 審查指南,Apple 規定應用程式若設有帳戶建立機制,就必須讓使用者能從應用程式內啟動帳戶刪除程序。

如要符合這項規定,請按照下列步驟操作:

  1. 請確認您已填寫「使用 Apple 帳戶登入」供應商設定的「服務 ID」和「OAuth 代碼流程設定」部分,如「設定使用 Apple 帳戶登入」一節所述。

  2. 由於 Firebase 不會儲存透過「使用 Apple 登入」建立的使用者權杖,因此您必須要求使用者再次登入,才能撤銷權杖並刪除帳戶。

    然後從 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. 在後端產生隨機字串 (即「隨機數」),並計算其 SHA256 雜湊值。隨機數是一次性值,用於驗證後端與 Apple 驗證伺服器之間的單一往返行程。

      Web

      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');

      Web

      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 帳戶登入」設定中指定雜湊的隨機值:

      <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. 從伺服器端 POST 的驗證回應中取得 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 權杖後,請使用該權杖建構 Credential 物件,然後使用憑證登入使用者:

    Web

    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);
      });

    Web

    // 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 Realtime DatabaseCloud Storage 安全規則中,您可以從 auth 變數取得已登入使用者的專屬使用者 ID, 並使用該 ID 控制使用者可存取的資料。

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

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

Web

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

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

Web

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