Вы можете разрешить пользователям выполнять аутентификацию в Firebase с помощью Apple ID, используя Firebase SDK для реализации сквозного процесса входа OAuth 2.0.
Подготовка
Чтобы пользователи могли входить в систему с помощью Apple, сначала настройте функцию "Войти с Apple" на сайте для разработчиков Apple, а затем включите Apple в качестве поставщика учетных данных для входа в проект Firebase.
Как присоединиться к программе Apple Developer Program
Настроить функцию "Войти через аккаунт Apple" могут только участники программы для разработчиков Apple.
Как настроить вход через аккаунт Apple
- Включите функцию "Войти с Apple ID" для своего приложения на странице Certificates, Identifiers & Profiles (Сертификаты, идентификаторы и профили) на сайте для разработчиков Apple.
- Свяжите сайт с приложением, как описано в первом разделе статьи Настройка входа с Apple ID для сайта. Когда появится запрос, зарегистрируйте следующий URL как URL возврата:
Идентификатор проекта Firebase можно найти на странице настроек консоли Firebase. После этого запишите новый идентификатор сервиса, который понадобится вам в следующем разделе.https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
- Создайте закрытый ключ для входа через Apple ID. В следующем разделе вам понадобятся новый закрытый ключ и идентификатор ключа.
- Если вы используете какие-либо функции Firebase Authentication, которые отправляют пользователям электронные письма, в том числе вход по ссылке в письме, подтверждение адреса электронной почты, отмену изменений в аккаунте и другие, настройте частный сервис ретрансляции электронной почты Apple и зарегистрируйте
noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com(или домен вашего шаблона письма), чтобы Apple могла пересылать письма, отправленные Firebase Authentication, на анонимизированные адреса электронной почты Apple.
Как включить Apple в качестве поставщика услуг входа
- Добавьте Firebase в свой проект Apple. При настройке приложения в консоли Firebase обязательно зарегистрируйте идентификатор пакета приложения.
- В консоли Firebase выберите Безопасность > Аутентификация.
- На вкладке Способ входа включите поставщика для входа через Apple. Укажите идентификатор сервиса, созданный в предыдущем разделе. Кроме того, в разделе конфигурации потока кода OAuth укажите идентификатор команды Apple, закрытый ключ и идентификатор ключа, созданные в предыдущем разделе.
Соблюдайте требования Apple к анонимизированным данным
При входе через Apple ID пользователи могут анонимизировать свои данные, в том числе адрес электронной почты. Пользователи, которые выберут этот вариант, получат адреса электронной почты с доменом privaterelay.appleid.com. При использовании функции "Войти с Apple" в приложении вы должны соблюдать все применимые правила для разработчиков и условия использования Apple, касающиеся анонимизированных идентификаторов Apple.
В частности, перед тем как связать с анонимизированным идентификатором Apple ID любую информацию, позволяющую установить личность, вы должны получить согласие пользователя, если это необходимо. При использовании Firebase Authentication могут выполняться следующие действия:
- Связать адрес электронной почты с анонимным Apple ID или наоборот.
- Как связать номер телефона с анонимизированным Apple ID или наоборот
- Связать анонимный идентификатор Apple ID с неанонимными учетными данными социальной сети (Facebook, Google и т. д.) или наоборот.
Этот список не является исчерпывающим. Чтобы убедиться, что ваше приложение соответствует требованиям Apple, ознакомьтесь с лицензионным соглашением программы для разработчиков Apple в разделе "Членство" вашего аккаунта разработчика.
Как войти с помощью аккаунта Apple и пройти аутентификацию в Firebase
Чтобы выполнить аутентификацию с помощью аккаунта Apple, сначала войдите в аккаунт Apple пользователя, используя фреймворк AuthenticationServices, а затем используйте токен идентификатора из ответа Apple, чтобы создать объект Firebase AuthCredential:
Для каждого запроса на вход генерируйте случайную строку – "однократно используемый номер" (nonce), – которая позволит убедиться, что полученный идентификатор был предоставлен в ответ на запрос аутентификации вашего приложения. Этот шаг важен для предотвращения атак с повторным воспроизведением.
Сгенерировать криптографически защищенное одноразовое число можно с помощью функции
SecRandomCopyBytes(_:_:_), как показано в следующем примере:Swift
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.
Swift
@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; }
Запустите процесс входа через Apple, включив в запрос хеш SHA256 для nonce и класс делегата, который будет обрабатывать ответ Apple (см. следующий шаг):
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() }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]; }Обработайте ответ Apple в реализации
ASAuthorizationControllerDelegate. Если вход выполнен успешно, используйте токен идентификатора из ответа Apple вместе с хешированным однократно числом, чтобы пройти аутентификацию в Firebase: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, 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(), чтобы получать новые учетные данные для операций, связанных с безопасностью и требующих недавнего входа в аккаунт:
Swift
// 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:
Swift
// 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.
// ...
}];
Отзыв токена
Согласно правилам проверки приложений для App Store, в приложениях, где можно создать аккаунт, должна быть возможность запросить его удаление.
Чтобы выполнить это требование, сделайте следующее:
Убедитесь, что вы заполнили разделы Идентификатор сервисов и Настройка потока кода OAuth в конфигурации поставщика "Войти с Apple", как описано в разделе Настройка функции "Войти с Apple".
Поскольку Firebase не сохраняет токены пользователей, созданных с помощью функции "Войти с Apple", вам нужно попросить пользователя войти в аккаунт ещё раз, прежде чем отзывать его токен и удалять аккаунт.
Swift
private var currentNonce: String? 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) } }
Получите код авторизации от
ASAuthorizationAppleIDCredentialи используйте его, чтобы вызватьAuth.auth().revokeToken(withAuthorizationCode:)и отозвать токены пользователя.Swift
private var user: User? 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) } } }
Наконец, удалите аккаунт пользователя (и все связанные с ним данные).
Дальнейшие действия
После первого входа пользователя создается новый аккаунт, связанный с учетными данными, которые он использовал (именем пользователя и паролем, номером телефона или информацией поставщика услуг аутентификации). Этот новый аккаунт хранится в проекте Firebase и позволяет идентифицировать пользователя во всех приложениях проекта независимо от того, как он вошел в аккаунт.
-
В своих приложениях вы можете получать основную информацию профиля пользователя из объекта
User. Подробнее о том, как управлять пользователями… В Firebase Realtime Database и Cloud Storage правилах безопасности можно получить уникальный идентификатор пользователя, выполнившего вход, из переменной
authи использовать его, чтобы контролировать, к каким данным у пользователя есть доступ.
Вы можете разрешить пользователям входить в ваше приложение, используя несколько поставщиков услуг аутентификации, связав учетные данные поставщика услуг аутентификации с существующим аккаунтом пользователя.
Чтобы выйти из аккаунта пользователя, вызовите функцию
signOut:.
Swift
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; }
Также рекомендуем добавить код обработки ошибок для всех возможных ошибок аутентификации. Подробнее об обработке ошибок…