Como criar uma página de login personalizada

Para usar identidades externas com o Identity-Aware Proxy (IAP), seu app precisa de uma página de login. O IAP redirecionará os usuários para esta página para autenticação antes que eles acessem recursos seguros.

Este artigo mostra como criar uma página de autenticação do zero. Criar esta página por conta própria é complexo, mas oferece controle total sobre o fluxo de autenticação e a experiência do usuário.

Se você não precisa personalizar totalmente sua IU, permita que o IAP hospede uma página de login para você ou use a FirebaseUI para simplificar seu código.

Antes de começar

Ativar identidades externas e selecionar a opção Fornecerei minha própria IU durante a configuração.

Como instalar a biblioteca gcip-iap

O módulo gcip-iap do NPM abstrai as comunicações entre seu aplicativo, o IAP e o Identity Platform.

É altamente recomendável usar essa biblioteca. Com ela, é possível personalizar todo o fluxo de autenticação sem se preocupar com as trocas subjacentes entre a IU e o IAP.

Inclua a biblioteca como uma dependência da seguinte maneira:

// 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';

Como implementar o AuthenticationHandler

O módulo gcip-iap define uma interface chamada AuthenticationHandler. A biblioteca chama automaticamente os próprios métodos no momento mais adequado para processar a autenticação. A interface tem a seguinte aparência:

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;
}

Para personalizar a IU, você precisa criar uma classe personalizada que implemente a interface. Não há restrições quanto ao uso de qualquer framework do JavaScript. Veja nas seções abaixo mais informações sobre como criar cada método.

Como selecionar um locatário

Se você escolheu a multilocação, precisará selecionar um locatário antes de autenticar um usuário. Se você tiver apenas um locatário ou estiver usando a autenticação para envolvidos no projeto, pule esta etapa.

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

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

Para selecionar um locatário, a biblioteca invoca o callback selectTenant(). É possível implementar esse método de escolha do locatário de maneira programática ou por meio da exibição de uma IU para que o próprio usuário faça a seleção.

Para escolher um locatário de maneira programática, aproveite o contexto atual. A classe Authentication contém um método getOriginalURL() que retorna o URL que o usuário estava tentando acessar antes de precisar fazer a autenticação. Use isso para localizar um locatário correspondente 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);
  });
}

Se você preferir exibir uma IU, há várias abordagens para determinar qual provedor usar. Por exemplo, é possível exibir uma lista de locatários e deixar o usuário escolher um. Outra opção é pedir que o usuário digite o endereço de e-mail e encontrar um locatário 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,
          });
        });
  });
}

Independentemente da abordagem que você escolher, o objeto SelectedTenantInfo retornado será usado posteriormente 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.

Como conseguir o objeto Auth

Depois de definir um provedor, você precisará de uma maneira para receber o objeto Auth. Implemente o callback getAuth() para retornar uma instância de firebase.auth.Auth, que corresponda à chave de API e ao ID de locatário fornecidos. Se nenhum ID de locatário for fornecido, serão usados provedores de identidade no nível do projeto.

O getAuth() é usado para rastrear o local em que está armazenado o usuário correspondente à configuração fornecida. 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 utilizar uma instância exclusiva de Auth para cada um. 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():

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. This makes it possible to have
    // multiple users 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;
}

Como fazer login dos usuários

Para processar o login, implemente o callback startSignIn(). Ele exibe uma IU para que o usuário faça a autenticação. Em seguida, ele retorna um UserCredential ao concluir o login do usuário.

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 pelo callback selectTenant().

O exemplo abaixo mostra como implementar startSignIn() para um usuário atual com um e-mail e uma senha:

startSignIn(auth, selectedTenantInfo) {
  return new Promise((resolve, reject) => {
    // Show 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 user for email and password.
      // 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 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:

startSignIn(auth, selectedTenantInfo) {
  // Show UI to sign-in or sign-up a user.
  return new Promise((resolve, reject) => {
    // Provide user multiple buttons to sign-in.
    // For example sign-in with popup using a SAML provider.
    // 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 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);
  });
}

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

startSignIn(auth, selectedTenantInfo) {
  // Show 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.
    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 error message.
       });
    });
}

Consulte a documentação do Identity Platform para saber mais sobre a autenticação de usuários.

Como processar um usuário

Use o método opcional processUser() para modificar um usuário conectado antes de redirecionar para o recurso do IAP. Isso é útil para:

  • 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():

processUser(user) {
  return lastAuthUsed.getRedirectResult().then(function(result) {
    // Save additional data, or ask 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:

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;
  });
}

Como exibir uma IU de progresso

Implemente os callbacks opcionais showProgressBar() e hideProgressBar() para exibir uma IU personalizada que mostre ao usuário o progresso das operações sempre que o módulo gcip-iap executar tarefas em rede de longa duração.

Como processar os erros

O callback opcional handleError() pode ser usado para processar erros. Implemente-o para exibir mensagens de erro aos usuários ou tentar se recuperar de determinados erros, como quando o tempo limite de rede é atingido.

O exemplo abaixo mostra como implementar o 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 traz uma lista com os códigos de erro específicos do IAP que podem ser gerados. O Identity Platform também pode gerar erros. Para saber mais, 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 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. Geralmente, isso ocorre quando um usuário sai de todos os locatários associados a uma página de login. Nesse caso, implemente o callback completeSignOut() para exibir uma mensagem que indique que o usuário saiu com êxito. Se você não fizer isso, uma página em branco será exibida.

Como usar sua IU personalizada

Depois de criar uma classe que implemente 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