在 Android 上使用 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 新增至您的 Android 專案。在 Firebase 控制台設定應用程式時,請務必註冊應用程式的 SHA-1 簽名。
  2. Firebase 控制台,開啟「Auth」(驗證) 區段。在「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 處理登入流程

在 Android 上,透過 Firebase 驗證使用者最簡單的方式,就是使用 Firebase Android SDK 處理整個登入流程。

如要透過 Firebase Android SDK 處理登入流程,請按照下列步驟操作:

  1. 使用提供者 ID apple.com 來建構 OAuthProvider 的執行個體:

    Kotlin+KTX

    val provider = OAuthProvider.newBuilder("apple.com")
    

    Java

    OAuthProvider.Builder provider = OAuthProvider.newBuilder("apple.com");
    
  2. 選用:指定您向驗證供應商要求的預設範圍以外的其他 OAuth 2.0 範圍。

    Kotlin+KTX

    provider.setScopes(arrayOf("email", "name"))
    

    Java

    List<String> scopes =
        new ArrayList<String>() {
          {
            add("email");
            add("name");
          }
        };
    provider.setScopes(scopes);
    

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

  3. 選用:如果您想以英文以外的語言顯示 Apple 的登入畫面,請設定 locale 參數。如要瞭解支援的地區,請參閱「使用 Apple 文件登入」一文。

    Kotlin+KTX

    // Localize the Apple authentication screen in French.
    provider.addCustomParameter("locale", "fr")
    

    Java

    // Localize the Apple authentication screen in French.
    provider.addCustomParameter("locale", "fr");
    
  4. 使用 OAuth 提供者物件進行 Firebase 驗證。請注意,不同於其他 FirebaseAuth 作業,這會開啟自訂 Chrome 分頁來控管 UI。因此,請勿參照您附加的 OnSuccessListenerOnFailureListener 中的活動,因為當作業啟動 UI 時,這些活動會立即卸離。

    請先檢查是否已收到回覆。如果使用這個方法登入,系統會在背景中保留您的活動,這表示系統在登入流程中可回收活動。為確保使用者如果遇到這種情況,請勿再試一次,請檢查是否已有結果。

    如要查看是否有待處理的結果,請呼叫 getPendingAuthResult()

    Kotlin+KTX

    val pending = auth.pendingAuthResult
    if (pending != null) {
        pending.addOnSuccessListener { authResult ->
            Log.d(TAG, "checkPending:onSuccess:$authResult")
            // Get the user profile with authResult.getUser() and
            // authResult.getAdditionalUserInfo(), and the ID
            // token from Apple with authResult.getCredential().
        }.addOnFailureListener { e ->
            Log.w(TAG, "checkPending:onFailure", e)
        }
    } else {
        Log.d(TAG, "pending: null")
    }
    

    Java

    mAuth = FirebaseAuth.getInstance();
    Task<AuthResult> pending = mAuth.getPendingAuthResult();
    if (pending != null) {
        pending.addOnSuccessListener(new OnSuccessListener<AuthResult>() {
            @Override
            public void onSuccess(AuthResult authResult) {
                Log.d(TAG, "checkPending:onSuccess:" + authResult);
                // Get the user profile with authResult.getUser() and
                // authResult.getAdditionalUserInfo(), and the ID
                // token from Apple with authResult.getCredential().
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                Log.w(TAG, "checkPending:onFailure", e);
            }
        });
    } else {
        Log.d(TAG, "pending: null");
    }
    

    如果沒有待處理的結果,請呼叫 startActivityForSignInWithProvider() 來啟動登入流程:

    Kotlin+KTX

    auth.startActivityForSignInWithProvider(this, provider.build())
            .addOnSuccessListener { authResult ->
                // Sign-in successful!
                Log.d(TAG, "activitySignIn:onSuccess:${authResult.user}")
                val user = authResult.user
                // ...
            }
            .addOnFailureListener { e ->
                Log.w(TAG, "activitySignIn:onFailure", e)
            }
    

    Java

    mAuth.startActivityForSignInWithProvider(this, provider.build())
            .addOnSuccessListener(
                    new OnSuccessListener<AuthResult>() {
                        @Override
                        public void onSuccess(AuthResult authResult) {
                            // Sign-in successful!
                            Log.d(TAG, "activitySignIn:onSuccess:" + authResult.getUser());
                            FirebaseUser user = authResult.getUser();
                            // ...
                        }
                    })
            .addOnFailureListener(
                    new OnFailureListener() {
                        @Override
                        public void onFailure(@NonNull Exception e) {
                            Log.w(TAG, "activitySignIn:onFailure", e);
                        }
                    });
    

    有別於 Firebase 驗證的其他供應商,Apple 不提供相片網址。

    此外,如果使用者選擇不與應用程式分享電子郵件,Apple 會為使用者佈建一個專屬電子郵件地址 (格式為 [email protected]),並與您的應用程式共用。如果您設定了私人電子郵件轉發服務,Apple 會將傳送至匿名地址的電子郵件轉寄至使用者的實際電子郵件地址。

    Apple 只會在使用者首次登入時,與應用程式分享使用者資訊 (例如顯示名稱)。一般來說,Firebase 會儲存使用者首次透過 Apple 登入的顯示名稱,您可以使用 getCurrentUser().getDisplayName()。不過,如果您先前已使用 Apple 登入使用者,但未使用 Firebase 登入應用程式,Apple 就不會提供該使用者的顯示名稱。

重新驗證和帳戶連結

您也可以將相同的模式與 startActivityForReauthenticateWithProvider() 搭配使用,針對需要最近登入的敏感作業擷取最新憑證:

Kotlin+KTX

// The user is already signed-in.
val firebaseUser = auth.getCurrentUser()

firebaseUser
    .startActivityForReauthenticateWithProvider(/* activity= */ this, provider.build())
    .addOnSuccessListener( authResult -> {
        // User is re-authenticated with fresh tokens and
        // should be able to perform sensitive operations
        // like account deletion and email or password
        // update.
    })
    .addOnFailureListener( e -> {
        // Handle failure.
    })

Java

// The user is already signed-in.
FirebaseUser firebaseUser = mAuth.getCurrentUser();

firebaseUser
    .startActivityForReauthenticateWithProvider(/* activity= */ this, provider.build())
    .addOnSuccessListener(
        new OnSuccessListener<AuthResult>() {
          @Override
          public void onSuccess(AuthResult authResult) {
            // User is re-authenticated with fresh tokens and
            // should be able to perform sensitive operations
            // like account deletion and email or password
            // update.
          }
        })
    .addOnFailureListener(
        new OnFailureListener() {
          @Override
          public void onFailure(@NonNull Exception e) {
            // Handle failure.
          }
        });

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

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

舉例來說,如要將 Facebook 帳戶連結至目前的 Firebase 帳戶,請使用使用者登入 Facebook 時取得的存取憑證:

Kotlin+KTX

// Initialize a Facebook credential with a Facebook access token.
val credential = FacebookAuthProvider.getCredential(token.getToken())

// Assuming the current user is an Apple user linking a Facebook provider.
mAuth.getCurrentUser().linkWithCredential(credential)
    .addOnCompleteListener(this, task -> {
        if (task.isSuccessful()) {
          // Facebook credential is linked to the current Apple user.
          // The user can now sign in to the same account
          // with either Apple or Facebook.
        }
      });

Java

// Initialize a Facebook credential with a Facebook access token.
AuthCredential credential = FacebookAuthProvider.getCredential(token.getToken());

// Assuming the current user is an Apple user linking a Facebook provider.
mAuth.getCurrentUser().linkWithCredential(credential)
    .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
      @Override
      public void onComplete(@NonNull Task<AuthResult> task) {
        if (task.isSuccessful()) {
          // Facebook credential is linked to the current Apple user.
          // The user can now sign in to the same account
          // with either Apple or Facebook.
        }
      }
    });

進階:手動處理登入流程

您也可以使用 Apple 登入 JS SDK、手動建構 OAuth 流程,或使用 AppAuth 等 OAuth 程式庫,使用 Apple 帳戶來驗證登入流程。

  1. 針對每個登入要求產生隨機字串 (「nonce」,請您將字串設為「nonce」,藉此確定您取得的 ID 權杖是專門回應應用程式的驗證要求)。請務必採取這個步驟,以防範重送攻擊。

    您可以使用 SecureRandom 在 Android 上產生加密編譯安全的 Nonce,如以下範例所示:

    Kotlin+KTX

    private fun generateNonce(length: Int): String {
        val generator = SecureRandom()
    
        val charsetDecoder = StandardCharsets.US_ASCII.newDecoder()
        charsetDecoder.onUnmappableCharacter(CodingErrorAction.IGNORE)
        charsetDecoder.onMalformedInput(CodingErrorAction.IGNORE)
    
        val bytes = ByteArray(length)
        val inBuffer = ByteBuffer.wrap(bytes)
        val outBuffer = CharBuffer.allocate(length)
        while (outBuffer.hasRemaining()) {
            generator.nextBytes(bytes)
            inBuffer.rewind()
            charsetDecoder.reset()
            charsetDecoder.decode(inBuffer, outBuffer, false)
        }
        outBuffer.flip()
        return outBuffer.toString()
    }
    

    Java

    private String generateNonce(int length) {
        SecureRandom generator = new SecureRandom();
    
        CharsetDecoder charsetDecoder = StandardCharsets.US_ASCII.newDecoder();
        charsetDecoder.onUnmappableCharacter(CodingErrorAction.IGNORE);
        charsetDecoder.onMalformedInput(CodingErrorAction.IGNORE);
    
        byte[] bytes = new byte[length];
        ByteBuffer inBuffer = ByteBuffer.wrap(bytes);
        CharBuffer outBuffer = CharBuffer.allocate(length);
        while (outBuffer.hasRemaining()) {
            generator.nextBytes(bytes);
            inBuffer.rewind();
            charsetDecoder.reset();
            charsetDecoder.decode(inBuffer, outBuffer, false);
        }
        outBuffer.flip();
        return outBuffer.toString();
    }
    

    然後,以十六進位字串的形式取得 Nonce 的 SHA246 雜湊:

    Kotlin+KTX

    private fun sha256(s: String): String {
        val md = MessageDigest.getInstance("SHA-256")
        val digest = md.digest(s.toByteArray())
        val hash = StringBuilder()
        for (c in digest) {
            hash.append(String.format("%02x", c))
        }
        return hash.toString()
    }
    

    Java

    private String sha256(String s) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] digest = md.digest(s.getBytes());
        StringBuilder hash = new StringBuilder();
        for (byte c: digest) {
            hash.append(String.format("%02x", c));
        }
        return hash.toString();
    }
    

    您需要透過登入要求傳送 Nonce 的 SHA256 雜湊,Apple 會在回應中以未經變更的方式傳遞。Firebase 會對原始 Nonce 進行雜湊處理,並與 Apple 傳送的值進行比較,藉此驗證回應。

  2. 使用 OAuth 資料庫或其他方法啟動 Apple 登入流程。請務必在要求中加入經雜湊處理的 Nonce 做為參數。

  3. 收到 Apple 的回應後,請從回應中取得 ID 權杖,並使用該權杖和未經雜湊處理的 Nonce 建立 AuthCredential

    Kotlin+KTX

    val credential =  OAuthProvider.newCredentialBuilder("apple.com")
        .setIdTokenWithRawNonce(appleIdToken, rawUnhashedNonce)
        .build()
    

    Java

    AuthCredential credential =  OAuthProvider.newCredentialBuilder("apple.com")
        .setIdTokenWithRawNonce(appleIdToken, rawUnhashedNonce)
        .build();
    
  4. 使用 Firebase 憑證進行 Firebase 驗證:

    Kotlin+KTX

    auth.signInWithCredential(credential)
          .addOnCompleteListener(this) { task ->
              if (task.isSuccessful) {
                // User successfully signed in with Apple ID token.
                // ...
              }
          }
    

    Java

    mAuth.signInWithCredential(credential)
        .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
          @Override
          public void onComplete(@NonNull Task<AuthResult> task) {
            if (task.isSuccessful()) {
              // User successfully signed in with Apple ID token.
              // ...
            }
          }
        });
    

如果呼叫 signInWithCredential 成功,您可以使用 getCurrentUser 方法取得使用者的帳戶資料。

撤銷權杖

Apple 規定,如果應用程式必須支援帳戶建立功能,就必須讓使用者能在應用程式中刪除帳戶,詳情請參閱 App Store 審查指南

此外,支援「使用 Apple 帳戶登入」功能的應用程式,應使用「使用 Apple REST API 登入」功能撤銷使用者權杖。

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

  1. 使用 startActivityForSignInWithProvider() 方法透過 Apple 登入並取得 AuthResult

  2. 取得 Apple 供應商的存取權杖。

    Kotlin+KTX

    val oauthCredential: OAuthCredential =  authResult.credential
    val accessToken = oauthCredential.accessToken
    

    Java

    OAuthCredential oauthCredential = (OAuthCredential) authResult.getCredential();
    String accessToken = oauthCredential.getAccessToken();
    
  3. 使用 revokeAccessToken API 撤銷權杖。

    Kotlin+KTX

    mAuth.revokeAccessToken(accessToken)
      .addOnCompleteListener(this) { task ->
        if (task.isSuccessful) {
          // Access token successfully revoked
          // for the user ...
        }
    }
    

    Java

    mAuth.revokeAccessToken(accessToken)
        .addOnCompleteListener(this, new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
              if (task.isSuccessful()) {
                // Access token successfully revoked
                // for the user ...
              }
            }
      });
    
  1. 最後,刪除使用者帳戶 (以及所有相關資料)

    後續步驟

    使用者首次登入後,系統會建立新的使用者帳戶,並連結至用來登入的使用者憑證,也就是使用者名稱與密碼、電話號碼或驗證提供者資訊。這個新帳戶以 Firebase 專案的形式儲存,無論使用者以何種方式登入,都能從專案中的每個應用程式識別使用者。

    • 在應用程式中,您可以從 FirebaseUser 物件取得使用者的基本個人資料。請參閱「 管理使用者」一文。

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

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

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

    Kotlin+KTX

    Firebase.auth.signOut()

    Java

    FirebaseAuth.getInstance().signOut();