אימות באמצעות Apple

אתם יכולים לאפשר למשתמשים לבצע אימות ב-Firebase באמצעות Apple ID שלהם, על ידי שימוש ב-Firebase SDK כדי לבצע את תהליך הכניסה של OAuth 2.0 מקצה לקצה.

לפני שמתחילים

כדי לאפשר למשתמשים להיכנס באמצעות Apple, קודם צריך להגדיר את הכניסה באמצעות Apple באתר למפתחים של Apple, ואז להפעיל את Apple כספק כניסה לפרויקט ב-Firebase.

הצטרפות לתוכנית המפתחים של Apple

רק חברים בתוכנית המפתחים של Apple יכולים להגדיר כניסה באמצעות Apple.

הגדרת כניסה באמצעות Apple

  1. מפעילים את האפשרות 'כניסה באמצעות חשבון Apple' באפליקציה בדף Certificates, Identifiers & Profiles באתר המפתחים של Apple.
  2. משייכים את האתר לאפליקציה כפי שמתואר בקטע הראשון של הגדרת 'כניסה באמצעות Apple' לאינטרנט. כשתתבקשו, רושמים את את כתובת האתר הבאה ככתובת URL להחזרה:
    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
    מזהה הפרויקט ב-Firebase מופיע בדף ההגדרות של מסוף Firebase. בסיום התהליך, מומלץ לשמור את מזהה השירות החדש, שאותו צריך להזין בקטע הבא.
  3. יוצרים כניסה באמצעות מפתח פרטי של Apple. המפתח הפרטי החדש ומזהה המפתח יידרשו בקטע הבא.
  4. אם משתמשים בתכונות של Firebase Authentication ששולחות אימיילים למשתמשים, כולל כניסה לקישור אימייל, אימות כתובת אימייל, שינוי חשבון לבטל, אחרים, יש להגדיר את שירות ממסר האימייל הפרטי של Apple ולהירשם noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com (או הדומיין של תבנית האימייל המותאמת אישית שלכם) כדי ש-Apple תוכל להעביר אימיילים שנשלחו. עד Firebase Authentication לכתובות אימייל אנונימיות של Apple.

הפעלת Apple כספק כניסה

  1. מוסיפים את Firebase לפרויקט Apple. חשוב לרשום את מזהה החבילה של האפליקציה כשמגדירים אותה במסוף Firebase.
  2. במסוף Firebase, פותחים את הקטע אימות. בכרטיסייה Sign in method מפעילים את הספק Apple. מציינים את מזהה השירות שיצרתם בקטע הקודם. בנוסף, בקטע OAuth code flow configuration, מציינים את מזהה הצוות ב-Apple ואת המפתח הפרטי ומזהה המפתח שיצרתם בקטע הקודם.

תאימות לדרישות של Apple לגבי נתונים אנונימיים

כשמשתמשים ב'כניסה באמצעות חשבון Apple', הם יכולים להפוך את הנתונים שלהם, כולל כתובת האימייל, לאנונימיים בזמן הכניסה. משתמשים שבוחרים באפשרות הזו יש כתובות אימייל עם הדומיין privaterelay.appleid.com. מתי בכניסה באמצעות חשבון Apple באפליקציה שלך, עליך לפעול בהתאם לכל במדיניות או בתנאים של Apple למפתחים בנוגע המזהים.

המשמעות היא שעליכם לקבל את הסכמת המשתמשים הנדרשת לפני שתשייכו פרטים אישיים מזהים באופן ישיר למזהה Apple אנונימי. כשמשתמשים באימות ב-Firebase, הפעולות האלה עשויות לכלול:

  • קישור כתובת אימייל ל-Apple ID שעברה אנונימיזציה או להפך.
  • קישור של מספר טלפון ל-Apple ID שהוסרה ממנו הפרטיות או להפך
  • לקשר פרטי כניסה אנונימיים לרשת חברתית (Facebook, Google וכו') אל Apple ID שעבר אנונימיזציה, או להפך.

הרשימה שלמעלה היא חלקית בלבד. מידע נוסף זמין בתוכנית למפתחים של Apple הסכם הרישיון בקטע 'מינויים' בחשבון הפיתוח, כדי: מוודאים שהאפליקציה עומדת בדרישות של Apple.

כניסה באמצעות Apple ואימות באמצעות Firebase

כדי לבצע אימות באמצעות חשבון Apple, קודם צריך להיכנס לחשבון של המשתמש ב-Apple באמצעות AuthenticationServices framework של Apple, ואז להשתמש באסימון המזהה מהתשובה של Apple כדי ליצור Firebase אובייקט AuthCredential:

  1. לכל בקשת כניסה צריך ליצור מחרוזת אקראית – "nonce" – שבו תשתמשו כדי לוודא שהאסימון המזהה שקיבלתם מוענקת במיוחד בתגובה לבקשת האימות של האפליקציה. השלב הזה חשוב כדי למנוע התקפות שליחה מחדש.

    אפשר ליצור קוד חד-פעמי מאובטח מבחינה קריפטוגרפית באמצעות SecRandomCopyBytes(_:_:_), כמו בדוגמה הבאה:

    private func randomNonceString(length: Int = 32) -> String {
      precondition(length > 0)
      var randomBytes = [UInt8](repeating: 0, count: length)
      let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
      if errorCode != errSecSuccess {
        fatalError(
          "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
        )
      }
    
      let charset: [Character] =
        Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
    
      let nonce = randomBytes.map { byte in
        // Pick a random character from the set, wrapping around if needed.
        charset[Int(byte) % charset.count]
      }
    
      return String(nonce)
    }
    
        
    // Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
    - (NSString *)randomNonce:(NSInteger)length {
      NSAssert(length > 0, @"Expected nonce to have positive length");
      NSString *characterSet = @"0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._";
      NSMutableString *result = [NSMutableString string];
      NSInteger remainingLength = length;
    
      while (remainingLength > 0) {
        NSMutableArray *randoms = [NSMutableArray arrayWithCapacity:16];
        for (NSInteger i = 0; i < 16; i++) {
          uint8_t random = 0;
          int errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random);
          NSAssert(errorCode == errSecSuccess, @"Unable to generate nonce: OSStatus %i", errorCode);
    
          [randoms addObject:@(random)];
        }
    
        for (NSNumber *random in randoms) {
          if (remainingLength == 0) {
            break;
          }
    
          if (random.unsignedIntValue < characterSet.length) {
            unichar character = [characterSet characterAtIndex:random.unsignedIntValue];
            [result appendFormat:@"%C", character];
            remainingLength--;
          }
        }
      }
    
      return [result copy];
    }
        

    עליכם לשלוח את הגיבוב של המזהה החד-פעמי (nonce) לפי פרוטוקול SHA256 עם בקשת הכניסה לחשבון, ו-Apple תעביר אותו ללא שינוי בתגובה. מערכת Firebase מאמתת את התשובה על ידי גיבוב של המזהה החד-פעמי המקורי והשוואה שלו לערך שהוענק על ידי Apple.

    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
      let inputData = Data(input.utf8)
      let hashedData = SHA256.hash(data: inputData)
      let hashString = hashedData.compactMap {
        String(format: "%02x", $0)
      }.joined()
    
      return hashString
    }
    
        
    - (NSString *)stringBySha256HashingString:(NSString *)input {
      const char *string = [input UTF8String];
      unsigned char result[CC_SHA256_DIGEST_LENGTH];
      CC_SHA256(string, (CC_LONG)strlen(string), result);
    
      NSMutableString *hashed = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
      for (NSInteger i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
        [hashed appendFormat:@"%02x", result[i]];
      }
      return hashed;
    }
        
  2. התחלת תהליך הכניסה של Apple, כולל בבקשה שלך את הגיבוב SHA256 של הצופן החד-פעמי (nonce) והסיווג של הענקת הגישה שתטפל בתשובה של Apple (ראו השלב הבא):

    import CryptoKit
    
    // Unhashed nonce.
    fileprivate var currentNonce: String?
    
    @available(iOS 13, *)
    func startSignInWithAppleFlow() {
      let nonce = randomNonceString()
      currentNonce = nonce
      let appleIDProvider = ASAuthorizationAppleIDProvider()
      let request = appleIDProvider.createRequest()
      request.requestedScopes = [.fullName, .email]
      request.nonce = sha256(nonce)
    
      let authorizationController = ASAuthorizationController(authorizationRequests: [request])
      authorizationController.delegate = self
      authorizationController.presentationContextProvider = self
      authorizationController.performRequests()
    }
    
    @import CommonCrypto;
    
    - (void)startSignInWithAppleFlow {
      NSString *nonce = [self randomNonce:32];
      self.currentNonce = nonce;
      ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
      ASAuthorizationAppleIDRequest *request = [appleIDProvider createRequest];
      request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
      request.nonce = [self stringBySha256HashingString:nonce];
    
      ASAuthorizationController *authorizationController =
          [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
      authorizationController.delegate = self;
      authorizationController.presentationContextProvider = self;
      [authorizationController performRequests];
    }
    
  3. מטפלים בתשובה של Apple בהטמעה של ASAuthorizationControllerDelegate. אם הכניסה בוצעה בהצלחה, יש להשתמש במזהה מהתשובה של Apple עם צופן חד-פעמי (nonce) לא מגובב כדי לבצע אימות Firebase:

    @available(iOS 13.0, *)
    extension MainViewController: ASAuthorizationControllerDelegate {
    
      func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
          guard let nonce = currentNonce else {
            fatalError("Invalid state: A login callback was received, but no login request was sent.")
          }
          guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
          }
          guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
            return
          }
          // Initialize a Firebase credential, including the user's full name.
          let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
                                                            rawNonce: nonce,
                                                            fullName: appleIDCredential.fullName)
          // Sign in with Firebase.
          Auth.auth().signIn(with: credential) { (authResult, error) in
            if error {
              // Error. If error.code == .MissingOrInvalidNonce, make sure
              // you're sending the SHA256-hashed nonce as a hex string with
              // your request to Apple.
              print(error.localizedDescription)
              return
            }
            // User is signed in to Firebase with Apple.
            // ...
          }
        }
      }
    
      func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
        print("Sign in with Apple errored: \(error)")
      }
    
    }
    
    - (void)authorizationController:(ASAuthorizationController *)controller
       didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
      if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
        ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
        NSString *rawNonce = self.currentNonce;
        NSAssert(rawNonce != nil, @"Invalid state: A login callback was received, but no login request was sent.");
    
        if (appleIDCredential.identityToken == nil) {
          NSLog(@"Unable to fetch identity token.");
          return;
        }
    
        NSString *idToken = [[NSString alloc] initWithData:appleIDCredential.identityToken
                                                  encoding:NSUTF8StringEncoding];
        if (idToken == nil) {
          NSLog(@"Unable to serialize id token from data: %@", appleIDCredential.identityToken);
        }
    
        // Initialize a Firebase credential, including the user's full name.
        FIROAuthCredential *credential = [FIROAuthProvider appleCredentialWithIDToken:IDToken
                                                                             rawNonce:self.appleRawNonce
                                                                             fullName:appleIDCredential.fullName];
    
        // Sign in with Firebase.
        [[FIRAuth auth] signInWithCredential:credential
                                  completion:^(FIRAuthDataResult * _Nullable authResult,
                                               NSError * _Nullable error) {
          if (error != nil) {
            // Error. If error.code == FIRAuthErrorCodeMissingOrInvalidNonce,
            // make sure you're sending the SHA256-hashed nonce as a hex string
            // with your request to Apple.
            return;
          }
          // Sign-in succeeded!
        }];
      }
    }
    
    - (void)authorizationController:(ASAuthorizationController *)controller
               didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
      NSLog(@"Sign in with Apple errored: %@", error);
    }
    

בניגוד לספקים אחרים שנתמכים על ידי Firebase Auth, Apple לא מספקת כתובת האתר של התמונה.

כמו כן, כשהמשתמש בוחר שלא לשתף את כתובת האימייל שלו עם האפליקציה, Apple מקצה כתובת אימייל ייחודית לאותו משתמש (בטופס xyz@privaterelay.appleid.com), שאותו הוא משתף עם האפליקציה. אם מוגדר שירות ממסר האימייל הפרטי, Apple מעבירה אימיילים שנשלחים אל שעברו אנונימיזציה לכתובת האימייל האמיתית של המשתמש.

אימות מחדש וקישור חשבונות

אפשר להשתמש באותו דפוס עם reauthenticateWithCredential(), כדי לאחזר פרטי כניסה חדשים לפעולות רגישות שדורשות כניסה לאחרונה:

// Initialize a fresh Apple credential with Firebase.
let credential = OAuthProvider.credential(
  withProviderID: "apple.com",
  IDToken: appleIdToken,
  rawNonce: rawNonce
)
// Reauthenticate current Apple user with fresh Apple credential.
Auth.auth().currentUser.reauthenticate(with: credential) { (authResult, error) in
  guard error != nil else { return }
  // Apple user successfully re-authenticated.
  // ...
}
FIRAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com",
                                                                   IDToken:appleIdToken,
                                                                  rawNonce:rawNonce];
[[FIRAuth auth].currentUser
    reauthenticateWithCredential:credential
                      completion:^(FIRAuthDataResult * _Nullable authResult,
                                   NSError * _Nullable error) {
  if (error) {
    // Handle error.
  }
  // Apple user successfully re-authenticated.
  // ...
}];

בנוסף, אפשר להשתמש ב-linkWithCredential() כדי לקשר ספקי זהויות שונים אל חשבונות קיימים.

חשוב לזכור ש-Apple דורשת לקבל מהמשתמשים הסכמה מפורשת לפני שמקשרים את חשבונות Apple שלהם לנתונים אחרים.

כניסה באמצעות חשבון Apple לא תאפשר לכם לעשות שימוש חוזר בפרטי כניסה לאימות כדי לקשר לחשבון קיים. אם רוצים לקשר פרטי כניסה של Apple לחשבון אחר עליך לנסות תחילה לקשר את החשבונות באמצעות הגרסה הקודמת של כניסה באמצעות פרטי כניסה של Apple ולאחר מכן בודקים את השגיאה שהוחזרה כדי למצוא פרטי כניסה חדשים. פרטי הכניסה החדשים יהיו במילון userInfo של השגיאה, ותהיה לך אפשרות ניתן לגשת אליהם דרך המפתח AuthErrorUserInfoUpdatedCredentialKey.

לדוגמה, כדי לקשר חשבון Facebook לחשבון Firebase הנוכחי, משתמשים באסימון הגישה שקיבלת אחרי שהמשתמש נכנס ל-Facebook:

// Initialize a Facebook credential with Firebase.
let credential = FacebookAuthProvider.credential(
  withAccessToken: AccessToken.current!.tokenString
)
// Assuming the current user is an Apple user linking a Facebook provider.
Auth.auth().currentUser.link(with: credential) { (authResult, error) in
  // Facebook credential is linked to the current Apple user.
  // The user can now sign in with Facebook or Apple to the same Firebase
  // account.
  // ...
}
// Initialize a Facebook credential with Firebase.
FacebookAuthCredential *credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken];
// Assuming the current user is an Apple user linking a Facebook provider.
[FIRAuth.auth linkWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
  // Facebook credential is linked to the current Apple user.
  // The user can now sign in with Facebook or Apple to the same Firebase
  // account.
  // ...
}];

ביטול טוקן

לפי דרישת Apple, אפליקציות שתומכות ביצירת חשבון חייבות לאפשר למשתמשים ליזום למחוק את החשבון שלו בתוך האפליקציה, כפי שמתואר בביקורת ב-App Store הנחיות

כדי לעמוד בדרישה הזו, צריך לבצע את השלבים הבאים:

  1. חשוב לוודא שמילאתם את מזהה השירותים ואת ההגדרה של תהליך האימות באמצעות קוד OAuth בהגדרות של הספק 'כניסה באמצעות חשבון Apple', כפי שמתואר הקטע הגדרת הכניסה באמצעות Apple.

  2. מאחר ש-Firebase לא מאחסן אסימוני משתמשים כאשר משתמשים נוצרים באמצעות 'כניסה באמצעות חשבון Apple', צריך לבקש מהמשתמש להיכנס שוב לפני ביטול האסימון ומחיקה של החשבון.

    Swift
    private func deleteCurrentUser() {
      do {
        let nonce = try CryptoUtils.randomNonceString()
        currentNonce = nonce
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        request.nonce = CryptoUtils.sha256(nonce)
    
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
      } catch {
        // In the unlikely case that nonce generation fails, show error view.
        displayError(error)
      }
    }
  3. מקבלים את קוד ההרשאה מ-ASAuthorizationAppleIDCredential, להשתמש בו כדי לקרוא ל-Auth.auth().revokeToken(withAuthorizationCode:) כדי לבטל האסימונים של המשתמש.

    Swift
    func authorizationController(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization) {
      guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
      else {
        print("Unable to retrieve AppleIDCredential")
        return
      }
    
      guard let _ = currentNonce else {
        fatalError("Invalid state: A login callback was received, but no login request was sent.")
      }
    
      guard let appleAuthCode = appleIDCredential.authorizationCode else {
        print("Unable to fetch authorization code")
        return
      }
    
      guard let authCodeString = String(data: appleAuthCode, encoding: .utf8) else {
        print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
        return
      }
    
      Task {
        do {
          try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString)
          try await user?.delete()
          self.updateUI()
        } catch {
          self.displayError(error)
        }
      }
    }
  4. לבסוף, מוחקים את חשבון המשתמש (ואת כל הנתונים המשויכים אליו)

השלבים הבאים

אחרי שמשתמש נכנס בפעם הראשונה, נוצר חשבון משתמש חדש שמקושרות לפרטי הכניסה - כלומר שם המשתמש והסיסמה, מספר הטלפון מספר, או פרטים של ספק אימות – המשתמש נכנס באמצעותו. החשבון החדש הזה מאוחסן כחלק מפרויקט Firebase, וניתן להשתמש בו כדי לזהות משתמש בכל האפליקציות בפרויקט, ללא קשר לאופן שבו המשתמש נכנס לחשבון.

  • באפליקציות שלכם, אתם יכולים לקבל את פרטי הפרופיל הבסיסיים של המשתמש מהאובייקט User. למידע נוסף, ראו ניהול משתמשים.

  • בכללי האבטחה של Firebase Realtime Database ו-Cloud Storage, אפשר לקבל את מזהה המשתמש הייחודי של המשתמש שנכנס לחשבון מהמשתנה auth, ולהשתמש בו כדי לקבוע לאילו נתונים למשתמש תהיה גישה.

כדי לאפשר למשתמשים להיכנס לאפליקציה באמצעות כמה ספקי אימות, אפשר לקשר את פרטי הכניסה של ספק האימות לחשבון משתמש קיים.

כדי לנתק משתמש מהחשבון, יש להתקשר אל signOut:

let firebaseAuth = Auth.auth()
do {
  try firebaseAuth.signOut()
} catch let signOutError as NSError {
  print("Error signing out: %@", signOutError)
}
NSError *signOutError;
BOOL status = [[FIRAuth auth] signOut:&signOutError];
if (!status) {
  NSLog(@"Error signing out: %@", signOutError);
  return;
}

כדאי גם להוסיף קוד טיפול בשגיאות לכל טווח האימות שגיאות. אפשר לעיין במאמר טיפול בשגיאות.