Аутентификация с помощью Apple

Вы можете разрешить своим пользователям проходить аутентификацию в Firebase с использованием их Apple ID, используя Firebase SDK для реализации сквозного процесса входа OAuth 2.0.

Прежде чем начать

Чтобы выполнить вход пользователей с помощью Apple, сначала настройте функцию «Вход с Apple» на сайте разработчиков Apple, а затем включите Apple в качестве поставщика входа для вашего проекта Firebase.

Присоединяйтесь к программе разработчиков Apple

Функцию «Войти с Apple» могут настроить только участники программы разработчиков Apple .

Настроить вход с Apple

  1. Включите функцию «Вход с Apple» для своего приложения на странице «Сертификаты, идентификаторы и профили» сайта разработчиков Apple.
  2. Свяжите свой веб-сайт с приложением, как описано в первом разделе статьи «Настройка входа с Apple для веб-сайта» . При появлении запроса зарегистрируйте следующий URL-адрес в качестве 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 откройте раздел «Аутентификация» . На вкладке «Метод входа» включите поставщика Apple . Укажите идентификатор сервиса, созданный в предыдущем разделе. Кроме того, в разделе «Конфигурация потока кода OAuth» укажите идентификатор вашей команды Apple Team ID, а также закрытый ключ и идентификатор ключа, созданные в предыдущем разделе.

Соблюдайте требования Apple к анонимным данным

Функция «Войти с Apple» предоставляет пользователям возможность анонимизировать свои данные, включая адрес электронной почты, при входе в систему. Адреса электронной почты пользователей, выбравших эту опцию, находятся в домене privaterelay.appleid.com . При использовании функции «Войти с Apple» в вашем приложении вы должны соблюдать все применимые политики разработчиков или условия Apple в отношении этих анонимизированных идентификаторов Apple ID.

Это включает получение любого необходимого согласия пользователя перед связыванием какой-либо персональной информации, позволяющей идентифицировать пользователя, с анонимизированным Apple ID. При использовании аутентификации Firebase это может включать следующие действия:

  • Свяжите адрес электронной почты с анонимным Apple ID и наоборот.
  • Привязать номер телефона к анонимному Apple ID и наоборот
  • Свяжите неанонимные учетные данные социальной сети (Facebook, Google и т. д.) с анонимным Apple ID или наоборот.

Приведённый выше список не является исчерпывающим. Чтобы убедиться, что ваше приложение соответствует требованиям Apple, ознакомьтесь с лицензионным соглашением программы Apple Developer Program в разделе «Участие» вашей учётной записи разработчика.

Войдите в систему с помощью Apple и пройдите аутентификацию с помощью Firebase

Чтобы выполнить аутентификацию с помощью учетной записи Apple, сначала войдите в учетную запись Apple, используя фреймворк AuthenticationServices от Apple, а затем используйте токен ID из ответа Apple для создания объекта Firebase AuthCredential :

  1. Для каждого запроса на вход генерируйте случайную строку — «nonce» — которую вы будете использовать, чтобы убедиться, что полученный вами токен ID был выдан именно в ответ на запрос аутентификации вашего приложения. Этот шаг важен для предотвращения атак повторного воспроизведения.

    Вы можете создать криптографически безопасный одноразовый код с помощью 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)
    }
    
        

    Objective-C

    // 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];
    }
        

    Вы отправите хэш 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
    }
    
        

    Objective-C

    - (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 одноразового значения и класс делегата, который будет обрабатывать ответ 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()
    }
    

    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];
    }
    
  3. Обработайте ответ Apple в вашей реализации ASAuthorizationControllerDelegate . Если вход прошёл успешно, используйте токен идентификатора из ответа Apple с нехешированным одноразовым кодом для аутентификации в 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)")
      }
    
    }
    

    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, 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 не предоставляет URL-адрес фотографии.

Кроме того, когда пользователь решает не предоставлять приложению свой адрес электронной почты, 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.
  // ...
}

Objective-C

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

Objective-C

// 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», вам необходимо попросить пользователя снова войти в систему, прежде чем отозвать его токен и удалить учетную запись.

    Быстрый

    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:) для отзыва токенов пользователя.

    Быстрый

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

Objective-C

NSError *signOutError;
BOOL status = [[FIRAuth auth] signOut:&signOutError];
if (!status) {
  NSLog(@"Error signing out: %@", signOutError);
  return;
}

Вы также можете добавить код обработки ошибок для всего спектра ошибок аутентификации. См. раздел Обработка ошибок .