כניסה של משתמשים באמצעות Apple ב-iOS

במאמר הזה מוסבר איך להשתמש ב-Identity Platform כדי להוסיף לאפליקציה ל-iOS את האפשרות כניסה באמצעות Apple.

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

  • צריך להפעיל את Identity Platform ולהשתמש באפליקציית iOS בסיסית. במאמר מדריך למתחילים מוסבר איך להפעיל את Identity Platform ולהיכנס לחשבון.

  • מצטרפים ל-Apple Developer Program.

הגדרת האפליקציה באמצעות אפל

באתר של Apple למפתחים:

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

  2. אם אתם משתמשים ב-Identity Platform כדי לשלוח אימיילים למשתמשים, אתם צריכים להגדיר את הפרויקט באמצעות שירות ממסר האימייל הפרטי של Apple באמצעות כתובת האימייל הבאה:

    noreply@project-id.firebaseapp.com
    

    אפשר גם להשתמש בתבנית אימייל מותאמת אישית, אם יש כזו באפליקציה.

עמידה בדרישות של Apple לגבי נתונים שעברו אנונימיזציה

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

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

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

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

הגדרת Apple כספק

כדי להגדיר את Apple כספק זהויות:

  1. נכנסים לדף Identity Providers במסוף Google Cloud .

    כניסה לדף Identity Providers

  2. לוחצים על הוספת ספק.

  3. בוחרים באפשרות Apple מהרשימה.

  4. בקטע פלטפורמה, בוחרים באפשרות iOS.

  5. מזינים את מזהה החבילה של האפליקציה.

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

  7. בקטע Configure your application (הגדרת האפליקציה), לוחצים על iOS. מעתיקים את קטע הקוד לקוד של האפליקציה כדי להפעיל את Identity Platform client SDK.

  8. לוחצים על Save.

כניסה של משתמשים באמצעות ה-SDK של הלקוח

  1. מבצעים כניסה של המשתמש ומקבלים טוקן ID באמצעות מסגרת שירותי האימות של Apple.

  2. יוצרים מחרוזת אקראית, שנקראת nonce, על ידי קריאה ל-SecRandomCopyBytes(_:_:_:).

    הערך nonce משמש למניעת התקפות שליחה מחדש. אתם כוללים את גיבוב SHA-256 של הערך החד-פעמי בבקשת האימות, ואפל מחזירה אותו ללא שינוי בתגובה. לאחר מכן, Identity Platform מאמת את התגובה על ידי השוואת הגיבוב המקורי לערך שמוחזר על ידי Apple.

  3. מתחילים את תהליך הכניסה של אפל, כולל גיבוב SHA-256 של הצופן החד-פעמי שיצרתם בשלב הקודם, ומחלקת נציגים לטיפול בתגובה של אפל:

    Swift

    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()
    }
    
    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
      let inputData = Data(input.utf8)
      let hashedData = SHA256.hash(data: inputData)
      let hashString = hashedData.compactMap {
        return String(format: "%02x", $0)
      }.joined()
    
      return hashString
    }
    

    Objective-C

    @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];
    }
    
    - (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;
    }
    
  4. צריך לטפל בתגובה של Apple בהטמעה של ASAuthorizationControllerDelegate. אם הכניסה תצליח, צריך להשתמש באסימון המזהה מהתגובה של Apple עם ה-nonce שלא עבר גיבוב כדי לבצע אימות באמצעות Identity Platform:

    Swift

    @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.
          let credential = OAuthProvider.credential(withProviderID: "apple.com",
                                                    IDToken: idTokenString,
                                                    rawNonce: nonce)
          // 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)")
      }
    }
    

    Objective-C

    - (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.
        FIROAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com"
                                                                            IDToken:idToken
                                                                           rawNonce:rawNonce];
        // 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);
    }
    

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

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

‫Apple משתפת מידע על משתמשים, כמו שמות לתצוגה, רק עם אפליקציות שמשתמשים נכנסים אליהן בפעם הראשונה. ברוב המקרים, Identity Platform מאחסן את הנתונים האלה, כך שאפשר לאחזר אותם באמצעות firebase.auth().currentUser.displayName במהלך סשנים עתידיים. עם זאת, אם אפשרתם למשתמשים להיכנס לאפליקציה שלכם באמצעות אפל לפני השילוב עם Identity Platform, פרטי המשתמשים לא יהיו זמינים.

מחיקת חשבון משתמש

‫Apple דורשת שאפליקציות ל-iOS שתומכות ביצירת חשבון יאפשרו למשתמשים גם להתחיל את תהליך מחיקת החשבון מתוך האפליקציה.

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

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

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

Swift

let user = Auth.auth().currentUser

// Check if the user has a token.
if let providerData = user?.providerData {
  for provider in providerData {
    guard let provider = provider as? FIRUserInfo else {
      continue
    }
    if provider.providerID() == "apple.com" {
      isAppleProviderLinked = true
    }
  }
}

// Re-authenticate the user and revoke their token
if isAppleProviderLinked {
  let request = appleIDRequest(withState: "revokeAppleTokenAndDeleteUser")
  let controller = ASAuthorizationController(authorizationRequests: [request])
  controller.delegate = self
  controller.presentationContextProvider = self
  controller.performRequests()
} else {
  // Usual user deletion
}

func authorizationController(
  controller: ASAuthorizationController,
  didCompleteWithAuthorization authorization: ASAuthorization
) {
  if authorization.credential is ASAuthorizationAppleIDCredential {
    let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
    if authorization.credential is ASAuthorizationAppleIDCredential {
      if appleIDCredential.state == "signIn" {
        // Sign in with Firebase.
        // ...
      } else if appleIDCredential.state == "revokeAppleTokenAndDeleteUser" {
        // Revoke token with Firebase.
        Auth.auth().revokeTokenWithAuthorizationCode(code) { error in
          if error != nil {
            // Token revocation failed.
          } else {
            // Token revocation succeeded then delete user again.
            let user = Auth.auth().currentUser
            user?.delete { error in
              // ...
            }
          }

        }
      }
    }
  }
}

Objective-C

FIRUser *user = [FIRAuth auth].currentUser;

// Check if the user has a token.
BOOL isAppleProviderLinked = false;
for (id<FIRUserInfo> provider in user.providerData) {
  if ([[provider providerID] isEqual:@"apple.com"]) {
    isAppleProviderLinked = true;
  }
}

// Re-authenticate the user and revoke their token
if (isAppleProviderLinked) {
  if (@available(iOS 13, *)) {
    ASAuthorizationAppleIDRequest *request =
        [self appleIDRequestWithState:@"revokeAppleTokenAndDeleteUser"];
    ASAuthorizationController *controller = [[ASAuthorizationController alloc]
        initWithAuthorizationRequests:@[ request ]];
    controller.delegate = self;
    controller.presentationContextProvider = self;
    [controller performRequests];
  }
} else {
  // Usual user deletion
}

- (void)authorizationController:(ASAuthorizationController *)controller
    didCompleteWithAuthorization:(ASAuthorization *)authorization
    API_AVAILABLE(ios(13.0)) {
  if ([authorization.credential
          isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
    ASAuthorizationAppleIDCredential *appleIDCredential =
        authorization.credential;

    if ([appleIDCredential.state isEqualToString:@"signIn"]) {
      // Sign in with Firebase.
      // ...
    } else if ([appleIDCredential.state
                  isEqualToString:@"revokeAppleTokenAndDeleteUser"]) {
      // Revoke token with Firebase.
      NSString *code =
          [[NSString alloc] initWithData:appleIDCredential.authorizationCode
                                encoding:NSUTF8StringEncoding];
      [[FIRAuth auth]
          revokeTokenWithAuthorizationCode:code
                                completion:^(NSError *_Nullable error) {
                                  if (error != nil) {
                                    // Token revocation failed.
                                  } else {
                                    // Token revocation succeeded then delete
                                    // user again.
                                    FIRUser *user = [FIRAuth auth].currentUser;
                                    [user deleteWithCompletion:^(
                                              NSError *_Nullable error){
                                        // ...
                                    }];
                                  }
                                }];
    }
  }
}

המאמרים הבאים