Permite que los usuarios accedan con Apple en iOS

En este documento, se muestra cómo usar Identity Platform a fin de agregar Acceder con Apple a tu app para iOS.

Antes de comenzar

Configura tu app con Apple

En el sitio para desarrolladores de Apple, haz lo siguiente:

  1. Habilita la función de acceso con Apple en tu app.

  2. Si usas Identity Platform para enviar correos electrónicos a tus usuarios, configura tu proyecto con el servicio de retransmisión de correo electrónico privado de Apple mediante este correo electrónico:

    noreply@project-id.firebaseapp.com
    

    También puedes usar una plantilla de correo electrónico personalizada, si tu app tiene una.

Cumple con los requisitos de datos anonimizados de Apple

Apple brinda a los usuarios la opción de anonimizar sus datos, incluida su dirección de correo electrónico. Apple asigna a los usuarios que seleccionan esta opción una dirección de correo electrónico ofuscada con el dominio privaterelay.appleid.com.

Tu app debe cumplir con las condiciones o las políticas para desarrolladores aplicables de Apple con respecto a los ID de Apple anónimos. Esto incluye obtener el consentimiento del usuario antes de asociar cualquier información de identificación personal (PII) con un ID de Apple anonimizado. Entre las acciones que implican PII, se incluyen las siguientes:

  • Vincular una dirección de correo electrónico a un ID de Apple anonimizado o viceversa.
  • Vincular un número de teléfono a un ID de Apple anonimizado, o viceversa.
  • Vincular una credencial social no anónima, como Facebook o Google, a ID de Apple anonimizado, o viceversa.

Para obtener más información, consulta el Contrato de licencia del Programa para desarrolladores de Apple de tu cuenta de desarrollador de Apple.

Configura Apple como proveedor

Para configurar Apple como proveedor de identidad, haz lo siguiente:

  1. Ve a la página Proveedores de identidad en la consola de Google Cloud.

    Ir a la página Proveedores de identidad

  2. Haz clic en Agregar un proveedor.

  3. Selecciona Apple en la lista.

  4. En Plataforma, selecciona iOS.

  5. Ingresa el ID del paquete de tu app.

  6. Para registrar los dominios de la app, haz clic en Agregar dominio en Dominios autorizados. Para fines de desarrollo, localhost ya está habilitado de forma predeterminada.

  7. En Configurar tu aplicación, haz clic en iOS. Copia el fragmento en el código de tu app para inicializar el SDK de cliente de Identity Platform.

  8. Haz clic en Guardar.

Acceso de los usuarios con el SDK de cliente

  1. Haz que el usuario acceda y obtén un token de ID con el framework de servicios de autenticación de Apple.

  2. Genera una string aleatoria, conocida como nonce, mediante una llamada a SecRandomCopyBytes(_:_:_:).

    El nonce se usa para evitar ataques de repetición. En la respuesta, incluye el hash SHA-256 de tu nonce en la solicitud de autenticación y Apple lo muestra, sin modificar. Luego, Identity Platform valida la respuesta mediante la comparación del hash original con el valor que muestra Apple.

  3. Inicia el flujo de acceso de Apple. Para ello, incluye el hash SHA-256 del nonce que creaste en el paso anterior y una clase delegada a fin de administrar la respuesta de 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()
    }
    
    @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. Controla la respuesta de Apple en tu implementación de ASAuthorizationControllerDelegate. Si el acceso se realiza de forma correcta, usa el token de ID de la respuesta de Apple con el nonce sin hash para autenticar con 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);
    }
    

A diferencia de muchos otros proveedores de identidad, Apple no proporciona una URL de foto.

Si un usuario elige no compartir su correo electrónico real con tu app, Apple aprovisiona una dirección de correo electrónico única para que ese usuario comparta en su lugar. Este correo electrónico tiene el formato xyz@privaterelay.appleid.com. Si configuraste el servicio privado de retransmisión de correo electrónico, Apple reenvía a la dirección de correo real del usuario los correos electrónicos enviados a la dirección anonimizada.

Apple solo comparte información de usuarios, como nombres visibles, con apps la primera vez que el usuario accede. En la mayoría de los casos, Identity Platform almacena estos datos, lo que te permite recuperarlos mediante firebase.auth().currentUser.displayName durante las sesiones futuras. Sin embargo, si permitiste que los usuarios accedan a tu app con Apple antes de su integración con Identity Platform, esta información no estará disponible.

Eliminación de cuentas de usuario

Apple exige que las apps para iOS que admiten la creación de cuentas también permitan que los usuarios inicien la eliminación de su cuenta desde la app.

Cuando borres una cuenta de usuario, debes revocar su token antes de borrar la cuenta, así como todos los datos que almacenaste en Firestore, Cloud Storage y Realtime Database de Firebase. Para obtener más información, consulta Cómo ofrecer la eliminación de cuentas en tu app en la documentación de asistencia para desarrolladores de Apple.

Debido a que Identity Platform no almacena tokens de usuario cuando los usuarios se crean con el acceso con Apple, debes solicitar al usuario que acceda antes de revocar su token y borrar la cuenta. Como alternativa, para evitar solicitarle al usuario que vuelva a acceder si ya accedió con el acceso con Apple, puedes almacenar el código de autorización para reutilizarlo durante la revocación del token.

Para revocar el token de un usuario y borrar su cuenta, ejecuta lo siguiente:

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

¿Qué sigue?