Como criar uma página de login personalizada

Neste artigo, você vai aprender a criar sua própria página de autenticação usando identidades externas e o IAP. Criar esta página por conta própria oferece controle total sobre o fluxo de autenticação e a experiência do usuário.

Se você não precisa personalizar totalmente sua UI, permita que o IAP hospede uma página de login para você ou use a FirebaseUI para uma experiência mais simplificada.

Visão geral

Para criar sua própria página de autenticação, siga estas etapas:

  1. Ative as identidades externas. Selecione Vou fornecer minha própria UI IU durante a configuração.
  2. Instale a biblioteca gcip-iap.
  3. Configure a UI implementando a interface AuthenticationHandler. Sua página de autenticação precisa processar os seguintes cenários:
    • Seleção de locatário
    • Autorização do usuário
    • Login do usuário
    • Tratamento de erros
  4. Opcional: personalize sua página de autenticação com outros recursos, como barras de progresso, páginas de desativação e processamento do usuário.
  5. Teste a interface.

Como instalar a biblioteca gcip-iap

Para instalar a biblioteca gcip-iap, execute o seguinte comando:

npm install gcip-iap --save

O módulo gcip-iap do NPM abstrai as comunicações entre seu aplicativo, o IAP e o Identity Platform. Isso permite personalizar todo o fluxo de autenticação sem precisar gerenciar as trocas subjacentes entre a UI e o IAP.

Use as importações corretas para sua versão do SDK:

gcip-iap v0.1.4 ou anterior

// Import Firebase/GCIP dependencies. These are installed on npm install.
import * as firebase from 'firebase/app';
import 'firebase/auth';
// Import GCIP/IAP module.
import * as ciap from 'gcip-iap';

gcip-iap v1.0.0 para v1.1.0

A partir da versão v1.0.0, o gcip-iap requer a dependência de peer firebase v9 ou mais recente. Se você estiver migrando para a gcip-iap v1.0.0 ou mais recente, conclua as seguintes ações:

  • Atualize a versão do firebase no arquivo package.json para v9.6.0 ou mais recente.
  • Atualize as instruções de importação firebase da seguinte maneira:
// Import Firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import the gcip-iap module.
import * as ciap from 'gcip-iap';

Não é necessário fazer outras mudanças no código.

gcip-iap v2.0.0

A partir da versão v2.0.0, o gcip-iap exige a reescrita do aplicativo UI personalizado usando o formato do SDK modular. Se você estiver migrando para o gcip-iap v2.0.0 ou mais recente, conclua as seguintes ações:

  • Atualize a versão do firebase no arquivo package.json para v9.8.3 ou mais recente.
  • Atualize as instruções de importação firebase da seguinte maneira:
  // Import Firebase modules.
  import { initializeApp } from 'firebase/app';
  import { getAuth, GoogleAuthProvider } 'firebase/auth';
  // Import the gcip-iap module.
  import * as ciap from 'gcip-iap';

Como configurar a UI

Para configurar a UI, crie uma classe personalizada que implemente a interface AuthenticationHandler:

interface AuthenticationHandler {
  languageCode?: string | null;
  getAuth(apiKey: string, tenantId: string | null): FirebaseAuth;
  startSignIn(auth: FirebaseAuth, match?: SelectedTenantInfo): Promise<UserCredential>;
  selectTenant?(projectConfig: ProjectConfig, tenantIds: string[]): Promise<SelectedTenantInfo>;
  completeSignOut(): Promise<void>;
  processUser?(user: User): Promise<User>;
  showProgressBar?(): void;
  hideProgressBar?(): void;
  handleError?(error: Error | CIAPError): void;
}

Durante a autenticação, a biblioteca chama automaticamente os métodos de AuthenticationHandler.

Como selecionar locatários

Para selecionar um locatário, implemente selectTenant(). É possível implementar esse método para escolher um locatário de forma programática ou exibir uma UI para que o usuário possa fazer a seleção.

Em ambos os casos, a biblioteca usa o objeto SelectedTenantInfo retornado para concluir o fluxo de autenticação. Ele contém o ID do locatário selecionado, os IDs do provedor e o e-mail que o usuário digitou.

Se você tiver vários locatários no projeto, selecione um antes de autenticar um usuário. Se você tiver apenas um locatário ou estiver usando a autenticação para envolvidos no projeto, não será necessário implementar selectTenant().

O IAP aceita os mesmos provedores do Identity Platform, como:

  • E-mail e senha
  • OAuth (Google, Facebook, Twitter, GitHub, Microsoft etc.)
  • SAML
  • OIDC
  • Número de telefone
  • Personalizado
  • Anônimo

Os tipos de autenticação por número de telefone, personalizada e anônima não são compatíveis com a multilocação.

Como selecionar locatários de maneira programática

Para selecionar um locatário de maneira programática, aproveite o contexto atual. A classe Authentication contém getOriginalURL(), que retorna o URL que o usuário estava acessando antes da autenticação.

Use isso para localizar uma correspondência em uma lista de locatários associados:

// Select provider programmatically.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    auth.getOriginalURL()
      .then((originalUrl) => {
        resolve({
          tenantId: getMatchingTenantBasedOnVisitedUrl(originalUrl),
          // If associated provider IDs can also be determined,
          // populate this list.
          providerIds: [],
        });
      })
      .catch(reject);
  });
}

Permitir que os usuários selecionem locatários

Para permitir que o usuário selecione um locatário, mostre uma lista de locatários e peça para que ele escolha um ou peça que ele insira o endereço de e-mail e, em seguida, encontre um correspondente com base no domínio:

// Select provider by showing UI.
selectTenant(projectConfig, tenantIds) {
  return new Promise((resolve, reject) => {
    // Show UI to select the tenant.
    renderSelectTenant(
        tenantIds,
        // On tenant selection.
        (selectedTenantId) => {
          resolve({
            tenantId: selectedTenantId,
            // If associated provider IDs can also be determined,
            // populate this list.
            providerIds: [],
            // If email is available, populate this field too.
            email: undefined,
          });
        });
  });
}

Como autenticar usuários

Depois de ter um provedor, implemente getAuth() para retornar uma instância de Auth, correspondente à chave de API e ao ID de locatário fornecidos. Se nenhum ID de locatário for fornecido, use provedores de identidade no nível do projeto.

O getAuth() rastreia o local em que o usuário correspondente à configuração fornecida está armazenado. Além disso, ele permite atualizar silenciosamente o token de ID do Identity Platform de um usuário que já tenha sido autenticado anteriormente, sem exigir que esse usuário insira novamente as credenciais dele.

Se você estiver usando vários recursos do IAP com locatários diferentes, recomendamos usar uma instância de autenticação exclusiva para cada recurso. Desse modo, vários recursos com configurações diferentes poderão usar a mesma página de autenticação. Essa abordagem também permite que vários usuários façam login ao mesmo tempo sem desconectar o usuário anterior.

Veja a seguir um exemplo de como implementar o getAuth():

gcip-iap v1.0.0

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = firebase.app(tenantId || undefined).auth();
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = firebase.initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = app.auth();
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

gcip-iap v2.0.0

import {initializeApp, getApp} from 'firebase/app';
import {getAuth} from 'firebase/auth';

getAuth(apiKey, tenantId) {
  let auth = null;
  // Make sure the expected API key is being used.
  if (apiKey !== expectedApiKey) {
    throw new Error('Invalid project!');
  }
  try {
    auth = getAuth(getApp(tenantId || undefined));
    // Tenant ID should be already set on initialization below.
  } catch (e) {
    // Use different App names for every tenant so that
    // multiple users can be signed in at the same time (one per tenant).
    const app = initializeApp(this.config, tenantId || '[DEFAULT]');
    auth = getAuth(app);
    // Set the tenant ID on the Auth instance.
    auth.tenantId = tenantId || null;
  }
  return auth;
}

Como fazer login dos usuários

Para processar o login, implemente startSignIn(), mostre uma UI para que o usuário faça a autenticação e retorne um UserCredential ao concluir o login.

Em um ambiente com vários locatários, é possível determinar os métodos de autenticação disponíveis com SelectedTenantInfo, caso tenha sido fornecido. Essa variável contém as mesmas informações retornadas por selectTenant().

O exemplo a seguir mostra uma implementação de startSignIn() para um usuário atual com um e-mail e uma senha:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
      auth.signInWithEmailAndPassword(email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

gcip-iap v2.0.0

import {signInWithEmailAndPassword} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show the UI to sign-in or sign-up a user.
    $('#sign-in-form').on('submit', (e) => {
      const email = $('#email').val();
      const password = $('#password').val();
      // Example: Ask the user for an email and password.
      // Note: The method of sign in may have already been determined from the
      // selectedTenantInfo object.
        signInWithEmailAndPassword(auth, email, password)
        .then((userCredential) => {
          resolve(userCredential);
        })
        .catch((error) => {
          // Show the error message.
        });
    });
  });
}

Também é possível fazer o login dos usuários com um provedor federado, como SAML ou OIDC, por meio de um pop-up ou redirecionamento:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in may have already been determined from the
    // selectedTenantInfo object.
    const provider = new firebase.auth.SAMLAuthProvider('saml.myProvider');
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    auth.signInWithRedirect(provider);
  });
}

gcip-iap v2.0.0

import {signInWithPopup, SAMLAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide the user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // Note: The method of sign in might have already been determined from the
    // selectedTenantInfo object.
    const provider = new SAMLAuthProvider('saml.myProvider');
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    // Using redirect flow. When the page redirects back and sign-in completes,
    // ciap will detect the result and complete sign-in without any additional
    // action.
    signInWithRedirect(auth, provider);
  });
}

Alguns provedores de OAuth aceitam a transmissão de uma dica de login para conectar o usuário:

gcip-iap v1.0.0

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new firebase.auth.OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    auth.signInWithPopup(provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

gcip-iap v2.0.0

import {signInWithPopup, OAuthProvider} from 'firebase/auth';

startSignIn(auth, selectedTenantInfo) {
  // Show the UI to sign in or sign up a user.
  return new Promise((resolve, reject) => {
    // Use selectedTenantInfo to determine the provider and pass the login hint
    // if that provider supports it and the user specified an email address.
    if (selectedTenantInfo &&
        selectedTenantInfo.providerIds &&
        selectedTenantInfo.providerIds.indexOf('microsoft.com') !== -1) {
      const provider = new OAuthProvider('microsoft.com');
      provider.setCustomParameters({
        login_hint: selectedTenantInfo.email || undefined,
      });
    } else {
      // Figure out the provider used...
    }
    signInWithPopup(auth, provider)
      .then((userCredential) => {
        resolve(userCredential);
      })
      .catch((error) => {
        // Show the error message.
      });
    });
}

Consulte Como autenticar com multilocatários para mais informações.

Como processar os erros

Para mostrar mensagens de erro aos usuários ou tentar recuperar erros, como o tempo limite de rede, implemente handleError().

O exemplo a seguir implementa handleError():

handleError(error) {
  showAlert({
    code: error.code,
    message: error.message,
    // Whether to show the retry button. This is only available if the error is
    // recoverable via retrial.
    retry: !!error.retry,
  });
  // When user clicks retry, call error.retry();
  $('.alert-link').on('click', (e) => {
    error.retry();
    e.preventDefault();
    return false;
  });
}

A tabela abaixo lista os códigos de erro específicos do IAP que podem ser retornados. O Identity Platform também pode retornar erros. Consulte a documentação de firebase.auth.Auth.

Código do erro Descrição
invalid-argument O cliente especificou um argumento inválido.
failed-precondition A solicitação não pode ser executada no estado atual do sistema.
out-of-range O cliente especificou um intervalo inválido.
unauthenticated Solicitação não autenticada devido a um token de OAuth ausente, inválido ou expirado.
permission-denied O cliente não tem permissão suficiente ou a IU está hospedada em um domínio não autorizado.
not-found O recurso especificado não foi encontrado.
aborted Conflito de simultaneidade, como leitura-modificação-gravação.
already-exists O recurso que um cliente tentou criar já existe.
resource-exhausted Excedeu a cota de recursos ou está perto de atingir a limitação de taxa.
cancelled Solicitação cancelada pelo cliente.
data-loss Perda ou corrupção de dados irrecuperável.
unknown Erro desconhecido de servidor.
internal Erro interno do servidor.
not-implemented Método da API não implementado pelo servidor.
unavailable Serviço indisponível.
restart-process Acesse novamente o URL que redirecionou você para esta página para reiniciar o processo de autenticação.
deadline-exceeded O prazo de solicitação foi excedido.
authentication-uri-fail Falha ao gerar o URI de autenticação.
gcip-token-invalid Token de ID do GCIP fornecido inválido.
gcip-redirect-invalid URL de redirecionamento inválido.
get-project-mapping-fail Falha ao receber o ID do projeto.
gcip-id-token-encryption-error Erro de criptografia do token de ID do GCIP.
gcip-id-token-decryption-error Erro de descriptografia do token de ID do GCIP.
gcip-id-token-unescape-error Falha na função unescape de base64 segura para a Web.
resource-missing-gcip-sign-in-url O URL de autenticação do GCIP está ausente para o recurso especificado do IAP.

Personalizar a UI

É possível personalizar a página de autenticação com recursos opcionais, como barras de progresso e páginas de desativação.

Como exibir uma IU de progresso

Para mostrar uma UI de progresso personalizada ao usuário sempre que o módulo gcip-iap executar tarefas de rede de longa duração, implemente showProgressBar() e hideProgressBar().

Como desconectar os usuários

Em alguns casos, convém permitir que os usuários saiam de todas as sessões atuais que compartilham o mesmo URL de autenticação.

Depois que um usuário sai, talvez não haja um URL para redirecioná-lo de volta. Isso geralmente ocorre quando um usuário sai de todos os locatários associados a uma página de login. Nesse caso, implemente completeSignOut() para mostrar uma mensagem indicando que o usuário saiu com êxito. Se você não implementar esse método, uma página em branco vai aparecer quando um usuário sair.

Processamento de usuários

Para modificar um usuário conectado antes de redirecionar para o recurso do IAP, implemente processUser().

Use esse método para fazer o seguinte:

  • vincular a outros provedores;
  • atualizar o perfil do usuário;
  • pedir mais dados do usuário após o registro;
  • processar tokens de acesso de OAuth retornados por getRedirectResult() depois de chamar signInWithRedirect().

Veja abaixo um exemplo de como implementar o processUser():

gcip-iap v1.0.0

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

gcip-iap v2.0.0

import {getRedirectResult} from 'firebase/auth';

processUser(user) {
  return getRedirectResult(lastAuthUsed).then(function(result) {
    // Save additional data, or ask the user for additional profile information
    // to store in database, etc.
    if (result) {
      // Save result.additionalUserInfo.
      // Save result.credential.accessToken for OAuth provider, etc.
    }
    // Return the user.
    return user;
  });
}

Se você quiser que as mudanças em um usuário sejam refletidas nas declarações de token de ID propagadas pelo IAP para seu app, será necessário forçar a atualização do token:

gcip-iap v1.0.0

processUser(user) {
  return user.updateProfile({
    photoURL: 'https://example.com/profile/1234/photo.png',
  }).then(function() {
    // To reflect updated photoURL in the ID token, force token
    // refresh.
    return user.getIdToken(true);
  }).then(function() {
    return user;
  });
}

gcip-iap v2.0.0

import {updateProfile} from 'firebase/auth';

processUser(user) {
  return updateProfile(user, {
    photoURL: 'https://example.com/profile/1234/photo.png',
  }).then(function() {
    // To reflect updated photoURL in the ID token, force token
    // refresh.
    return user.getIdToken(true);
  }).then(function() {
    return user;
  });
}

Como testar a UI

Depois de criar uma classe que implementa AuthenticationHandler, será possível usá-la para criar uma nova instância de Authentication e iniciá-la:

// Implement interface AuthenticationHandler.
// const authHandlerImplementation = ....
const ciapInstance = new ciap.Authentication(authHandlerImplementation);
ciapInstance.start();

Implante seu aplicativo e navegue até a página de autenticação. Você verá sua IU de login personalizada.

A seguir