Como criar uma página de login personalizada

Neste artigo, mostramos como criar sua própria página de autenticação usando o IAP e as identidades externas. Se você criar essa página por conta própria, terá controle total sobre o fluxo de autenticação e a experiência do usuário.

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

Informações gerais

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

  1. Ative as identidades externas. Selecione Vou fornecer minha própria UI interface durante a configuração.
  2. Instale a biblioteca gcip-iap.
  3. Implemente a UI AuthenticationHandler para configurar a interface. A 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 recursos adicionais, como barras de progresso, páginas de saída e processamento de usuários.
  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. Assim, é possível personalizar todo o fluxo de autenticação sem precisar gerenciar as trocas entre a UI e o IAP.

Use as importações corretas para a 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 a v1.1.0

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

  • Atualize a versão do firebase no arquivo package.json para a v9.6.0+.
  • 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ária nenhuma mudança de código adicional.

gcip-iap v2.0.0

A partir da versão v2.0.0, o gcip-iap exige a regravação do seu aplicativo de UI personalizado usando o formato modular do SDK. Se você estiver migrando para o gcip-iap v2.0.0 ou mais recente, faça o seguinte:

  • Atualize a versão do firebase no arquivo package.json para a 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 do AuthenticationHandler.

Como selecionar locatários

Para selecionar um locatário, implemente selectTenant(). É possível implementar esse método para escolher um locatário de maneira programática ou exibir uma UI para que o próprio usuário possa selecionar um.

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 o projeto tiver vários locatários, selecione um para autenticar um usuário. Se você tiver apenas um locatário ou estiver usando autenticação no nível do projeto, não precisará 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, personalizado e anônimo 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-o 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);
  });
}

Como permitir que os usuários selecionem locatários

Para permitir que o usuário selecione um locatário, exiba uma lista de locatários e peça para o usuário escolher um. Outra opção é solicitar que o usuário digite o endereço de e-mail e, em seguida, localize uma correspondência 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 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.

getAuth() rastreia onde 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.

Caso esteja usando vários recursos do IAP com locatários diferentes, recomendamos utilizar 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 o usuário autenticar e retorne um UserCredential para o usuário que fez login após a conclusão.

Em um ambiente multilocatário, é possível determinar os métodos de autenticação disponíveis do SelectedTenantInfo, se ele tiver 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 existente com e-mail e 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 multilocação para mais informações.

Como processar os erros

Para exibir mensagens de erro aos usuários ou tentar a recuperação de erros, como tempos limite de rede, implemente handleError().

O exemplo abaixo 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.

Como personalizar a UI

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

Como exibir uma IU de progresso

Para exibir uma UI de progresso personalizada para o 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 sair, talvez não haja um URL para redirecionamento. 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 exibir 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.

Usuários de processamento

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 alterações feitas em um usuário sejam refletidas nas declarações de token de ID propagadas pelo IAP para o app, force 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, você pode 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