在 Android 上使用 Apple 驗證

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

事前準備

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

加入 Apple 開發人員計畫

只有 Apple 開發人員計畫成員才能設定「使用 Apple 帳戶登入」功能。

設定「使用 Apple 帳戶登入」功能

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

  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 新增至 Android 專案。在 Firebase 主控台中設定應用程式時,請務必註冊應用程式的 SHA-1 簽名。
  2. Firebase 控制台中,開啟「Auth」部分。在「Sign in method」分頁中,啟用 Apple 供應器。指定您在上一節中建立的服務 ID。此外,請在「OAuth 程式碼流程設定」專區中指定 Apple 團隊 ID,以及您在前一個專區中建立的私密金鑰和金鑰 ID。

遵守 Apple 去識別化資料規定

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

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

  • 將電子郵件地址連結至匿名 Apple ID,或將匿名 Apple ID 連結至電子郵件地址。
  • 將電話號碼連結至匿名 Apple ID,或將匿名 Apple ID 連結至電話號碼
  • 將非匿名社群媒體憑證 (Facebook、Google 等) 連結至匿名 Apple ID,或反之。

請注意,上方清單僅列出部分示例。請參閱開發人員帳戶「會員」部分的《Apple Developer Program License Agreement》,確保您的應用程式符合 Apple 的規定。

使用 Firebase SDK 處理登入流程

在 Android 上,如要讓 Firebase 使用 Apple 帳戶驗證使用者,最簡單的方法就是使用 Firebase Android SDK 處理整個登入流程。

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

  1. 使用提供者 ID apple.com,使用建構工具建構 OAuthProvider 的例項:

    Kotlin

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

    Java

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

    Kotlin

    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

    // 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 自訂分頁來控制使用者介面。因此,請勿在您附加的 OnSuccessListenerOnFailureListener 中參照活動,因為這兩者會在作業啟動 UI 時立即解除連結。

    請先確認你是否已收到回覆。使用這種方法登入會將活動置於背景,也就是說,系統可以在登入流程中回收活動。為確保在發生這種情況時,您不會讓使用者重試,請檢查是否已顯示結果。

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

    Kotlin

    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

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

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

    Apple 只會在使用者首次登入時,與應用程式分享顯示名稱等使用者資訊。通常,Firebase 會儲存使用者首次透過 Apple 登入時的顯示名稱,您可以使用 getCurrentUser().getDisplayName() 取得這項資訊。不過,如果您先前曾使用 Apple 服務,讓使用者登入應用程式,但未使用 Firebase,Apple 就不會向 Firebase 提供使用者的顯示名稱。

重新驗證和帳戶連結

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

Kotlin

// 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

// 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 帳戶透過 Firebase 進行驗證,方法是使用 Apple 登入 JS SDK 處理登入流程、手動建構 OAuth 流程,或是使用 AppAuth 等 OAuth 程式庫。

  1. 針對每個登入要求,產生隨機字串 (「nonce」),您可以使用該字串,確認您取得的 ID 權杖是特別針對應用程式驗證要求而核發。這項步驟對於防範重播攻擊至關重要。

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

    Kotlin

    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

    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

    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

    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

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

    Java

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

    Kotlin

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

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

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

    Kotlin

    Firebase.auth.signOut()

    Java

    FirebaseAuth.getInstance().signOut();