Como personalizar o fluxo de autenticação usando funções de bloqueio

Este documento mostra como estender a autenticação do Identity Platform usando funções do Cloud Run de bloqueio.

As funções de bloqueio permitem executar um código personalizado que modifica o resultado de um usuário que registra ou faz login no app. Por exemplo, é possível impedir que um usuário faça a autenticação se não atender a determinados critérios ou atualizar as informações de um usuário antes de retorná-lo ao aplicativo cliente.

Antes de começar

Crie um app com o Identity Platform. Consulte o Guia de início rápido para saber como.

Noções básicas sobre funções de bloqueio

É possível registrar funções de bloqueio para dois eventos:

  • beforeCreate: acionado antes que um novo usuário seja salvo no banco de dados do Identity Platform e antes que um token seja retornado ao app cliente.

  • beforeSignIn: acionado depois que as credenciais de um usuário forem verificadas, mas antes que o Identity Platform retorne um token de ID para o app cliente. Se o aplicativo usar autenticação multifator, a função será acionada depois que o usuário verificar o segundo fator. Observe que a criação de um novo usuário também aciona beforeSignIn, além de beforeCreate.

Informações importantes ao usar funções de bloqueio:

  • A função precisa responder em até sete segundos. Após sete segundos, o Identity Platform retorna um erro, e a operação do cliente falha.

  • Códigos de resposta HTTP diferentes de 200 são transmitidos para os apps cliente. Confira se o código do cliente processa todos os erros que a função pode retornar.

  • As funções se aplicam a todos os usuários do projeto, incluindo aqueles contidos em um locatário. O Identity Platform fornece informações sobre os usuários para a função, incluindo locatários a que pertencem.

  • Vincular outro provedor de identidade a uma conta aciona novamente as funções beforeSignIn registradas. Isso não inclui provedores de e-mail e senha.

  • A autenticação anônima e personalizada não é suportada por funções de bloqueio.

  • Se você também estiver usando funções assíncronas, o objeto de usuário que uma função assíncrona recebe não conterá atualizações da função de bloqueio.

Como criar uma função de bloqueio

As etapas a seguir demonstram como criar uma função de bloqueio:

  1. Acesse a página Configurações do Identity Platform no console do Google Cloud.

    Ir para a página "Configurações"

  2. Selecione a guia Gatilhos.

  3. Se quiser criar uma função de bloqueio para registro de usuário, selecione o menu suspenso Função em Antes da criação (beforeCreate) e clique em Criar função. Para criar uma função de bloqueio para o login do usuário, crie uma função em Antes de fazer login (beforeSignIn).

  4. Crie uma nova função:

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

    2. No campo Gatilho, selecione HTTP.

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

    4. Clique em Next.

  5. Usando o editor in-line, abra index.js. Exclua o código helloWorld de exemplo e substitua-o por um dos seguintes:

    Para responder ao registro, faça o seguinte:

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

    Para responder ao login:

    import gcipCloudFunctions from 'gcip-cloud-functions';
    
    const authClient = new gcipCloudFunctions.Auth();
    
    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      // TODO
    });
    
  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 Implantar para publicar a função.

  9. Clique em Salvar na página de funções de bloqueio do Identity Platform.

Consulte as seções a seguir para saber como implementar a função. É necessário implantar novamente a função sempre que atualizá-la.

Também é possível criar e gerenciar funções usando a Google Cloud CLI ou a API REST. Consulte a documentação das funções do Cloud Run para saber como usar a Google Cloud CLI para implantar uma função.

Como receber informações do usuário e de contexto

Os eventos beforeSignIn e beforeCreate fornecem objetos User e EventContext que contêm informações sobre o login do usuário. Use esses valores no seu código para determinar se quer permitir que uma operação continue.

Para uma lista de propriedades disponíveis no objeto User, consulte a referência da API UserRecord.

O objeto EventContext contém as seguintes propriedades:

Nome Descrição Exemplo
locale A localidade do aplicativo. Para definir, use o SDK de cliente ou transmita o cabeçalho de localidade na API REST. fr ou sv-SE
ipAddress O endereço IP do dispositivo de registro ou de login do usuário final. 114.14.200.1
userAgent O user agent 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. Assim, você tem informações sobre o nome do evento, como beforeSignIn ou beforeCreate, e o método de login associado, como Google ou e-mail/senha. providers/cloud.auth/eventTypes/user.beforeSignIn:password
authType Sempre USER. USER
resource O projeto ou locatário do Identity Platform. projects/project-id/tenants/tenant-id
timestamp É a hora em que o evento foi acionado, formatado como uma string RFC 3339. Tue, 23 Jul 2019 21:10:57 GMT
additionalUserInfo Um objeto que contém informações sobre o usuário. AdditionalUserInfo
credential Um objeto que contém informações sobre a credencial do usuário. AuthCredential

Bloquear o registro ou o login

Para bloquear uma tentativa de registro ou login, gere um HttpsError na sua função. Exemplo:

Node.js

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

A tabela a seguir lista os erros que podem ser gerados com a mensagem de erro padrão:

Nome Código Mensagem
invalid-argument 400 O cliente especificou um argumento inválido.
failed-precondition 400 A solicitação não pode ser executada no estado atual do sistema.
out-of-range 400 O cliente especificou um intervalo inválido.
unauthenticated 401 Token OAuth inválido, ausente ou expirado.
permission-denied 403 O cliente não tem permissão suficiente.
not-found 404 O recurso especificado não foi encontrado.
aborted 409 Conflito de simultaneidade, como leitura-modificação-gravação.
already-exists 409 O recurso que um cliente tentou criar já existe.
resource-exhausted 429 Excedeu a cota de recursos ou está perto de atingir a limitação de taxa.
cancelled 499 Solicitação cancelada pelo cliente.
data-loss 500 Perda ou corrupção de dados irrecuperável.
unknown 500 Erro desconhecido de servidor.
internal 500 Erro interno do servidor.
not-implemented 501 Método da API não implementado pelo servidor.
unavailable 503 Serviço indisponível.
deadline-exceeded 504 O prazo de solicitação foi excedido.

Também é possível especificar uma mensagem de erro personalizada:

Node.js

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

O exemplo a seguir mostra como bloquear o registro de usuários que não são membros de um domínio específico para seu 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 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 você usar uma mensagem padrão ou personalizada, as funções do Cloud Run incluem o erro e o retornam para o cliente como um erro interno. Por exemplo, se você gerar o seguinte erro na função:

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

Um erro semelhante ao seguinte é retornado ao app cliente. Se você estiver usando o SDK do cliente, o erro será encapsulado 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"
      }
    ]
  }
}

Seu app vai capturar e processar o erro. 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.
    }
  });

Como modificar um usuário

Em vez de bloquear uma tentativa de registro ou login, é possível permitir que a operação continue e também modificar o objeto User que é salvo no banco de dados do Identity Platform e retornado ao cliente.

Para modificar um usuário, retorne um objeto do seu manipulador de eventos que contém os campos a serem modificados. É possível modificar os campos a seguir:

  • displayName
  • disabled
  • emailVerified
  • photoURL
  • customClaims
  • sessionClaims (aplicável somente a beforeSignIn)

Exceto sessionClaims, todos os campos modificados são salvos no banco de dados do Identity Platform. Isso significa que eles são incluídos no token de resposta e persistem entre as sessões do usuário.

O exemplo a seguir mostra como definir um nome de exibição padrão:

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 você registrar um manipulador de eventos para beforeCreate e beforeSignIn, observe que beforeSignIn é executado depois de beforeCreate. Os campos do usuário atualizados em beforeCreate estão visíveis em beforeSignIn. Se você definir um campo diferente de sessionClaims em ambos os manipuladores de eventos, o valor definido em beforeSignIn substituirá o valor definido em beforeCreate. Somente para sessionClaims, elas são propagadas para as declarações de token da sessão atual, mas não são mantidas nem armazenadas no banco de dados.

Por exemplo, se sessionClaims estiver definido, beforeSignIn os retornará com todas as reivindicações de beforeCreate e eles serão mesclados. Quando elas forem mescladas, se uma chave sessionClaims corresponder a uma chave em customClaims, o customClaims correspondente será substituído nas declarações de token pelo sessionClaims. No entanto, a chave customClaims substituída ainda é mantida no banco de dados para solicitações futuras.

Dados e credenciais OAuth compatíveis

É possível transmitir dados e credenciais do OAuth para bloquear funções de vários provedores de identidade. A tabela a seguir mostra quais credenciais e dados são compatíveis com cada provedor de identidade:

Provedor de identidade Token de ID Token de acesso Tempo de vencimento Secret do token Token de atualização Reivindicações de login
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 em uma função de bloqueio, primeiro é preciso marcar a caixa de seleção na seção Acionadores do menu suspenso Incluir credenciais do token no console do Google Cloud.

Os tokens de atualização não serão retornados por nenhum provedor de identidade quando você fizer login diretamente com uma credencial OAuth, como um token de código ou um token de acesso. Nessa situação, a mesma credencial OAuth do lado do cliente será transmitida para a função de bloqueio. No entanto, para fluxos de três etapas, um token de atualização poderá ser disponibilizado se o provedor de identidade for compatível com ele.

As seções a seguir descrevem cada tipo de provedor de identidade e as credenciais e dados compatíveis.

Provedores OIDC genéricos

Quando um usuário faz login com um provedor OIDC genérico, as seguintes credenciais são transmitidas:

  • Token de ID: fornecido se o fluxo id_token estiver selecionado.
  • Token de acesso: fornecido se o fluxo de código estiver selecionado. O fluxo de código atualmente é compatível apenas com a API REST.
  • Token de atualização: fornecido se o escopo 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 usuário faz login com o Google, as seguintes credenciais são transmitidas:

  • Token de ID
  • Token de acesso
  • Token de atualização, fornecido somente se os seguintes parâmetros personalizados forem solicitados:
    • access_type=offline
    • prompt=consent, se o usuário autorizou anteriormente e nenhum novo escopo foi solicitado.

Exemplo:

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

Saiba mais sobre os tokens de atualização do Google.

Facebook

Quando um usuário faz login com o Facebook, a seguinte credencial é transmitida:

  • Token de acesso: é retornado um token de acesso que pode ser trocado por outro. Saiba mais sobre os diferentes tipos de tokens de acesso compatíveis com o Facebook e como trocá-los por tokens de longa duração.

GitHub

Quando um usuário faz login com o GitHub, a seguinte credencial é transmitida:

  • Token de acesso: não expira, a menos que seja revogado.

Microsoft

Quando um usuário faz login com a Microsoft, as seguintes credenciais são transmitidas:

  • Token de ID
  • Token de acesso
  • Token de atualização: passado para a função de bloqueio se o escopo offline_access estiver selecionado.

Exemplo:

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

Yahoo

Quando um usuário faz login com o Yahoo, as seguintes credenciais serão transmitidas sem parâmetros ou escopos personalizados:

  • Token de ID
  • Token de acesso
  • Token de atualização

LinkedIn

Quando um usuário faz login com o LinkedIn, a seguinte credencial é transmitida:

  • Token de acesso

Apple

Quando um usuário faz login com a Apple, as seguintes credenciais serão transmitidas sem parâmetros ou escopos personalizados:

  • Token de ID
  • Token de acesso
  • Token de atualização

Cenários comuns

Os exemplos a seguir demonstram alguns casos de uso comuns para funções de bloqueio:

Permitir somente o registro de um domínio específico

O exemplo a seguir mostra como impedir que usuários que não façam parte do domínio example.com se registrem no 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}"`);
  }
});

Como bloquear o registro de usuários com e-mails não verificados

O exemplo a seguir mostra como impedir que usuários com e-mails não verificados sejam registrados no seu aplicativo:

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

Como exigir a verificação de e-mail no registro

O exemplo a seguir mostra como exigir que um usuário verifique o e-mail após o registro:

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

Como tratar alguns e-mails de provedores de identidade como verificados

O exemplo a seguir mostra como tratar e-mails de usuários de determinados provedores de identidade como verificados:

Node.js

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

Como bloquear o login de determinados endereços IP

O exemplo a seguir mostra como bloquear o login 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!');
  }
});

Como definir declarações personalizadas e de sessão

O exemplo a seguir mostra como definir declaraçõ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,
      }
    }
  }
});

Como rastrear endereços IP para monitorar atividades suspeitas

Para evitar o roubo de tokens, rastreie o endereço IP de onde um usuário faz login e compare-o ao endereço IP nas solicitações subsequentes. Se a solicitação parecer suspeita, por exemplo, se os IPs forem de regiões geográficas diferentes, peça ao usuário para fazer login novamente.

  1. Use as declarações de sessão para acompanhar o endereço IP com que o usuário faz login:

    Node.js

    exports.beforeSignIn = authClient.functions().beforeSignInHandler((user, context) => {
      return {
        sessionClaims: {
          signInIpAddress: context.ipAddress,
        },
      };
    });
    
  2. Quando um usuário tentar acessar recursos que exigem autenticação com o Identity Platform, compare o endereço IP na solicitação com o IP usado para fazer login:

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

Como filtrar fotos do usuário

O exemplo a seguir mostra como limpar as fotos de perfil dos usuários:

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 mais sobre como detectar e limpar imagens, consulte a documentação do Cloud Vision.

Como acessar as credenciais OAuth do provedor de identidade de um usuário

O exemplo a seguir demonstra como receber um token de atualização para um usuário que fez login com o Google e usá-lo para chamar as APIs do Google Agenda. O token de atualização é armazenado para acesso off-line.

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

A seguir