Como integrar o reCAPTCHA em um projeto frontend Angular

Como adicionar o Google reCATPCHA na tela de login de um projeto Angular e uma API com Express JS + Node.js.

Como integrar o reCAPTCHA em um projeto frontend Angular
Como integrar o Google reCAPTCHA em um projeto Angular e uma API com Express JS

Introdução

Acredito que muitas pessoas já reclamaram em algum momento ao ter que digitar um código de difícil leitura que aparece em uma imagem, como a figura abaixo ilustra:

Exemplo de um formulário com CAPTCHA
Exemplo de um formulário com CAPTCHA

Este desafio que as pessoas precisam resolver para prosseguir com sua jornada é conhecido como  CAPTCHA ("Completely Automated Public Turing test to tell Computers and Humans Apart").

A ideia do CAPTCHA é diferenciar um ser humano de um computador através de desafios. Por exemplo, podemos renderizar um código de modo a dificultar a sua leitura através de ângulos diferentes, tamanhos de fontes variáveis, cores alternadas, entre outros recursos. Então na teoria um ser humano consegue efetuar sua leitura mas um computador não. Essa diferenciação nos ajuda por exemplo, a evitar spams e ataques de força bruta.

Em um exemplo bem simples, suponha que em seu sistema haja uma API. Um dos recursos desta API é responsável pela autenticação do usuário através do e-mail e senha. Sabendo disso uma pessoa mal-intencionada poderia criar um script ou usar um software como o Hydra para tentar descobrir um usuário e senha válido através de tentativa e erro, também conhecido como força bruta. Adicionando um CAPTCHA irá dificultar este processo visto que a cada requisição será necessário resolver um desafio, que na teoria só poderia ser resolvido por um ser humano.

Quando escrevi "na teoria", quero dizer que podemos ter outros cenários como:

  • eventualmente um ser humano não será capaz de identificar um CATPCHA, tanto que normalmente existe um botão para trocar a imagem.
  • é possível que eventuais códigos possam ser interpretados por algoritmos de OCR, Machine Learning ou mesmo empresas que vendem serviços em forma de APIs para decodificar CATPCHAs.
OCR é um acrónimo para o inglêsOptical Character Recognition, é uma tecnologia para reconhecer caracteres a partir de um arquivo de imagem ou mapa de bits sejam eles escaneados, escritos a mão, datilografados ou impressos.

https://pt.wikipedia.org/wiki/Reconhecimento_ótico_de_caracteres

Então tenha em mente que um CAPTCHA pode ajudar a aumentar a segurança adicionando uma camada a mais de proteção/verificação, mas por si só não mitiga o risco de ataques/spams.

Google reCAPTCHA

Neste post vamos falar sobre o uso do  reCAPTCHA v2, que é um tipo de CAPTCHA. Foi lançado em 2014, mas ainda é bem utilizado. Podemos ver sua aplicação no site da B3, no site Nota Fiscal Paulista, no cadastro do DISQUS, na tela de login do aplicativo FGTS, entre outros.

reCAPTCHA uses an advanced risk analysis engine and adaptive challenges to keep malicious software from engaging in abusive activities on your website. Meanwhile, legitimate users will be able to login, make purchases, view pages, or create accounts and fake users will be blocked.

https://www.google.com/recaptcha/about/

Apesar de haver versões mais recentes, 3 (2018) e a Enterprise (2020), vamos criar um exemplo completo ao longo deste texto mostrando a utilização da v2 em um frontend Angular e uma API utilizando o Express JS.

Efetuando o cadastro - reCAPTCHA

O primeiro passo é efetuar o cadastro através deste link.

Página do Google para efetuar o cadastro do reCAPTCHA
Página do Google para efetuar o cadastro do reCAPTCHA

No campo Domínios eu deixei localhost porque vou efetuar o teste direto no meu computador. Caso seu sistema tenha um domínio, basta adicioná-lo neste campo, por exemplo, consolelog.com.br.

Quando você finalizar o cadastro serão geradas duas chaves, guarde estas chaves:

Após o cadastro o site existe 2 chaves: uma pública e outra privada
Após o cadastro o site existe 2 chaves: uma pública e outra privada

Configurando o reCAPTCHA no frontend Angular

O primeiro passo é adicionar a referência do script do Google no projeto. Vou adicioná-lo no arquivo index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>ConsolelogRecaptchaV2</title>
    <base href="/" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1"
    />
    <link
      rel="icon"
      type="image/x-icon"
      href="favicon.ico"
    />
  </head>
  <body>
    <app-root></app-root>
    <script
      src="https://www.google.com/recaptcha/api.js?render=explicit"
      async
      defer
    ></script>
  </body>
</html>
index.html

O ?render=explicit indica que a renderização do CAPTCHA será efetuada de forma explícita, ou seja, em algum ponto do código nós vamos "dizer" que o CAPTCHA deve ser renderizado.


Este <script src="https://www.google.com... (acima) irá criar um objeto chamado grecaptcha que será utilizado para renderizar o CAPTCHA no formulário de forma explícita. A título de curiosidade, este objeto pode ser acessado direto do console do seu navegador:

Console do navegador mostrando que o objeto grecaptcha existe
Console do navegador mostrando que o objeto grecaptcha existe

Após inserir o script no index.html podemos partir para o código do formulário de login, que será a base do nosso exemplo. Por favor, leia os comentários ao longo do código para entender a lógica aplicada:

import {
  Component,
  ElementRef,
  NgZone,
  ViewChild,
} from '@angular/core';
import {
  FormControl,
  FormGroup,
  Validators,
} from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  // *
  // * O "email" e "senha" serão preenchidos pelo usuário.
  // *
  // * O "recaptcha" será retornado pelo Google assim
  // * que o usuário resolver o CAPTCHA
  // *
  form: FormGroup = new FormGroup({
    email: new FormControl(null, [
      Validators.email,
      Validators.required,
    ]),
    senha: new FormControl(null, [Validators.required]),
    recaptcha: new FormControl(null, [Validators.required]),
  });

  @ViewChild('divRecaptcha')
  divRecaptcha!: ElementRef<HTMLDivElement>;

  // *
  // * Quando adicionamos o script do reCAPTCHA no
  // * index.html, o script cria uma variável de
  // * escopo global chamada "grecaptcha".
  // * Então para pegar sua referência podemos
  // * acessá-la através do "window"
  // *
  get grecaptcha(): any {
    const w = window as any;
    return w['grecaptcha'];
  }

  constructor(private ngZone: NgZone) {
    this.renderizarReCaptcha();
  }

  renderizarReCaptcha() {
    // *
    // * Para evitar que change detection seja disparado
    // * cada vez que o setTimeout for executado,
    // * executamos essa recorrência fora da zona
    // * do Angular, por isso o usamos o runOutsideAngular
    // *
    // * Para saber mais sobre change detection:
    // * https://consolelog.com.br/como-funciona-change-detection-angular/
    // * 
    this.ngZone.runOutsideAngular(() => {
      // *
      // * Se o "grecaptcha" ainda não foi carregado ou
      // * o elemento <div> onde o reCAPTCHA será
      // * renderizado ainda não foi construído,
      // * aguardamos algum tempo e executamos novamente
      // * este método:
      // *
      if (!this.grecaptcha || !this.divRecaptcha) {
        setTimeout(() => {
          this.renderizarReCaptcha();
        }, 500);

        return;
      }

      // * Se chegou aqui é porque o recaptcha já está
      // * carregado. Então solicitamos sua renderização
      // * na tela.
      const idElemento =
        this.divRecaptcha.nativeElement.getAttribute('id');

      this.grecaptcha.render(idElemento, {
        sitekey: '<CHAVE SITE>',
        callback: (response: string) => {
          // * Este método é chamado quando o usuário
          // * resolver o desafio do CAPTCHA
          this.ngZone.run(() => {
            this.form.get('recaptcha')?.setValue(response);
          });
        },
      });
    });
  }

  login() {
    console.log(this.form.value);
  }
}
app.component.ts
<form [formGroup]="form" (ngSubmit)="login()">
  <div>
    <input
      type="email"
      formControlName="email"
      placeholder="email..."
    />
  </div>
  <div>
    <input
      type="password"
      formControlName="senha"
      placeholder="senha..."
    />
  </div>
  <div #divRecaptcha id="recaptcha"></div>
  <div>
    <button [disabled]="form.invalid" type="submit">
      Submit
    </button>
  </div>
</form>
app.component.html

Quando o usuário preencher o email, senhae resolver o CAPTCHA, o callback (linha 87 do app.component.ts) será executado e irá preencher o campo recaptcha da variável form com um código. Consequentemente, como todos os campos estão preenchidos, o formulário passa a ser válido e o botão "Submit" é habilitado. Veja no GIF abaixo o fluxo completo no frontend:

Preenchendo o formulário e resolvendo o desafio reCAPTCHA
Preenchendo o formulário e resolvendo o desafio reCAPTCHA

Perceba que o formulário (form) já tem todos os dados necessários, então podemos enviá-los para a nossa API para validação. Então vamos apenas alterar o método login() para chamar nossa API:

  login() {
    this.http
      .post('http://localhost:3000/auth', this.form.value)
      .subscribe({
        next: (resultado) => {
          console.log(resultado);
        },
        error: (error) => {
          console.error(error);
        },
      });
  }
app.component.ts (trecho)

Observação: no meu ambiente local a aplicação Angular está rodando na porta 4200 e a API na 3000. Para evitar problemas de CORS no ambiente de desenvolvimento eu selecionei a opção "Disable Cross-Origin Restriction" no menu  Develop do Safari 15. Se estiver usando o Chrome no Windows, pressione Win + R e cole o comando a seguir: chrome.exe" --disable-web-security --disable-gpu --user-data-dir=~/chromeTemp.


Validando o reCAPTCHA na API

Agora que o frontend já está resolvido, temos que validar na API o email, senha e também o código retornado na resolução do CAPTCHA. Para validar este código temos que chamar uma API do Google (documentação) . É um processo bem simples, conseguimos resolver isto com um único POST:

const axios = require("axios").default;

// * A API recebe dois parâmetros:
// * Chave secreta que foi obtida no cadastro
// *
// * Código gerado após a resolução do CAPTCHA
// * no frontend
function validarGoogleReCaptcha(recaptcha) {
  const dados = {
    response: recaptcha,
    secret: process.env.RECAPTCHA_CHAVE_PRIVADA,
  };

  return axios.post(
    "https://www.google.com/recaptcha/api/siteverify",
    null,
    { params: dados }
  );
}

Então podemos escrever a API com um único endpoint, POST /auth, da seguinte forma:

const express = require("express");
const axios = require("axios").default;
const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/auth", async (req, res) => {
  const { email, senha, recaptcha } = req.body;

  if (!recaptcha) {
    res.status(401).end();
    return;
  }

  try {
    // *
    // * Faz a validação do código retornado na resolução
    // * do CAPTCHA no frontend:
    // *
    const { data: resultadoValidacao } =
      await validarGoogleReCaptcha(recaptcha);

    // * Se o resultado for falso retornamos 401
    if (resultadoValidacao.success === false) {
      // * Erros retornados pela API:
      const erros = resultadoValidacao["error-codes"];
      console.error(erros);

      res.status(401).end();
      return;
    }
  } catch (error) {
    console.error(error);
    res.status(500).end();
    return;
  }

  if (
    email === "[email protected]" &&
    senha === "123456"
  ) {
    res.json({ accessToken: "abc" });
    return;
  }

  res.status(401).end();
});

/**
 * Consulta na API do Google se o código
 * CAPTCHA é válido
 */
function validarGoogleReCaptcha(recaptcha) {
  const dados = {
    response: recaptcha,
      
    // Chave secreta gerada no cadastro
    secret: process.env.RECAPTCHA_CHAVE_PRIVADA,
  };

  return axios.post(
    "https://www.google.com/recaptcha/api/siteverify",
    null,
    { params: dados }
  );
}

app.listen(3000);

Perceba que só efetuamos a validação do emaile senha depois que o CAPTCHA foi validado. Então fica extremamente difícil utilizar um bot para efetuar força bruta no seu processo de login.

Resultado final digitando um email e senha válidos:

Preenchendo o formulário e resolvendo o desafio reCAPTCHA com validação na API
Preenchendo o formulário e resolvendo o desafio reCAPTCHA com validação na API

Para simular um cenário de erro alterei manualmente o código retornado pela resolução do CAPTCHA:

 this.form
  .get('recaptcha')
  ?.setValue(response.substring(0, 20));
trecho alterado no app.component.ts

Desta forma o frontend irá postar algo do tipo:

{
  "email": "[email protected]",
  "senha": "123456",
  "recaptcha": "03AGdBq26OZIJYUQWdg0"
}

Como o código recaptcha é inválido, a API do Google irá retornar um código de erro, invalid-input-response, indicando que o código não confere com o que o frontend recebeu, consequentemente nossa API retornará um HTTP 401 para o frontend e não prosseguirá com o processo de validação do email e `senha:

Resposta da API: 401 Unauthorized
Resposta da API: 401 Unauthorized

Considerações

Tentei abordar a integração de uma forma bem simples. Existem libs e outras formas de chegarmos no mesmo resultado. Este é um exemplo funcional e pode servir de base para outros projetos.

Abordei a versão 2 do reCAPTCHA porque vejo sua utilização em vários sites. A implementação para uso da versão 3 é bem parecida com a 2. Acredito que a maior diferença no frontend é que a versão 3 não interrompe o fluxo do usuário, ou seja, o usuário nem sabe que o reCAPTCHA v3 está atuando. Também podemos destacar o novo parâmetro action, onde é possível especificar o tipo de ação que o usuário está executando. Já no seu backend o que irá mudar é a resposta da API de validação, ela retornará um score para cada requisição. Deste modo você pode tomar alguma decisão se baseando neste score.

reCAPTCHA v3 returns a score (1.0 is very likely a good interaction, 0.0 is very likely a bot). Based on the score, you can take variable action in the context of your site. Every site is different, but below are some examples of how sites use the score. As in the examples below, take action behind the scenes instead of blocking traffic to better protect your site.

https://developers.google.com/recaptcha/docs/v3#interpreting_the_score

Outro ponto importante é que utilizei o formato explícito de integração, ou seja, nós programamos quando o CAPTCHA será renderizado, mas existe a opção de renderizá-lo de forma automática ou invisível.

Vou deixar alguns links interessantes e recomendo a leitura da documentação oficial para obter mais detalhes.