Iniciar sessão de utilizadores com a Apple no iOS

Este documento mostra como usar a Identity Platform para adicionar a opção Iniciar sessão com a Apple à sua app iOS.

Antes de começar

Configurar a sua app com a Apple

No site do programador da Apple:

  1. Ative a capacidade Iniciar sessão com a Apple para a sua app.

  2. Se usar a Identity Platform para enviar emails aos seus utilizadores, configure o seu projeto com o serviço de retransmissão de email privado da Apple usando o seguinte email:

    noreply@project-id.firebaseapp.com
    

    Também pode usar um modelo de email personalizado, se a sua app tiver um.

Agir em conformidade com os requisitos de dados anónimos da Apple

A Apple dá aos utilizadores a opção de anonimizar os respetivos dados, incluindo o endereço de email. A Apple atribui aos utilizadores que selecionam esta opção um endereço de email ocultado com o domínio privaterelay.appleid.com.

A sua app tem de estar em conformidade com todas as políticas ou termos de programadores aplicáveis da Apple relativamente a IDs Apple anónimos. Isto inclui obter o consentimento do utilizador antes de associar quaisquer informações de identificação pessoal (IIP) a um ID Apple anónimo. As ações que envolvem PII incluem, entre outras:

  • Associar um endereço de email a um ID Apple anónimo ou vice-versa.
  • Associar um número de telefone a um ID Apple anónimo ou vice-versa
  • Associar uma credencial social não anónima, como o Facebook ou o Google, a um ID Apple anónimo ou vice-versa.

Para mais informações, consulte o Contrato de Licença do Programa para Programadores da Apple para a sua conta de programador da Apple.

Configurar a Apple como fornecedor

Para configurar a Apple como um Fornecedor de identidade:

  1. Aceda à página Fornecedores de identidade na Google Cloud consola.

    Aceda à página Fornecedores de identidade

  2. Clique em Adicionar um fornecedor.

  3. Selecione Apple na lista.

  4. Em Plataforma, selecione iOS.

  5. Introduza o ID do pacote da sua app.

  6. Registe os domínios da sua app clicando em Adicionar domínio em Domínios autorizados. Para fins de programação, o localhost já está ativado por predefinição.

  7. Em Configure a sua aplicação, clique em iOS. Copie o fragmento para o código da sua app para inicializar o SDK do cliente do Identity Platform.

  8. Clique em Guardar.

Iniciar sessão de utilizadores com o SDK do cliente

  1. Inicie sessão no utilizador e obtenha um token de ID através da estrutura de serviços de autenticação da Apple.

  2. Gere uma string aleatória, conhecida como nonce, chamando SecRandomCopyBytes(_:_:_:).

    O nonce é usado para evitar ataques de repetição. Inclui o hash SHA-256 do seu valor único no pedido de autenticação e a Apple devolve-o, sem modificações, na resposta. Em seguida, a Identity Platform valida a resposta comparando o hash original com o valor devolvido pela Apple.

  3. Inicie o fluxo de início de sessão da Apple, incluindo o hash SHA-256 do nonce que criou no passo anterior e uma classe delegada para processar a resposta da 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. Trate a resposta da Apple na sua implementação de ASAuthorizationControllerDelegate. Se a sessão for iniciada com êxito, use o token de ID da resposta da Apple com o nonce não hash para fazer a autenticação com a 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);
    }
    

Ao contrário de muitos outros fornecedores de identidade, a Apple não fornece um URL de foto.

Se um utilizador optar por não partilhar o respetivo email real com a sua app, a Apple disponibiliza um endereço de email exclusivo para esse utilizador partilhar. Este email tem o formato xyz@privaterelay.appleid.com. Se configurou o serviço de retransmissão de email privado, a Apple encaminha os emails enviados para o endereço anónimo para o endereço de email real do utilizador.

A Apple só partilha informações do utilizador, como nomes a apresentar, com as apps na primeira vez que um utilizador inicia sessão. Na maioria dos casos, a Identity Platform armazena estes dados, o que lhe permite obtê-los através de firebase.auth().currentUser.displayName durante sessões futuras. No entanto, se permitiu que os utilizadores iniciassem sessão na sua app através da Apple antes da integração com a Identity Platform, as informações dos utilizadores não estão disponíveis.

Eliminação da conta de utilizador

A Apple exige que as apps iOS que suportam a criação de contas também permitam que os utilizadores iniciem a eliminação da respetiva conta na app.

Quando elimina uma conta de utilizador, tem de revogar o token do utilizador antes de eliminar a conta do utilizador, bem como todos os dados que armazenou para o utilizador no Firestore, no Cloud Storage e na Firebase Realtime Database. Para mais informações, consulte o artigo Oferecer eliminação de contas na sua app na documentação de apoio técnico para programadores da Apple.

Uma vez que a Identity Platform não armazena tokens de utilizador quando os utilizadores são criados com o início de sessão da Apple, tem de pedir ao utilizador para iniciar sessão antes de revogar o respetivo token e eliminar a conta. Em alternativa, para evitar pedir ao utilizador que inicie sessão novamente, se um utilizador tiver sessão iniciada com o início de sessão da Apple, pode armazenar o código de autorização para reutilização durante a revogação do token.

Para revogar o token de um utilizador e eliminar a respetiva conta, execute o seguinte:

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

O que se segue?