Personalizzazione del flusso di autenticazione mediante 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 l'autenticazione di un utente se non soddisfa determinati criteri oppure aggiornare le informazioni di un utente prima di restituirlo all'app client.
Prima di iniziare
Creare un'app con Identity Platform. Per scoprire come, consulta la guida rapida.
Informazioni sulle funzioni di blocco
Puoi registrare le funzioni di blocco per due eventi:
beforeCreate
: si attiva prima che un nuovo utente venga salvato nel database di Identity Platform e prima che un token venga restituito all'app client.beforeSignIn
: si attiva dopo che le credenziali dell'utente sono state verificate, ma prima che Identity Platform restituisca un token ID all'app client. Se l'app utilizza l'autenticazione a più fattori, la funzione si attiva dopo che l'utente ha verificato il secondo fattore. Tieni presente che la creazione di un nuovo utente attiva anchebeforeSignIn
, oltre abeforeCreate
.
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 va a buon fine.
I codici di risposta HTTP diversi da
200
vengono trasmessi alle tue app client. Assicurati che il codice client gestisca gli eventuali errori che la 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 i tenant a cui appartengono, in modo che tu possa rispondere di conseguenza.
Il collegamento di un altro provider di identità a un account riattiva qualsiasi funzione
beforeSignIn
registrata. Non sono inclusi i provider email e password.L'autenticazione anonima e personalizzata non supporta le funzioni di blocco.
Se utilizzi anche funzioni asincrone, l'oggetto utente ricevuto da una funzione asincrona non contiene aggiornamenti dalla funzione di blocco.
Creazione di una funzione di blocco
I passaggi seguenti spiegano come creare una funzione di blocco:
Vai alla pagina Impostazioni di Identity Platform nella console Google Cloud.
Seleziona la scheda Attivatori.
Per creare una funzione di blocco per la registrazione degli utenti, seleziona il menu a discesa Funzione in Prima della creazione (beforeCreate) e fai clic su Crea funzione. Per creare una funzione di blocco per l'accesso degli utenti, crea una funzione in Prima dell'accesso (beforeSignIn).
Crea una nuova funzione:
Inserisci un Nome per la funzione.
Nel campo Attivatore, seleziona HTTP.
Nel campo Autenticazione, seleziona Consenti chiamate non autenticate.
Tocca Avanti.
Utilizzando l'editor incorporato, apri
index.js
. Elimina il codicehelloWorld
di esempio e sostituiscilo con uno dei seguenti:Per rispondere alla registrazione:
import gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => { // TODO });
Per rispondere all'accesso:
import gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { // TODO });
Apri
package.json
e aggiungi il seguente blocco di dipendenze: Per la versione più recente dell'SDK, consultagcip-cloud-functions
.{ "type": "module", "name": ..., "version": ..., "dependencies": { "gcip-cloud-functions": "^0.2.0" } }
Imposta il punto di ingresso della funzione su
beforeSignIn
Fai clic su Esegui il deployment per pubblicare la funzione.
Fai clic su Salva nella pagina Funzioni di blocco di Identity Platform.
Consulta le sezioni seguenti per scoprire come implementare la funzione. Devi eseguire nuovamente il deployment della funzione ogni volta che la aggiorni.
Puoi anche creare e gestire le funzioni utilizzando Google Cloud CLI o l'API REST. Consulta la documentazione di Cloud Functions per scoprire come utilizzare Google Cloud CLI per eseguire il deployment di una funzione.
Recupero di informazioni sull'utente e sul contesto
Gli eventi beforeSignIn
e beforeCreate
forniscono oggetti User
e EventContext
che contengono informazioni sull'accesso dell'utente. Utilizza questi valori nel codice per determinare se consentire il proseguimento di un'operazione.
Per un elenco delle proprietà disponibili nell'oggetto User
, consulta la documentazione di riferimento sull'API UserRecord
.
L'oggetto EventContext
contiene le seguenti proprietà:
Nome | Descrizione | Esempio |
---|---|---|
locale |
Le impostazioni internazionali dell'applicazione. Puoi impostare le impostazioni internazionali utilizzando 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 da cui esegue 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 dell'evento, ad esempio
beforeSignIn o beforeCreate , e sul
metodo di accesso associato utilizzato, come Google o email/password.
|
providers/cloud.auth/eventTypes/user.beforeSignIn:password
|
authType
| Sempre USER . |
USER
|
resource
| Il progetto o tenant Identity Platform. |
projects/project-id/tenants/tenant-id
|
timestamp
| L'ora in cui l'evento è stato attivato, formattata 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
|
Blocco della registrazione o dell'accesso
Per bloccare un tentativo di registrazione o accesso, inserisci un HttpsError
nella funzione. Ad esempio:
Node.js
throw new gcipCloudFunctions.https.HttpsError('permission-denied');
La tabella seguente elenca gli errori che puoi segnalare, insieme al 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 e 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 all'interno di un dominio specifico di registrarsi per la tua app:
Node.js
// Import the Cloud Auth Admin module.
import gcipCloudFunctions from '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 funzione generi il seguente errore:
throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);
Nella tua app client viene restituito un errore simile al seguente (se utilizzi l'SDK client, l'errore è riportato 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 di conseguenza. 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 continuare, ma 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. È possibile modificare i seguenti campi:
displayName
disabled
emailVerified
photoURL
customClaims
sessionClaims
(solobeforeSignIn
)
Ad 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 vengono mantenuti 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 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 propagate alle rivendicazioni dei token della sessione corrente, ma non vengono salvate in modo permanente o archiviate nel database.
Ad esempio, se sono presenti sessionClaims
, beforeSignIn
li restituirà
con eventuali rivendicazioni beforeCreate
, che verranno uniti. Quando vengono unite, se una chiave sessionClaims
corrisponde a una chiave in customClaims
, la chiave customClaims
corrispondente verrà sovrascritta nelle rivendicazioni dei token dalla chiave sessionClaims
. Tuttavia, la chiave customClaims
sovrascritta è ancora salvata nel database per le richieste future.
Dati e credenziali OAuth supportati
Puoi passare dati e credenziali OAuth alle funzioni di blocco di vari provider di identità. La seguente tabella mostra le credenziali e i dati supportati per ciascun provider di identità:
Provider di identità | Token ID | Token di accesso | Data di scadenza | Secret del token | Aggiorna token | Richieste di accesso |
---|---|---|---|---|---|---|
Sì | Sì | Sì | No | Sì | No | |
No | Sì | Sì | No | No | No | |
No | Sì | No | Sì | No | No | |
GitHub | No | Sì | No | No | No | No |
Microsoft | Sì | Sì | Sì | No | Sì | No |
No | Sì | Sì | No | No | No | |
Yahoo | Sì | Sì | Sì | No | Sì | No |
Apple | Sì | Sì | Sì | No | Sì | No |
SAML | No | No | No | No | No | Sì |
OIDC | Sì | Sì | Sì | No | Sì | Sì |
Token di aggiornamento
Per utilizzare un token di aggiornamento in una funzione di blocco, devi prima selezionare la casella di controllo nella sezione Attivatori del menu a discesa Includi credenziali token nella console Google Cloud.
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, la stessa credenziale OAuth lato client verrà passata alla funzione di blocco. Tuttavia, per i flussi a tre vie, potrebbe essere disponibile un token di aggiornamento se il provider di identità lo supporta.
Le seguenti sezioni descrivono ogni tipo di provider di identità e i relativi dati e credenziali supportati.
Provider OIDC generici
Quando un utente accede con un provider OIDC generico, verranno trasmesse le seguenti credenziali:
- Token ID: fornito se è selezionato il flusso
id_token
. - Token di accesso: fornito se è selezionato il flusso di codice. Tieni presente che il flusso di codice è attualmente supportato solo tramite l'API REST.
- Aggiorna token: fornito se è selezionato l'ambito
offline_access
.
Esempio:
const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Quando un utente accede con Google, vengono trasmesse le seguenti credenziali:
- Token ID
- Token di accesso
- Token di aggiornamento: fornito solo se sono richiesti i seguenti parametri personalizzati:
access_type=offline
prompt=consent
, se l'utente ha precedentemente acconsentito 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 di Google.
Quando un utente accede con Facebook, verranno passate le seguenti credenziali:
- Token di accesso: viene restituito un token di accesso che può essere scambiato con un altro token di accesso. Scopri di più sui diversi tipi di token di accesso supportati da Facebook e su come scambiarli con token di lunga durata.
GitHub
Quando un utente accede con GitHub, verranno trasmesse le seguenti credenziali:
- Token di accesso: non scade se non viene revocato.
Microsoft
Quando un utente accede con Microsoft, vengono passate le seguenti credenziali:
- Token ID
- Token di accesso
- Aggiorna token: viene passato alla funzione di blocco se è selezionato l'ambito
offline_access
.
Esempio:
const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Yahoo
Quando un utente accede con Yahoo, le seguenti credenziali verranno passate senza parametri o ambiti personalizzati:
- Token ID
- Token di accesso
- Aggiorna token
Quando un utente accede con LinkedIn, verranno passate le seguenti credenziali:
- Token di accesso
Apple
Quando un utente accede con Apple, le seguenti credenziali verranno passate senza parametri o ambiti personalizzati:
- Token ID
- Token di accesso
- Aggiorna token
Scenari comuni
I seguenti esempi mostrano alcuni casi d'uso comuni delle funzioni di blocco:
Consentire la registrazione solo da un dominio specifico
L'esempio seguente mostra come impedire agli utenti che non fanno parte del dominio example.com
di registrarsi con la tua app:
Node.js
exports.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}"`);
}
});
Bloccare la registrazione di utenti con email non verificate
L'esempio seguente mostra come impedire agli utenti con email non verificate di registrarsi nella tua app:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.email && !user.emailVerified) {
throw new gcipCloudFunctions.https.HttpsError(
'invalid-argument', `Unverified email "${user.email}"`);
}
});
Richiedere la verifica email al momento della registrazione
L'esempio seguente mostra come richiedere a un utente di verificare il proprio indirizzo email dopo la registrazione:
Node.js
exports.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);
});
}
});
exports.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.`);
}
});
Trattare alcune email di provider di identità come verificate
L'esempio seguente mostra come trattare le email degli utenti da determinati provider di identità come verificate:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
return {
emailVerified: true,
};
}
});
Blocco dell'accesso da determinati indirizzi IP
Nell'esempio seguente, come bloccare l'accesso da determinati intervalli di indirizzi IP:
Node.js
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (isSuspiciousIpAddress(context.ipAddress)) {
throw new gcipCloudFunctions.https.HttpsError(
'permission-denied', 'Unauthorized access!');
}
});
Impostazione di rivendicazioni personalizzate e di sessione
L'esempio seguente mostra come impostare rivendicazioni personalizzate e di sessione:
Node.js
exports.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
Puoi prevenire il furto di token monitorando l'indirizzo IP da cui un utente esegue l'accesso e confrontandolo con l'indirizzo IP nelle richieste successive. Se la richiesta sembra sospetta, ad esempio se gli IP provengono da regioni geografiche diverse, puoi chiedere all'utente di accedere di nuovo.
Utilizza le rivendicazioni relative alle sessioni per monitorare l'indirizzo IP con cui l'utente accede:
Node.js
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });
Quando un utente tenta di accedere a risorse che richiedono l'autenticazione con Identity Platform, confronta l'indirizzo IP nella richiesta con l'IP utilizzato per accedere:
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 in corso...
L'esempio seguente mostra come sanitizzare le foto del profilo degli utenti:
Node.js
exports.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 saperne di più su come rilevare ed eliminare 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 di Google Calendar. Il token di aggiornamento viene archiviato per l'accesso offline.
Node.js
const {OAuth2Client} = require('google-auth-library');
const {google} = require('googleapis');
const gcipCloudFunctions = require('gcip-cloud-functions');
// ...
// Initialize Google OAuth client.
const keys = require('./oauth2.keys.json');
const oAuth2Client = new OAuth2Client(
keys.web.client_id,
keys.web.client_secret
);
exports.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
- Estendi l'autenticazione con le funzioni asincrone.
- Scopri di più su Cloud Functions.