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 aciona beforeSignIn, além de beforeCreate.

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:

  1. Aceda à página Definições do Identity Platform na Google Cloud consola.

    Aceda à página Definições

  2. Selecione o separador Acionadores.

  3. 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).

  4. Crie uma nova função:

    1. Introduza um Nome para a função.

    2. No campo Acionador, selecione HTTP.

    3. No campo Autenticação, selecione Permitir invocações não autenticadas.

    4. Clicar em Seguinte.

  5. Com o editor inline, abra index.js. Elimine o código de exemplo helloWorld 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) => {
    
    });
    
  6. Abra package.json e adicione o seguinte bloco de dependências: Para a versão mais recente do SDK, consulte gcip-cloud-functions.

    {
      "type": "module",
      "name": ...,
      "version": ...,
    
      "dependencies": {
        "gcip-cloud-functions": "^0.2.0"
      }
    }
    
  7. Defina o ponto de entrada da função como beforeSignIn

  8. Clique em Implementar para publicar a sua função.

  9. 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 (apenas beforeSignIn)

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
Google Sim Sim Sim Não Sim Não
Facebook Não Sim Sim Não Não Não
Twitter 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
LinkedIn 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);

Google

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.

Facebook

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

LinkedIn

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.

  1. 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,
        },
      };
    });
    
  2. 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?