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

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

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

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

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

Вход с Apple могут настроить только участники программы Apple Developer Program .

Настройте вход с помощью 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 откройте раздел Auth . На вкладке «Метод входа» включите поставщика 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.

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

Чтобы пройти аутентификацию с помощью учетной записи Apple, сначала войдите в свою учетную запись Apple с помощью платформы Apple AuthenticationServices , а затем используйте токен идентификатора из ответа 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];
    }
       

    Вы отправите хэш SHA256 nonce вместе с запросом на вход, который 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 с нехешированным одноразовым номером для аутентификации в 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 не предоставляет 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.
  // ...
}
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», вы должны попросить пользователя войти в систему еще раз, прежде чем отзывать его токен и удалять учетную запись.

    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 Firebase вы можете получить уникальный идентификатор пользователя, вошедшего в систему, из переменной 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;
}

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