Criar uma página de início de sessão personalizada

Este artigo mostra como criar a sua própria página de autenticação com identidades externas e IAP. A criação desta página por si confere-lhe controlo total sobre o fluxo de autenticação e a experiência do utilizador.

Se não precisar de personalizar totalmente a IU, pode permitir que a IAP aloje uma página de início de sessão por si ou usar a FirebaseUI para uma experiência mais simplificada.

Vista geral

Para criar a sua própria página de autenticação, siga estes passos:

  1. Ative as identidades externas. Selecione Vou fornecer a minha própria opção de IU durante a configuração.
  2. Instale a biblioteca gcip-iap.
  3. Configure a IU implementando a interface AuthenticationHandler. A sua página de autenticação tem de processar os seguintes cenários:
    • Seleção de inquilinos
    • Autorização do utilizador
    • Início de sessão do utilizador
    • Processamento de erros
  4. Opcional: personalize a sua página de autenticação com funcionalidades adicionais, como barras de progresso, páginas de saída e processamento de utilizadores.
  5. Teste a IU.

Instalar a biblioteca gcip-iap

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

npm install gcip-iap --save

O módulo gcip-iapNPM abstrai as comunicações entre a sua aplicação, a IAP e a Identity Platform. Isto permite-lhe personalizar todo o fluxo de autenticação sem ter de gerir as trocas subjacentes entre a IU e a IAP.

Use as importações corretas para a versão do seu 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 pares firebase v9 ou superior. Se estiver a migrar para a versão gcip-iap 1.0.0 ou superior, conclua as seguintes ações:

  • Atualize a versão do firebase no ficheiro package.json para v9.6.0 ou superior.
  • Atualize as firebasedeclarações de importação da seguinte forma:
// 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 são necessárias alterações adicionais ao código.

gcip-iap v2.0.0

A partir da versão v2.0.0, o gcip-iap requer a reescrita da sua aplicação de IU personalizada através do formato de SDK modular. Se estiver a migrar para a versão gcip-iap 2.0.0 ou superior, conclua as seguintes ações:

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

Configurar a IU

Para configurar a IU, 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.

Selecionar inquilinos

Para selecionar um inquilino, implemente selectTenant(). Pode implementar este método para escolher um inquilino programaticamente ou apresentar uma IU para que o utilizador possa selecionar um.

Em qualquer dos casos, a biblioteca usa o objeto SelectedTenantInfo devolvido para concluir o fluxo de autenticação. Contém o ID do inquilino selecionado, os IDs de fornecedor e o email introduzido pelo utilizador.

Se tiver vários inquilinos no seu projeto, tem de selecionar um antes de poder autenticar um utilizador. Se tiver apenas um inquilino ou estiver a usar a autenticação ao nível do projeto, não precisa de implementar selectTenant().

O IAP suporta os mesmos fornecedores que a Identity Platform, como:

  • Email e palavra-passe
  • 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, personalizados e anónimos não são suportados para a multilocação.

Selecionar inquilinos programaticamente

Para selecionar um inquilino de forma programática, tire partido do contexto atual. A classe Authentication contém getOriginalURL() que devolve o URL ao qual o utilizador estava a aceder antes da autenticação.

Use esta opção para localizar uma correspondência numa lista de inquilinos 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 utilizadores selecionem inquilinos

Para permitir que o utilizador selecione um inquilino, apresente uma lista de inquilinos e peça ao utilizador para escolher um ou peça-lhe para introduzir o respetivo endereço de email 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,
          });
        });
  });
}

Autenticação de utilizadores

Depois de ter um fornecedor, implemente getAuth() para devolver uma instância Auth, correspondente à chave da API e ao ID do inquilino fornecidos. Se não for fornecido nenhum ID do inquilino, use fornecedores de identidade ao nível do projeto.

getAuth() acompanha a localização onde o utilizador correspondente à configuração fornecida está armazenado. Também permite atualizar silenciosamente o token de ID da Identity Platform de um utilizador autenticado anteriormente sem exigir que o utilizador introduza novamente as respetivas credenciais.

Se estiver a usar vários recursos de IAP com inquilinos diferentes, recomendamos que use uma instância de autenticação exclusiva para cada recurso. Isto permite que vários recursos com configurações diferentes usem a mesma página de autenticação. Também permite que vários utilizadores iniciem sessão em simultâneo sem terminar a sessão do utilizador anterior.

Segue-se um exemplo de como implementar 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;
}

Iniciar sessão dos utilizadores

Para processar o início de sessão, implemente startSignIn(), apresente uma IU para o utilizador se autenticar e, em seguida, devolva um UserCredential para o utilizador com sessão iniciada na conclusão.

Num ambiente multiinquilino, pode determinar os métodos de autenticação disponíveis a partir de SelectedTenantInfo, se tiver sido fornecido. Esta variável contém as mesmas informações devolvidas por selectTenant().

O exemplo seguinte mostra uma implementação de startSignIn() para um utilizador existente com um email e uma palavra-passe:

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 pode iniciar sessão nos utilizadores com um fornecedor federado, como SAML ou OIDC, através de um pop-up ou um 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 fornecedores de OAuth suportam a transmissão de uma sugestão de início de sessão:

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 o artigo Autenticação com multi-tenancy para mais informações.

Processamento de erros

Para apresentar mensagens de erro aos utilizadores ou tentar a recuperação de erros, como limites de tempo de rede, implemente handleError().

O exemplo seguinte 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 apresenta códigos de erro específicos da IAP que podem ser devolvidos. O Identity Platform também pode devolver erros. Consulte a documentação para firebase.auth.Auth.

Código de erro Descrição
invalid-argument O cliente especificou um argumento inválido.
failed-precondition Não é possível executar o pedido no estado atual do sistema.
out-of-range O cliente especificou uma alteração inválida.
unauthenticated Pedido não autenticado devido a um token OAuth em falta, inválido ou expirado.
permission-denied O cliente não tem autorização suficiente ou a IU está alojada num domínio não autorizado.
not-found . Não foi possível encontrar o recurso especificado.
aborted Conflito de simultaneidade, tal como conflito de ler-modificar-gravar.
already-exists O recurso que um cliente tentou criar já existe.
resource-exhausted Sem quota de recursos ou a atingir a limitação de velocidade.
cancelled Pedido cancelado pelo cliente.
data-loss Perda de dados irrecuperável ou corrupção de dados.
unknown Erro de servidor desconhecido.
internal Erro interno do servidor.
not-implemented Método de API não implementado pelo servidor.
unavailable Serviço indisponível.
restart-process Aceda novamente ao URL que o redirecionou para esta página, para reiniciar o processo de autenticação.
deadline-exceeded Prazo do pedido excedido.
authentication-uri-fail Falha ao gerar o URI de autenticação.
gcip-token-invalid Foi fornecido um token de ID do GCIP inválido.
gcip-redirect-invalid URL de redirecionamento inválido.
get-project-mapping-fail Não foi possível obter o ID do projeto.
gcip-id-token-encryption-error Erro de encriptação do token de ID da GCIP.
gcip-id-token-decryption-error Erro de desencriptação do token de ID da GCIP.
gcip-id-token-unescape-error Falha ao anular a escape base64 segura para a Web.
resource-missing-gcip-sign-in-url URL de autenticação do GCIP em falta para o recurso do IAP especificado.

Personalizar a IU

Pode personalizar a página de autenticação com funcionalidades opcionais, como barras de progresso e páginas de encerramento de sessão.

Apresentar uma IU de progresso

Para apresentar uma IU de progresso personalizada ao utilizador sempre que o módulo gcip-iap executar tarefas de rede de longa duração, implemente showProgressBar() e hideProgressBar().

Terminar sessão dos utilizadores

Em alguns casos, pode querer permitir que os utilizadores terminem sessão em todas as sessões atuais que partilham o mesmo URL de autenticação.

Depois de um utilizador terminar sessão, pode não existir um URL para o redirecionar de volta. Isto ocorre normalmente quando um utilizador termina sessão em todos os inquilinos associados a uma página de início de sessão. Neste caso, implemente completeSignOut() para apresentar uma mensagem a indicar que o utilizador terminou sessão com êxito. Se não implementar este método, é apresentada uma página em branco quando um utilizador termina sessão.

Processamento de utilizadores

Para modificar um utilizador com sessão iniciada antes de redirecionar para o recurso de IAP, implemente processUser().

Pode usar este método para fazer o seguinte:

  • Link para fornecedores adicionais.
  • Atualize o perfil do utilizador.
  • Pedir dados adicionais ao utilizador após o registo.
  • Processe as chaves de acesso OAuth devolvidas por getRedirectResult() depois de chamar signInWithRedirect().

Segue-se um exemplo de implementação de 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 quiser que as alterações a um utilizador se reflitam nas reivindicações do token de ID propagadas pela IAP para a sua app, tem de 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;
  });
}

Testar a IU

Depois de criar uma classe que implemente AuthenticationHandler, pode usá-la para criar uma nova instância Authentication e iniciá-la:

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

Implemente a sua aplicação e navegue para a página de autenticação. Deve ver a IU de início de sessão personalizada.

O que se segue?