Personalize o fluxo de autenticação através de funções de bloqueio
Este documento mostra como estender a autenticação da Identity Platform usando funções do Cloud Run de bloqueio.
As funções de bloqueio permitem-lhe executar código personalizado que modifica o resultado do registo ou início de sessão de um utilizador na sua app. Por exemplo, pode impedir que um utilizador seja autenticado se não cumprir determinados critérios ou atualizar as informações de um utilizador antes de as devolver à sua app cliente.
Antes de começar
Crie uma app com a Identity Platform. Consulte o início rápido para saber como.
Compreenda as funções de bloqueio
Pode registar funções de bloqueio para dois eventos:
beforeCreate
: aciona-se antes de um novo utilizador ser guardado na base de dados do Identity Platform e antes de um token ser devolvido à sua app cliente.beforeSignIn
: é acionado depois de as credenciais de um utilizador serem validadas, mas antes de a Identity Platform devolver um token de ID à sua app cliente. Se a sua app usar a autenticação multifator, a função é acionada depois de o utilizador validar o respetivo segundo fator. Tenha em atenção que a criação de um novo utilizador também acionabeforeSignIn
, além debeforeCreate
.
Tenha em atenção o seguinte quando usar funções de bloqueio:
A sua função tem de responder no prazo de 7 segundos. Após 7 segundos, a Identity Platform devolve um erro e a operação do cliente falha.
Os códigos de resposta HTTP diferentes de
200
são transmitidos às suas apps de cliente. Certifique-se de que o código do cliente processa todos os erros que a sua função pode devolver.As funções aplicam-se a todos os utilizadores no seu projeto, incluindo os contidos num inquilino. O Identity Platform fornece informações sobre os utilizadores à sua função, incluindo quaisquer inquilinos aos quais pertencem, para que possa responder em conformidade.
Associar outro fornecedor de identidade a uma conta reativa todas as funções
beforeSignIn
registadas. Isto não inclui fornecedores de email e palavras-passe.A autenticação anónima e personalizada não suporta funções de bloqueio.
Se também estiver a usar funções assíncronas, o objeto user que uma função assíncrona recebe não contém atualizações da função de bloqueio.
Crie uma função de bloqueio
Os passos seguintes demonstram como criar uma função de bloqueio:
Aceda à página Definições do Identity Platform na Google Cloud consola.
Selecione o separador Acionadores.
Para criar uma função de bloqueio para o registo de utilizadores, selecione o menu pendente Função em Antes de criar (beforeCreate) e, de seguida, clique em Criar função. Para criar uma função de bloqueio para o início de sessão do utilizador, crie uma função em Antes de iniciar sessão (beforeSignIn).
Crie uma nova função:
Introduza um Nome para a função.
No campo Acionador, selecione HTTP.
No campo Autenticação, selecione Permitir invocações não autenticadas.
Clicar em Seguinte.
Com o editor inline, abra
index.js
. Elimine o código de exemplohelloWorld
e substitua-o por um dos seguintes:Para responder ao registo:
import * as gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => { });
Para responder ao início de sessão:
import * as gcipCloudFunctions from 'gcip-cloud-functions'; const authClient = new gcipCloudFunctions.Auth(); exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { });
Abra
package.json
e adicione o seguinte bloco de dependências: Para a versão mais recente do SDK, consultegcip-cloud-functions
.{ "type": "module", "name": ..., "version": ..., "dependencies": { "gcip-cloud-functions": "^0.2.0" } }
Defina o ponto de entrada da função como
beforeSignIn
Clique em Implementar para publicar a sua função.
Clique em Guardar na página de funções de bloqueio da Identity Platform.
Consulte as secções seguintes para saber como implementar a sua função. Tem de implementar novamente a função sempre que a atualiza.
Também pode criar e gerir funções através da Google Cloud CLI ou da API REST. Consulte a documentação das funções do Cloud Run para saber como usar a CLI gcloud para implementar uma função.
Obtenha informações do utilizador e de contexto
Os eventos beforeSignIn
e beforeCreate
fornecem objetos User
e EventContext
que contêm informações sobre o utilizador que está a iniciar sessão. Use estes valores
no seu código para determinar se permite que uma operação continue.
Para ver uma lista das propriedades disponíveis no objeto User
, consulte a
UserRecord
referência da API.
O objeto EventContext
contém as seguintes propriedades:
Nome | Descrição | Exemplo |
---|---|---|
locale |
O local da aplicação. Pode definir a localidade através do SDK de cliente ou transmitindo o cabeçalho de localidade na API REST. | fr ou sv-SE |
ipAddress |
O endereço IP do dispositivo a partir do qual o utilizador final está a registar-se ou a iniciar sessão. | 114.14.200.1 |
userAgent |
O agente do utilizador que aciona a função de bloqueio. | Mozilla/5.0 (X11; Linux x86_64) |
eventId |
O identificador exclusivo do evento. | rWsyPtolplG2TBFoOkkgyg |
eventType |
O tipo de evento. Isto fornece informações sobre o nome do evento, como
beforeSignIn ou beforeCreate , e o
método de início de sessão associado usado, como o Google ou o email/palavra-passe.
|
providers/cloud.auth/eventTypes/user.beforeSignIn:password
|
authType |
Sempre USER . |
USER
|
resource |
O projeto ou o inquilino do Identity Platform. |
projects/project-id/tenants/tenant-id
|
timestamp |
A hora em que o evento foi acionado, formatada como uma string RFC 3339. | Tue, 23 Jul 2019 21:10:57 GMT
|
additionalUserInfo |
Um objeto que contém informações sobre o utilizador. |
AdditionalUserInfo
|
credential |
Um objeto que contém informações sobre as credenciais do utilizador. |
AuthCredential
|
Bloqueie o registo ou o início de sessão
Para bloquear um registo ou uma tentativa de início de sessão, lance um HttpsError
na sua função. Por exemplo:
Node.js
throw new gcipCloudFunctions.https.HttpsError('permission-denied');
A tabela seguinte apresenta os erros que pode gerar, juntamente com a respetiva mensagem de erro predefinida:
Nome | Código | Mensagem |
---|---|---|
invalid-argument |
400 |
O cliente especificou um argumento inválido. |
failed-precondition |
400 |
Não é possível executar o pedido no estado atual do sistema. |
out-of-range |
400 |
O cliente especificou uma alteração inválida. |
unauthenticated |
401 |
Token OAuth em falta, inválido ou expirado. |
permission-denied |
403 |
O cliente não tem autorização suficiente. |
not-found |
404 |
Não foi possível encontrar o recurso especificado. |
aborted |
409 |
Conflito de simultaneidade, como um conflito de leitura-modificação-escrita. |
already-exists |
409 |
O recurso que um cliente tentou criar já existe. |
resource-exhausted |
429 |
Sem quota de recursos ou a atingir a limitação de velocidade. |
cancelled |
499 |
Pedido cancelado pelo cliente. |
data-loss |
500 |
Perda de dados irrecuperável ou corrupção de dados. |
unknown |
500 |
Erro de servidor desconhecido. |
internal |
500 |
Erro interno do servidor. |
not-implemented |
501 |
Método de API não implementado pelo servidor. |
unavailable |
503 |
Serviço indisponível. |
deadline-exceeded |
504 |
Prazo do pedido excedido. |
Também pode especificar uma mensagem de erro personalizada:
Node.js
throw new gcipCloudFunctions.https.HttpsError('permission-denied', 'Unauthorized request origin!');
O exemplo seguinte mostra como bloquear utilizadores que não estão num domínio específico de se registarem na sua app:
Node.js
// Import the Cloud Auth Admin module.
import * as gcipCloudFunctions from 'gcip-cloud-functions';
// Initialize the Auth client.
const authClient = new gcipCloudFunctions.Auth();
// Http trigger with Cloud Run 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}"`);
}
});
Independentemente de usar uma mensagem predefinida ou personalizada, as funções do Cloud Run envolvem o erro e devolvem-no ao cliente como um erro interno. Por exemplo, se gerar o seguinte erro na sua função:
throw new gcipCloudFunctions.https.HttpsError('invalid-argument', `Unauthorized email user@evil.com}`);
É devolvido um erro semelhante ao seguinte à sua app cliente (se estiver a usar o SDK do cliente, o erro é incluído como um erro 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"
}
]
}
}
A sua app deve detetar o erro e processá-lo em conformidade. Por exemplo:
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.
}
});
Modifique um utilizador
Em vez de bloquear uma tentativa de registo ou início de sessão, pode permitir que a operação continue, mas modificar o objeto User
que é guardado na base de dados do Identity Platform e devolvido ao cliente.
Para modificar um utilizador, devolva um objeto do seu controlador de eventos que contenha os campos a modificar. Pode modificar os seguintes campos:
displayName
disabled
emailVerified
photoURL
customClaims
sessionClaims
(apenasbeforeSignIn
)
Com exceção de sessionClaims
, todos os campos modificados são guardados na base de dados do Identity Platform, o que significa que são incluídos no token de resposta e persistem entre sessões de utilizador.
O exemplo seguinte mostra como definir um nome a apresentar predefinido:
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 registar um controlador de eventos para beforeCreate
e beforeSignIn
, tenha em atenção que beforeSignIn
é executado após beforeCreate
. Os campos de utilizador atualizados em beforeCreate
são visíveis em beforeSignIn
. Se definir um campo diferente de
sessionClaims
em ambos os controladores de eventos, o valor definido em beforeSignIn
substitui o valor definido em beforeCreate
. Apenas para o sessionClaims
, são propagados para as reivindicações de tokens da sessão atual, mas não são mantidos nem armazenados na base de dados.
Por exemplo, se forem definidos quaisquer sessionClaims
, beforeSignIn
devolve-os com quaisquer reivindicações beforeCreate
, e estes são unidos. Quando são unidas, se uma chave sessionClaims
corresponder a uma chave em customClaims
, a chave customClaims
correspondente é substituída nas reivindicações de tokens pela chave sessionClaims
. No entanto, a chave customClaims
substituída continua a ser persistida na base de dados para pedidos futuros.
Credenciais e dados OAuth suportados
Pode transmitir credenciais e dados do OAuth para funções de bloqueio de vários fornecedores de identidade. A tabela seguinte mostra as credenciais e os dados suportados para cada fornecedor de identidade:
Fornecedor de identidade | Token de ID | Token de acesso | Prazo de validade | Segredo da chave | Símbolo de atualização | Reivindicações de início de sessão |
---|---|---|---|---|---|---|
Sim | Sim | Sim | Não | Sim | Não | |
Não | Sim | Sim | Não | Não | Não | |
Não | Sim | Não | Sim | Não | Não | |
GitHub | Não | Sim | Não | Não | Não | Não |
Microsoft | Sim | Sim | Sim | Não | Sim | Não |
Não | Sim | Sim | Não | Não | Não | |
Yahoo | Sim | Sim | Sim | Não | Sim | Não |
Apple | Sim | Sim | Sim | Não | Sim | Não |
SAML | Não | Não | Não | Não | Não | Sim |
OIDC | Sim | Sim | Sim | Não | Sim | Sim |
Tokens de atualização
Para usar um token de atualização numa função de bloqueio, primeiro tem de selecionar a caixa de verificação na secção Acionadores do menu pendente Incluir credenciais de token na Google Cloud consola.
Os tokens de atualização não são devolvidos por fornecedores de identidade quando inicia sessão diretamente com uma credencial OAuth, como um token de ID ou um token de acesso. Nesta situação, a mesma credencial OAuth por parte do cliente é transmitida à função de bloqueio. No entanto, para fluxos de 3 passos, pode estar disponível um token de atualização se o fornecedor de identidade o suportar.
As secções seguintes descrevem cada tipo de fornecedor de identidade e as respetivas credenciais e dados suportados.
Fornecedores de OIDC genéricos
Quando um utilizador inicia sessão com um fornecedor OIDC genérico, são transmitidas as seguintes credenciais:
- Token de ID: fornecido se o fluxo
id_token
estiver selecionado. - Token de acesso: fornecido se o fluxo de código estiver selecionado. Tenha em atenção que o fluxo de código só é suportado com a API REST.
- Token de atualização: fornecido se o âmbito
offline_access
estiver selecionado.
Exemplo:
const provider = new firebase.auth.OAuthProvider('oidc.my-provider');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Quando um utilizador inicia sessão com o Google, são transmitidas as seguintes credenciais:
- Token de ID
- Chave de acesso
- Token de atualização: só é fornecido se os seguintes parâmetros personalizados forem pedidos:
access_type=offline
prompt=consent
, se o utilizador deu o consentimento anteriormente e não foi pedido nenhum novo âmbito
Exemplo:
const provider = new firebase.auth.GoogleAuthProvider();
provider.setCustomParameters({
'access_type': 'offline',
'prompt': 'consent'
});
firebase.auth().signInWithPopup(provider);
Saiba mais acerca dos tokens de atualização da Google.
Quando um utilizador inicia sessão com o Facebook, são transmitidas as seguintes credenciais:
- Chave de acesso: é devolvida uma chave de acesso que pode ser trocada por outra chave de acesso. Saiba mais sobre os diferentes tipos de tokens de acesso suportados pelo Facebook e como pode trocá-los por tokens de longa duração.
GitHub
Quando um utilizador inicia sessão com o GitHub, é transmitida a seguinte credencial:
- Chave de acesso: não expira, a menos que seja revogada.
Microsoft
Quando um utilizador inicia sessão com a Microsoft, são transmitidas as seguintes credenciais:
- Token de ID
- Chave de acesso
- Token de atualização: transmitido à função de bloqueio se o âmbito
offline_access
for selecionado.
Exemplo:
const provider = new firebase.auth.OAuthProvider('microsoft.com');
provider.addScope('offline_access');
firebase.auth().signInWithPopup(provider);
Yahoo
Quando um utilizador inicia sessão com o Yahoo, são transmitidas as seguintes credenciais sem parâmetros personalizados nem âmbitos:
- Token de ID
- Chave de acesso
- Token de atualização
Quando um utilizador inicia sessão com o LinkedIn, são transmitidas as seguintes credenciais:
- Chave de acesso
Apple
Quando um utilizador inicia sessão com a Apple, são transmitidas as seguintes credenciais sem parâmetros personalizados nem âmbitos:
- Token de ID
- Chave de acesso
- Token de atualização
Cenários comuns
Os exemplos seguintes demonstram alguns exemplos de utilização comuns para funções de bloqueio:
Permita o registo a partir de um domínio específico
O exemplo seguinte mostra como impedir que os utilizadores que não fazem parte do domínio example.com
se registem na sua 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}"`);
}
});
Impeça o registo de utilizadores com emails não validados
O exemplo seguinte mostra como impedir que os utilizadores com emails não validados se registem na sua 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}"`);
}
});
Exija a validação de email no registo
O exemplo seguinte mostra como exigir que um utilizador valide o respetivo email após o registo:
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.`);
}
});
Trate determinados emails de fornecedores de identidade como validados
O exemplo seguinte mostra como tratar os emails dos utilizadores de determinados fornecedores de identidade como validados:
Node.js
exports.beforeCreate = authClient.functions().beforeCreateHandler((user, context) => {
if (user.email && !user.emailVerified && context.eventType.indexOf(':facebook.com') !== -1) {
return {
emailVerified: true,
};
}
});
Bloqueie o início de sessão a partir de determinados endereços IP
O exemplo seguinte mostra como bloquear o início de sessão a partir de determinados intervalos de endereços IP:
Node.js
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
if (isSuspiciousIpAddress(context.ipAddress)) {
throw new gcipCloudFunctions.https.HttpsError(
'permission-denied', 'Unauthorized access!');
}
});
Defina reivindicações personalizadas e de sessão
O exemplo seguinte mostra como definir reivindicações personalizadas e de sessão:
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,
}
}
}
});
Monitorize endereços IP para monitorizar atividade suspeita
Pode impedir o roubo de tokens monitorizando o endereço IP a partir do qual um utilizador inicia sessão e comparando-o com o endereço IP em pedidos subsequentes. Se o pedido parecer suspeito, por exemplo, se os IPs forem de diferentes regiões geográficas, pode pedir ao utilizador que inicie sessão novamente.
Use reivindicações de sessão para acompanhar o endereço IP com o qual o utilizador inicia sessão:
Node.js
exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => { return { sessionClaims: { signInIpAddress: context.ipAddress, }, }; });
Quando um utilizador tenta aceder a recursos que requerem autenticação com o Identity Platform, compare o endereço IP no pedido com o IP usado para iniciar sessão:
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!' }) }); } }); });
Filtrar fotos de utilizadores
O exemplo seguinte mostra como limpar as fotos dos perfis dos utilizadores:
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,
};
}
});
});
Para saber como detetar e limpar imagens, consulte a documentação do Cloud Vision.
Aceda às credenciais OAuth do fornecedor de identidade de um utilizador
O exemplo seguinte demonstra como obter um token de atualização para um utilizador que iniciou sessão com o Google e usá-lo para chamar as APIs Google Calendar. O token de atualização é armazenado para acesso 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();
});
});
})
}
});
O que se segue?
- Estenda a autenticação com funções assíncronas.
- Saiba mais sobre as funções do Cloud Run.