Personalizzare il flusso di autenticazione utilizzando le funzioni di blocco

Questo documento mostra come estendere l'autenticazione di Identity Platform mediante il blocco di Cloud Functions.

Le funzioni di blocco ti consentono di eseguire codice personalizzato che modifica il risultato della registrazione o dell'accesso di un utente alla tua app. Ad esempio, puoi impedire a un utente di autenticarsi se non soddisfa determinati criteri o aggiornare le informazioni di un utente prima di restituirlo all'app client.

Prima di iniziare

Creare un'app con Identity Platform. Per informazioni, consulta la Guida rapida.

Informazioni sulle funzioni di blocco

Puoi registrare funzioni di blocco per due eventi:

  • beforeCreate: gli attivatori vengono attivati prima che un nuovo utente venga salvato nel database Identity Platform e prima che venga restituito un token nell'app client.

  • beforeSignIn: si attiva dopo aver verificato le credenziali degli utenti, ma prima che Identity Platform restituisca un token ID all'app client. Se l'app utilizza l'autenticazione a più fattori, la funzione viene attivata dopo che l'utente verifica il secondo fattore. Tieni presente che la creazione di un nuovo utente attiva anche beforeSignIn, oltre a beforeCreate.

Quando utilizzi le funzioni di blocco, tieni presente quanto segue:

  • La funzione deve rispondere entro 7 secondi. Dopo 7 secondi, Identity Platform restituisce un errore e l'operazione del client non riesce.

  • I codici di risposta HTTP diversi da 200 vengono trasmessi alle app client. Assicurati che il codice client gestisca gli errori che la tua funzione può restituire.

  • Le funzioni si applicano a tutti gli utenti del progetto, inclusi quelli contenuti in un tenant. Identity Platform fornisce informazioni sugli utenti alla funzione, inclusi eventuali tenant a cui appartengono, per consentirti di rispondere.

  • Il collegamento di un altro provider di identità a un account riattiva le funzioni beforeSignIn registrate.

  • L'autenticazione anonima e personalizzata non supporta le funzioni di blocco.

Creazione di una funzione di blocco

I seguenti passaggi spiegano come creare una funzione di blocco:

  1. Vai alla pagina Settings (Impostazioni) di Identity Platform nella console.

    Vai alla pagina Impostazioni

  2. Seleziona la scheda Trigger.

  3. Per creare una funzione di blocco per la registrazione dell'utente, seleziona il menu a discesa Funzione in Prima di creare (beforeCreate), quindi fai clic su Crea funzione. Per creare una funzione di blocco per l'accesso dell'utente, crea una funzione in Prima di accedere (beforeSignIn).

  4. Crea una nuova funzione:

    1. Inserisci un Nome per la funzione.

    2. Nel campo Trigger, seleziona HTTP.

    3. Nel campo Autenticazione, seleziona Consenti chiamate non autenticate.

    4. Tocca Avanti.

  5. Apri l'editor incorporato per aprire index.js. Elimina il codice helloWorld di esempio e sostituiscilo con uno dei seguenti:

    Per rispondere alla registrazione:

    gcipCloudFunctions = require('gcip-cloud-functions');
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
      // TODO
    });
    

    Per rispondere all'accesso:

    gcipCloudFunctions = require('gcip-cloud-functions');
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      // TODO
    });
    
  6. Apri package.json e aggiungi il seguente blocco delle dipendenze:

    {
      "name": ...,
      "version": ...,
    
      "dependencies": {
        "gcip-cloud-functions": "^0.0.1"
      }
    }
    
  7. Imposta il punto di ingresso della funzione su beforeSignIn

  8. Fai clic su Esegui il deployment per pubblicare la funzione.

  9. Fai clic su Salva nella pagina delle funzioni di blocco di Identity Platform.

Consulta le sezioni seguenti per informazioni su come implementare la funzione. Devi eseguire nuovamente il deployment della funzione ogni volta che la aggiorni.

Puoi anche creare e gestire funzioni utilizzando l'interfaccia a riga di comando di Google Cloud o l'API REST. Consulta la documentazione di Cloud Functions per scoprire come utilizzare l'interfaccia a riga di comando di Google Cloud per eseguire il deployment di una funzione.

Recupero di informazioni su utente e contesto

Gli eventi beforeSignIn e beforeCreate forniscono oggetti User e EventContext contenenti informazioni sull'accesso dell'utente. Utilizza questi valori nel codice per determinare se consentire un'operazione per procedere.

Per un elenco delle proprietà disponibili nell'oggetto User, consulta il riferimento API UserRecord.

L'oggetto EventContext contiene le seguenti proprietà:

Nome Descrizione Esempio
locale Le impostazioni internazionali dell'applicazione. Puoi impostare le impostazioni internazionali usando l'SDK del client o passando l'intestazione delle impostazioni internazionali nell'API REST. fr o sv-SE
ipAddress L'indirizzo IP del dispositivo da cui l'utente finale si sta registrando o sta effettuando l'accesso. 114.14.200.1
userAgent Lo user agent che attiva la funzione di blocco. Mozilla/5.0 (X11; Linux x86_64)
eventId L'identificatore univoco dell'evento. rWsyPtolplG2TBFoOkkgyg
eventType Il tipo di evento. Fornisce informazioni sul nome evento, ad esempio beforeSignIn o beforeCreate, e sul metodo di accesso associato utilizzato, come Google o indirizzo email/password. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Sempre USER. USER
resource Il progetto o il tenant di Identity Platform. projects/project-id/tenants/tenant-id
timestamp La data e l'ora di attivazione dell'evento, formattate come stringa RFC 3339. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Un oggetto contenente informazioni sull'utente. AdditionalUserInfo
credential Un oggetto contenente informazioni sulle credenziali dell'utente. AuthCredential

Bloccare la registrazione o l'accesso

Per bloccare un tentativo di registrazione o di accesso, lancia un HttpsError nella tua funzione. Ad esempio:

Node.js

throw new gcipCloudFunctions.https.HttpsError('permission-denied');

La tabella riportata di seguito elenca gli errori che puoi segnalare e il relativo messaggio di errore predefinito:

Nome Codice Messaggio
invalid-argument 400 Il client ha specificato un argomento non valido.
failed-precondition 400 La richiesta non può essere eseguita nello stato attuale del sistema.
out-of-range 400 Il client ha specificato un intervallo non valido.
unauthenticated 401 Token OAuth mancante, non valido o scaduto.
permission-denied 403 Il client non dispone di autorizzazioni sufficienti.
not-found 404 La risorsa specificata non è stata trovata.
aborted 409 Conflitto di contemporaneità, ad esempio un conflitto di lettura-modifica-scrittura.
already-exists 409 La risorsa che un client ha cercato di creare esiste già.
resource-exhausted 429 Quota di risorse esaurita o vicina alla limitazione della frequenza.
cancelled 499 La richiesta è stata annullata dal client.
data-loss 500 Perdita di dati non recuperabili o danneggiamento dei dati.
unknown 500 Errore sconosciuto del server.
internal 500 Errore interno del server.
not-implemented 501 Metodo API non implementato dal server.
unavailable 503 Servizio non disponibile.
deadline-exceeded 504 Scadenza richiesta superata.

Puoi anche specificare un messaggio di errore personalizzato:

Node.js

throw new gcipCloudFunctions.https.HttpsError('permission-denied', 'Unauthorized request origin!');

L'esempio seguente mostra come impedire agli utenti che non si trovano in un dominio specifico di registrarsi per la tua app:

Node.js

// Import the Cloud Auth Admin module.
const gcipCloudFunctions = require('gcip-cloud-functions');
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Functions.
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  // If the user is authenticating within a tenant context, the tenant ID can be determined from
  // user.tenantId or from context.resource, eg. 'projects/project-id/tenant/tenant-id-1'
  // Only users of a specific domain can sign up.
  if (!user.email.endsWith('@acme.com')) {
    throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Indipendentemente dal fatto che utilizzi un messaggio predefinito o personalizzato, Cloud Functions esegue il wrapping dell'errore e lo restituisce al client come errore interno. Ad esempio, se nella tua funzione viene attivato il seguente errore:

throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);

Nell'app client viene restituito un errore simile al seguente (se utilizzi l'SDK client, l'errore viene aggregato come errore interno):

{
  "error": {
    "code": 400,
    "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
    "errors": [
      {
        "message": "BLOCKING_FUNCTION_ERROR_RESPONSE : HTTP Cloud Function returned an error. Code: 400, Status: \"INVALID_ARGUMENT\", Message: \"Unauthorized email user@evil.com\"",
        "domain": "global",
        "reason": "invalid"
      }
    ]
  }
}

L'app dovrebbe rilevare l'errore e gestirlo in modo adeguato. Ad esempio:

JavaScript

// Blocking functions can also be triggered in a multi-tenant context before user creation.
// firebase.auth().tenantId = 'tenant-id-1';
firebase.auth().createUserWithEmailAndPassword('johndoe@example.com', 'password')
  .then((result) => {
    result.user.getIdTokenResult()
  })
  .then((idTokenResult) => {
    console.log(idTokenResult.claim.admin);
  })
  .catch((error) => {
    if (error.code !== 'auth/internal-error' && error.message.indexOf('Cloud Function') !== -1) {
      // Display error.
    } else {
      // Registration succeeds.
    }
  });

Modificare un utente

Invece di bloccare un tentativo di registrazione o accesso, puoi consentire l'operazione continua, ma puoi modificare l'oggetto User che viene salvato nel database di Identity Platform e restituito al client.

Per modificare un utente, restituisci un oggetto dal gestore di eventi contenente i campi da modificare. Puoi modificare i seguenti campi:

  • displayName
  • disabled
  • emailVerified
  • photoURL
  • customClaims
  • sessionClaims (solo beforeSignIn)

Con l'eccezione di sessionClaims, tutti i campi modificati vengono salvati nel database di Identity Platform, il che significa che sono inclusi nel token di risposta e rimangono invariate tra una sessione utente e l'altra.

L'esempio seguente mostra come impostare un nome visualizzato predefinito:

Node.js

exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  return {
    // If no display name is provided, set it to "guest".
    displayName: user.displayName || 'guest'
  };
});

Se registri un gestore di eventi sia per beforeCreate sia per beforeSignIn, tieni presente che beforeSignIn viene eseguito dopo il giorno beforeCreate. I campi utente aggiornati in beforeCreate sono visibili in beforeSignIn. Se imposti un campo diverso da sessionClaims in entrambi i gestori di eventi, il valore impostato in beforeSignIn sovrascrive il valore impostato in beforeCreate. Solo per sessionClaims, vengono propagati alle richieste di token della sessione corrente, ma non vengono memorizzate in modo permanente o memorizzate nel database.

Ad esempio, se viene impostata una proprietà sessionClaims, beforeSignIn le restituirà con qualsiasi rivendicazione beforeCreate e verrà unita. Quando vengono unite, se una chiave sessionClaims corrisponde a una chiave in customClaims, la chiave corrispondente customClaims viene sovrascritta nelle rivendicazioni del token da sessionClaims. Tuttavia, la chiave customClaims sovrascritta continuerà a essere permanente nel database per le richieste future.

Credenziali e dati OAuth supportati

Puoi trasmettere le credenziali OAuth e i dati alle funzioni di blocco di vari provider di identità. La tabella seguente mostra le credenziali e i dati supportati per ogni provider di identità:

Provider di identità Token ID Token di accesso Data di scadenza Secret token Token di aggiornamento Richieste di accesso
Google Yes Yes No No
Facebook No Yes No No No
Twitter No No No No
GitHub No No No No No
Microsoft Yes Yes No No
LinkedIn No Yes No No No
Yahoo Yes Yes No No
Apple Yes Yes No No
SAML No No No No No
OIDC Yes Yes No

Token di aggiornamento

Per utilizzare un token di aggiornamento in una funzione di blocco, devi prima selezionare la casella di controllo nella sezione Trigger del menu a discesa Includi credenziali token nella console.

I token di aggiornamento non verranno restituiti da alcun provider di identità quando si accede direttamente con una credenziale OAuth, ad esempio un token ID o un token di accesso. In questa situazione, le stesse credenziali OAuth lato client verranno trasmesse alla funzione di blocco. Tuttavia, per i flussi a tre vie, potrebbe essere disponibile un token di aggiornamento se supportato dal provider di identità.

Le seguenti sezioni descrivono ogni tipo di provider di identità, con le relative credenziali e dati supportati.

Provider OIDC generici

Quando un utente accede con un provider OIDC generico, vengono trasmesse le seguenti credenziali:

  • Token ID: fornito se è selezionato il flusso id_token.
  • Token di accesso: fornito se il flusso di codice è selezionato. Tieni presente che il flusso di codice è attualmente supportato solo tramite l'API REST.
  • Aggiorna token: fornito se l'ambitooffline_access è selezionato.

Esempio:

const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Google

Quando un utente accede con Google, vengono trasmesse le seguenti credenziali:

  • Token ID
  • Token di accesso
  • Token di aggiornamento: fornito solo se vengono richiesti i seguenti parametri personalizzati:
    • access_type=offline
    • prompt=consent, se l'utente ha acconsentito in precedenza e non è stato richiesto un nuovo ambito

Esempio:

const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
  'access_type': 'offline',
  'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);

Scopri di più sui token di aggiornamento Google.

Facebook

Quando un utente accede a Facebook, vengono trasmesse le seguenti credenziali:

  • Token di accesso: viene restituito un token di accesso che può essere scambiato con un altro token. Scopri di più sui diversi tipi di token di accesso supportati da Facebook e su come scambiarli con token a lunga durata.

GitHub

Quando un utente accede con GitHub, vengono trasmesse le seguenti credenziali:

  • Token di accesso: non scade a meno che non venga revocato.

Microsoft

Quando un utente accede a Microsoft, vengono trasmesse le seguenti credenziali:

  • Token ID
  • Token di accesso
  • Token di aggiornamento: Passato alla funzione di blocco se offline_access l'ambito è selezionato.

Esempio:

const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);

Yahoo

Quando un utente accede a Yahoo, le seguenti credenziali vengono trasmesse senza parametri o ambiti personalizzati:

  • Token ID
  • Token di accesso
  • Aggiorna token

LinkedIn

Quando un utente accede con LinkedIn, vengono trasmesse le seguenti credenziali:

  • Token di accesso

Apple

Quando un utente accede ad Apple, le seguenti credenziali vengono trasmesse senza parametri o ambiti personalizzati:

  • Token ID
  • Token di accesso
  • Aggiorna token

Scenari comuni

I seguenti esempi illustrano alcuni casi d'uso comuni delle funzioni di blocco:

Consentire la registrazione solo da un dominio specifico.

Il seguente esempio mostra come impedire agli utenti che non fanno parte del dominio example.com di registrarsi alla tua app:

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (!user.email || user.email.indexOf('@example.com') === -1) {
    throw new gcipCloudFunctions.https.HttpsError(
      'invalid-argument', `Unauthorized email "${user.email}"`);
  }
});

Impedire agli utenti con indirizzi email non verificati di registrarsi

L'esempio seguente mostra come impedire agli utenti con email non verificate di registrarsi nella tua app:

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (user.email && !user.emailVerified) {
    throw new gcipCloudFunctions.https.HttpsError(
      'invalid-argument', `Unverified email "${user.email}"`);
  }
});

Richiesta di verifica e-mail al momento della registrazione

L'esempio seguente mostra come richiedere a un utente di verificare il proprio indirizzo email dopo aver effettuato la registrazione:

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  const locale = context.locale;
  if (user.email && !user.emailVerified) {
    // Send custom email verification on sign-up.
    return admin.auth().generateEmailVerificationLink(user.email).then((link) => {
      return sendCustomVerificationEmail(user.email, link, locale);
    });
  }
});

export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
 if (user.email && !user.emailVerified) {
   throw new gcipCloudFunctions.https.HttpsError(
     'invalid-argument', `"${user.email}" needs to be verified before access is granted.`);
  }
});

Considerare determinate email del provider di identità come verificate

L'esempio seguente mostra come trattare le email utente di determinati provider di identità come verificate:

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
    return {
      emailVerified: true,
    };
  }
});

Bloccare l'accesso da determinati indirizzi IP

L'esempio seguente mostra come bloccare l'accesso da determinati intervalli di indirizzi IP:

Node.js

export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
  if (isSuspiciousIpAddress(context.ipAddress)) {
    throw new gcipCloudFunctions.https.HttpsError(
      'permission-denied', 'Unauthorized access!');
  }
});

Impostare rivendicazioni personalizzate e sessioni

L'esempio seguente mostra come impostare rivendicazioni personalizzate e personalizzate di sessione:

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'saml.my-provider-id') {
    return {
      // Employee ID does not change so save in persistent claims (stored in
      // Auth DB).
      customClaims: {
        eid: context.credential.claims.employeeid,
      },
      // Copy role and groups to token claims. These will not be persisted.
      sessionClaims: {
        role: context.credential.claims.role,
        groups: context.credential.claims.groups,
      }
    }
  }
});

Monitorare gli indirizzi IP per monitorare le attività sospette

Per prevenire il furto di token, puoi monitorare l'indirizzo IP da cui un utente esegue l'accesso e confrontarlo con l'indirizzo IP nelle richieste successive. Se la richiesta sembra sospetta, ad esempio gli IP di diverse aree geografiche, puoi chiedere all'utente di eseguire nuovamente l'accesso.

  1. Utilizza le rivendicazioni della sessione per monitorare l'indirizzo IP con cui l'utente accede:

    Node.js

    export.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Quando un utente tenta di accedere alle risorse che richiedono l'autenticazione con Identity Platform, confronta l'indirizzo IP della richiesta con l'IP utilizzato per l'accesso:

    Node.js

    app.post('/getRestrictedData', (req, res) => {
      // Get the ID token passed.
      const idToken = req.body.idToken;
      // Verify the ID token, check if revoked and decode its payload.
      admin.auth().verifyIdToken(idToken, true).then((claims) => {
        // Get request IP address
        const requestIpAddress = req.connection.remoteAddress;
        // Get sign-in IP address.
        const signInIpAddress = claims.signInIpAddress;
        // Check if the request IP address origin is suspicious relative to
        // the session IP addresses. The current request timestamp and the
        // auth_time of the ID token can provide additional signals of abuse,
        // especially if the IP address suddenly changed. If there was a sudden
        // geographical change in a short period of time, then it will give
        // stronger signals of possible abuse.
        if (!isSuspiciousIpAddressChange(signInIpAddress, requestIpAddress)) {
          // Suspicious IP address change. Require re-authentication.
          // You can also revoke all user sessions by calling:
          // admin.auth().revokeRefreshTokens(claims.sub).
          res.status(401).send({error: 'Unauthorized access. Please login again!'});
        } else {
          // Access is valid. Try to return data.
          getData(claims).then(data => {
            res.end(JSON.stringify(data);
          }, error => {
            res.status(500).send({ error: 'Server error!' })
          });
        }
      });
    });
    

Filtro delle foto degli utenti

L'esempio seguente mostra come igienizzare le foto del profilo degli utenti:

Node.js

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (user.photoURL) {
    return isPhotoAppropriate(user.photoURL)
      .then((status) => {
        if (!status) {
          // Sanitize inappropriate photos by replacing them with guest photos.
          // Users could also be blocked from sign-up, disabled, etc.
          return {
            photoURL: PLACEHOLDER_GUEST_PHOTO_URL,
          };
        }
      });
});

Per scoprire di più su come rilevare e disinfettare le immagini, consulta la documentazione di Cloud Vision.

Accesso alle credenziali OAuth del provider di identità di un utente

L'esempio seguente mostra come ottenere un token di aggiornamento per un utente che ha eseguito l'accesso con Google e utilizzarlo per chiamare le API Google Calendar. Il token di aggiornamento viene memorizzato per l'accesso offline.

Node.js

const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
  keys.web.client_id,
  keys.web.client_secret
);

export.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
  if (context.credential &&
      context.credential.providerId === 'google.com') {
    // Store the refresh token for later offline use.
    // These will only be returned if refresh tokens credentials are included
    // (enabled by Cloud console).
    return saveUserRefreshToken(
        user.uid,
        context.credential.refreshToken,
        'google.com'
      )
      .then(() => {
        // Blocking the function is not required. The function can resolve while
        // this operation continues to run in the background.
        return new Promise((resolve, reject) => {
          // For this operation to succeed, the appropriate OAuth scope should be requested
          // on sign in with Google, client-side. In this case:
          // https://www.googleapis.com/auth/calendar
          // You can check granted_scopes from within:
          // context.additionalUserInfo.profile.granted_scopes (space joined list of scopes).

          // Set access token/refresh token.
          oAuth2Client.setCredentials({
            access_token: context.credential.accessToken,
            refresh_token: context.credential.refreshToken,
          });
          const calendar = google.calendar('v3');
          // Setup Onboarding event on user's calendar.
          const event = {/** ... */};
          calendar.events.insert({
            auth: oauth2client,
            calendarId: 'primary',
            resource: event,
          }, (err, event) => {
            // Do not fail. This is a best effort approach.
            resolve();
          });
      });
    })
  }
});

Passaggi successivi