Creazione di una pagina di accesso personalizzata

Per utilizzare identità esterne con Identity-Aware Proxy (IAP), la tua app richiede una pagina di accesso. IAP reindirizzerà gli utenti a questa pagina per autenticarsi prima che possano accedere alle risorse sicure.

Questo articolo spiega come creare una pagina di autenticazione da zero. La creazione indipendente di questa pagina è complessa, ma ti offre un controllo completo sul flusso di autenticazione e sull'esperienza utente.

Se non hai la necessità di personalizzare completamente la tua interfaccia utente, puoi lasciare che sia IAP a ospitare una pagina di accesso per te, oppure a utilizzare FirebaseUI, per semplificare il codice.

Prima di iniziare

Abilita identità esterne e seleziona l'opzione Fornire la mia interfaccia utente durante la configurazione.

Installazione della libreria gcip-iap

Il modulo NPM gcip-iap nasconde le comunicazioni tra la tua applicazione, IAP e Identity Platform.

Ti consigliamo vivamente di utilizzare la raccolta. Ti consente di personalizzare l'intero flusso di autenticazione senza preoccuparti delle piattaforme di scambio sottostanti tra l'interfaccia utente e gli IAP.

Includi la libreria come una dipendenza simile alla seguente:

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

Le istruzioni precedenti riguardano gcip-iap 0.1.4 o versioni precedenti.

A partire dalla versione 1.0.0, gcip-iap richiede la dipendenza peer v9 firebase o superiore. Se stai eseguendo la migrazione alla gcip-iap v1.0.0 o successiva, completa le seguenti azioni:

  • Aggiorna la versione firebase del file package.json alla versione 9.6.0 o successiva.
  • Aggiorna le istruzioni di importazione firebase come segue.

Non sono necessarie ulteriori modifiche al codice:

// Import firebase modules.
import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
// Import gcip-iap module.
import * as ciap from 'gcip-iap';

Implementazione di AuthenticationHandler

Il modulo gcip-iap definisce un'interfaccia denominata AuthenticationHandler. La libreria chiama automaticamente i propri metodi al momento opportuno per gestire l'autenticazione. L'interfaccia ha il seguente aspetto:

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

Per personalizzare l'interfaccia utente, devi creare una classe personalizzata che implementi l'interfaccia. Non esistono limitazioni relative al framework JavaScript che utilizzi. Le seguenti sezioni forniscono ulteriori informazioni su come creare ogni metodo.

Selezione di un tenant

Se utilizzi la multi-tenancy, devi selezionare un tenant prima di poter autenticare un utente. Se hai un solo tenant o utilizzi l'autenticazione a livello di progetto, puoi saltare questo passaggio.

IAP supporta gli stessi provider di Identity Platform, ad esempio:

  • Email e password
  • OAuth (Google, Facebook, Twitter, GitHub, Microsoft ecc.)
  • SAML
  • OIDC
  • Numero di telefono
  • Personalizzato
  • Anonimo

Tieni presente che numero di telefono, autenticazione personalizzata e autenticazione non sono supportate per la modalità multi-tenancy.

Per selezionare un tenant, la libreria richiama il callback selectTenant(). Puoi implementare questo metodo per scegliere un tenant in modo programmatico o visualizzare una UI in modo che l'utente possa selezionarne una da sola.

Per scegliere un tenant in modo programmatico, sfrutta il contesto attuale. La classe Authentication contiene un metodo getOriginalURL() che restituisce l'URL a cui l'utente stava accedendo prima di dover eseguire l'autenticazione. che potrai utilizzare per individuare una corrispondenza da un elenco di tenant associati.

// 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 scegli di visualizzare un'interfaccia utente, esistono diversi approcci per determinare il provider da utilizzare. Ad esempio, potresti visualizzare un elenco di inquilini e chiedere all'utente di sceglierne uno, oppure puoi inserire il suo indirizzo email e individuare una corrispondenza in base al dominio.

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

Indipendentemente dal tuo approccio, l'oggetto SelectedTenantInfo restituito verrà utilizzato in un secondo momento per completare il flusso di autenticazione. Contiene l'ID del tenant selezionato, gli eventuali ID provider e l'email inserita dall'utente.

Recupero dell'oggetto Auth

Dopo avere ottenuto un provider, devi ottenere un modo per ottenere un oggetto Auth. Implementa il callback getAuth() per restituire un'istanza firebase.auth.Auth corrispondente alla chiave API e all'ID tenant forniti. Se non viene fornito un ID tenant, deve utilizzare i provider di identità a livello di progetto.

getAuth() viene utilizzato per monitorare dove viene archiviato l'utente corrispondente alla configurazione fornita. Consente inoltre di aggiornare automaticamente il token ID Identity Platform di un utente autenticato in precedenza senza richiedere all'utente di reinserire le proprie credenziali.

Se utilizzi più risorse IAP con tenant diversi, ti consigliamo di utilizzare un'istanza Auth univoca per ciascuna. In questo modo, più risorse con configurazioni diverse possono utilizzare la stessa pagina di autenticazione. Inoltre, permette a più utenti di accedere contemporaneamente senza disconnettere l'utente precedente.

Di seguito è riportato un esempio di come implementare 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;
}

Accesso degli utenti

Per gestire l'accesso, implementa il callback startSignIn(). Dovrebbe essere visualizzata un'interfaccia utente per l'autenticazione, quindi restituire una UserCredential per l'utente che ha eseguito l'accesso al termine.

In un ambiente multi-tenant, i metodi di autenticazione disponibili possono essere determinati da SelectedTenantInfo, se fornito. Questa variabile contiene le stesse informazioni restituite dal callback selectTenant().

L'esempio seguente mostra come implementare startSignIn() per un utente esistente con un indirizzo email e una password:

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

Puoi anche accedere agli utenti con un provider federato, come SAML o OIDC, utilizzando un popup o un reindirizzamento:

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

Alcuni provider OAuth supportano il passaggio di suggerimenti per l'accesso:

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

Consulta la documentazione di Identity Platform per scoprire di più sull'autenticazione degli utenti.

Elaborazione di un utente

Il metodo facoltativo processUser() consente di modificare un utente che ha eseguito l'accesso prima di reindirizzare alla risorsa IAP. Puoi utilizzarlo per:

  • Link a fornitori aggiuntivi.
  • Aggiorna il profilo dell'utente.
  • Chiedi all'utente ulteriori dati dopo la registrazione.
  • Elabora i token di accesso OAuth restituiti da getRedirectResult() dopo la chiamata a signInWithRedirect().

Di seguito è riportato un esempio di implementazione di 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;
  });
}

Tieni presente che se vuoi apportare eventuali modifiche a un utente nelle rivendicazioni del token ID propagate da IAP nella tua app, dovrai forzare l'aggiornamento del 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;
  });
}

Visualizzazione di un'interfaccia utente di avanzamento

Implementa i callback facoltativi showProgressBar() e hideProgressBar() per visualizzare un'interfaccia utente di avanzamento personalizzata per l'utente ogni volta che il modulo gcip-iap esegue attività di rete di lunga durata.

Gestione degli errori

handleError() è un callback facoltativo per la gestione degli errori. Implementalo per mostrare messaggi di errore agli utenti o tentare di recuperarli da determinati errori (come un timeout di rete).

L'esempio seguente mostra come implementare 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;
  });
}

La tabella seguente elenca i codici di errore specifici di IAP che possono essere generati. Identity Platform può anche generare errori; consulta la documentazione per firebase.auth.Auth.

Codice di errore Descrizione
invalid-argument Il client ha specificato un argomento non valido.
failed-precondition La richiesta non può essere eseguita nello stato attuale del sistema.
out-of-range Il client ha specificato un intervallo non valido.
unauthenticated Richiesta non autenticata a causa di un token OAuth mancante, non valido o scaduto.
permission-denied Il client non dispone di autorizzazioni sufficienti oppure l'interfaccia utente è ospitata su un dominio non autorizzato.
not-found La risorsa specificata non è stata trovata.
aborted Conflitto di contemporaneità, ad esempio conflitto di lettura, modifica e scrittura.
already-exists La risorsa che un client ha cercato di creare esiste già.
resource-exhausted Quota di risorse esaurita o vicina alla limitazione della frequenza.
cancelled La richiesta è stata annullata dal client.
data-loss Perdita di dati non recuperabili o danneggiamento dei dati.
unknown Errore sconosciuto del server.
internal Errore interno del server.
not-implemented Metodo API non implementato dal server.
unavailable Servizio non disponibile.
restart-process Consulta l'URL che ti ha reindirizzato a questa pagina per riavviare la procedura di autenticazione.
deadline-exceeded Scadenza richiesta superata.
authentication-uri-fail Impossibile generare l'URI di autenticazione.
gcip-token-invalid Token ID GCIP fornito non valido.
gcip-redirect-invalid URL di reindirizzamento non valido.
get-project-mapping-fail Impossibile ottenere l'ID progetto.
gcip-id-token-encryption-error Errore di crittografia del token ID GCIP.
gcip-id-token-decryption-error Errore di decriptazione del token ID GCIP.
gcip-id-token-unescape-error Annullamento avanzamento Base64 sicuro per il Web non riuscito.
resource-missing-gcip-sign-in-url URL di autenticazione GCIP mancante per la risorsa IAP specificata.

Disconnessione degli utenti

In alcuni casi, potresti voler consentire agli utenti di uscire da tutte le sessioni in corso che condividono lo stesso URL di autenticazione.

Dopo che un utente si disconnette, potrebbe non esistere alcun URL per reindirizzare l'utente a (questo accade di solito quando un utente si disconnette da tutti i tenant associati a una pagina di accesso). In questo caso, implementa il callback completeSignOut() per visualizzare un messaggio che indica che l'utente è stato disconnesso. In caso contrario, verrà visualizzata una pagina vuota.

Utilizzo dell'interfaccia utente personalizzata

Dopo aver creato una classe che implementa AuthenticationHandler, puoi utilizzarla per creare una nuova istanza Authentication e avviarla.

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

Esegui il deployment della tua applicazione e vai alla pagina di autenticazione. Dovresti visualizzare la tua interfaccia utente di accesso personalizzata.

Passaggi successivi