Crie a autenticação de dois fatores no Angular com Twilio Authy

January 29, 2019
Escrito por
Maciej Treder
Contribuidor
As opiniões expressas pelos colaboradores da Twilio são de sua autoria

Crie a autenticação de dois fatores no Angular com Twilio Authy

A autenticação do usuário é um requisito fundamental para muitos aplicativos do Angular e simplesmente fazer login com ID de usuário e senha é uma segurança cada vez menos adequada. A autenticação de dois fatores (2FA) oferece segurança baseada em dispositivo que é substancialmente mais difícil de invadir, mas criar seu próprio sistema 2FA é um grande desafio. O Twilio Authy facilita a adição da 2FA em aplicativos criados com Angular.

Esta publicação mostrará como adicionar o Authy no projeto do Angular. Você também aprenderá como melhorar a experiência do usuário e a segurança do app usando o Angular Universal para implementar o processo de login.

Nesta publicação, iremos:

  • Criar um app básico do Angular com uma página de login
  • Configurar um serviço de proteção de autorização e um serviço de autorização
  • Adicionar a renderização no lado do servidor com o Angular Universal
  • Configurar a autenticação do lado do servidor
  • Implementar a autenticação de dois fatores com o Twilio Authy

Pré-requisitos para criar com Angular e Authy

Para realizar essas tarefas nesta publicação, você vai precisar do:

Essas ferramentas são mencionadas nas instruções, mas não são necessárias:

Para aprender de forma mais eficaz com esta publicação, você deve ter o seguinte:

  • Conhecimento em TypeScript e da estrutura do Angular
  • Familiaridade com os observables do Angular, injeção de dependência e pipes

Há um projeto complementar para esta publicação disponível no GitHub. Cada etapa principal nesta publicação tem seu próprio branch no repositório.

Crie um projeto em Angular e gere os componentes

Cada projeto do Angular começa com a inicialização e instalação dos pacotes necessários.

Acesse o diretório em que o projeto será criado e digite as seguintes instruções de linha de comando:

ng new angular-twilio-authy --style css --routing true
cd angular-twilio-authy/
ng g c loginPage --spec false
ng g c protectedPage --spec false

Os comandos anteriores criaram o app e geraram dois componentes:

  • LoginPageComponent um formulário para coletar o ID de usuário e a senha e
  • ProtectedPageComponent uma página que será o destino do caminho home

O app usará um serviço para determinar se o usuário está autorizado a acessar páginas específicas no app, como a página ProtectedPageComponent.

Gere o AuthService digitando a seguinte na linha de comando:

ng g s auth --spec false

Implemente o serviço substituindo o conteúdo de src/app/auth.service.ts pelo seguinte código:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

@Injectable({
 providedIn: 'root'
})
export class AuthService {

 private authenticated = false;
 private redirectUrl: string;

 constructor(private router: Router) { }

 public setRedirectUrl(url: string) {
   this.redirectUrl = url;
 }

 public auth(login: string, password: string): void {
   if (login === 'foo' && password === 'bar') {
     this.authenticated = true;
     this.redirectUrl = this.redirectUrl === undefined ? '/' : this.redirectUrl;
     this.router.navigate([this.redirectUrl]);
   }
 }

 public isAuthenticated(): boolean {
   return this.authenticated;
 }
}

No método auth, os valores para o ID de usuário, login e password foram codificados para fins de simplicidade. Em um app de produção, os valores coletados na página /login e passados para o método seriam validados com os dados recuperados em um armazenamento de dados persistente, como um banco de dados.

Se as credenciais forem validadas, a flag booleano authenticated será definida como verdadeira e o usuário será redirecionado para o URL transmitido pelo AuthGuardService.

Outro serviço, conhecido como uma route guard (proteção de rota), redirecionará os usuários para longe dos componentes protegidos para a rota de /login se o valor fornecido pelo AuthService não for true. Crie o AuthGuardComponent com este comando:

ng g s authGuard --spec false

Implemente o serviço substituindo o código existente em src/app/auth-guard.service.ts pelo seguinte:

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
providedIn: 'root'
})
export class AuthGuardService implements CanActivate {

 constructor(private authService: AuthService, private router: Router) { }

 public canActivate(): boolean {
   if (!this.authService.isAuthenticated()) {
     this.authService.setRedirectUrl(this.router.url);
     this.router.navigate(['login']);
     return false;
   }
   return true;
 }
}

A classe AuthGuardService implementa a interface CanActivate, que especifica o método public canActivate(): boolean. O método usa AuthService para determinar se o usuário está autenticado. Caso contrário, o método passa o URL da rota que invocou e chamou o AuthGuardService para o AuthService e retorna false. Quando o usuário fizer login com êxito, o AuthService direciona automaticamente para a página que pretendia acessar antes de efetuar login. Se o usuário for autenticado, o método retornará true, permitindo que o usuário navegue até o componente.

LoginPageComponentProtectedPageComponent e AuthGuardService precisam ser adicionados ao roteamento do app. Substitua o conteúdo de src/app/app-routing.module.ts pelo seguinte:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProtectedPageComponent } from './protected-page/protected-page.component';
import { LoginPageComponent } from './login-page/login-page.component';
import { AuthGuardService } from './auth-guard.service';

const routes: Routes = [
 { path: '', redirectTo: 'home', pathMatch: 'full' },
 { path: 'login', component: LoginPageComponent },
 { path: 'home', component: ProtectedPageComponent, canActivate: [AuthGuardService] },
];

@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

O roteamento do app agora inclui um caminho /login e um caminho /home que usam os componentes criados anteriormente. Há também uma rota para redirecionar o URL raiz para o caminho /home.

O caminho /home usa o ProtectedPageComponent, que é protegido pelo serviço AuthGuardService da seguinte forma:

{ path: 'home', component: ProtectedPageComponent, canActivate: [AuthGuardService] },

Quando o usuário navega para o URL base do app (www.example.com) ou para o caminho /home, o Angular aciona o método canActivate, que determina se o usuário tem permissão para abrir determinado recurso.

Para simplificar o HTML processado pelo app remove todos os elementos HTML de src/app/app.component.html, exceto o seguinte:

<router-outlet></router-outlet>

Além disso, substitua o modelo da página de login em src/app/login-page/login-page.component.html pelo seguinte:

<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
 <label>login: </label><input type="text" formControlName="login" /><br/>
 <label>password: </label><input type="password" formControlName="password"/><br/>
 <input type="submit" value="log in" />
</form>

A lógica para a página de login precisa refletir o design da página e implementar AuthService. Substitua o conteúdo de src/app/login-page/login-page.component.ts pelo seguinte:

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { AuthService } from '../auth.service';

@Component({
 selector: 'app-login-page',
 templateUrl: './login-page.component.html',
 styleUrls: ['./login-page.component.css']
})
export class LoginPageComponent {

 public loginForm: FormGroup = new FormGroup({
   login: new FormControl(''),
   password: new FormControl('')
 });

 constructor(private authService: AuthService) { }

 public onSubmit(): void {
   this.authService.auth(
     this.loginForm.get('login').value,
     this.loginForm.get('password').value
   );
 }
}

Como o componente de login usa conceito de Reactive Form (Formulário reativo), o ReactiveFormsModule precisa ser incluído no ponto de entrada do app. Para isso, é necessário fazer duas alterações no arquivo src/app/app.module.ts.

1. Adicione a seguinte linha à parte superior do arquivo:

import { ReactiveFormsModule } from '@angular/forms';

2. Atualize a seção de importações adicionando ReactiveFormsModule para que ela se pareça com o seguinte:

  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule
  ],

Verifique se o app está funcionando, digitando o seguinte na linha de comando:

ng serve

Abra as ferramentas do desenvolvedor do navegador (F12) e navegue até [http://localhost:4200](http://localhost:4200). Você deve ver algo semelhante ao seguinte:

Tela de login básica do app Angular

O que acabou de acontecer? Você navegou para a rota raiz do app, que o redirecionou para o caminho /home, que é protegido pelo AuthGuardService. Como você ainda não foi autorizado, houve um redirecionado para o caminho /login.

Digite as credenciais de login (login: foo e senha: bar) e envie o formulário: você deve ver que foi redirecionado para o caminho /home e o HTML de protected-page.component.html é exibido.

Página protegida no Angular

Lembre-se de que ProtectedPageComponent é fornecido como o destino da rota /home em app-routing.module.ts e que as credenciais de login são codificadas em server.ts.

Se quiser acompanhar essa etapa, clone esse branch do repositório do GitHub e execute o app com os comandos abaixo no diretório em que deseja criar o projeto. (Você precisará ter o Git instalado em sua máquina para clonar o repositório)

git clone https://github.com/maciejtreder/angular-twilio-authy.git
cd angular-twilio-authy
git co step1
npm install 
ng serve

Mova a autenticação do usuário para o servidor

Manter a lógica de autenticação no navegador não é uma boa ideia. Nesta etapa, vamos "matar dois coelhos com uma cajadada só"adicionando o suporte do Angular Universal. O uso do Angular Universal (AU) irá: 

  1. Melhorar a experiência do usuário e aprimorar a otimização do mecanismo de pesquisa (SEO) diminuindo o tempo até a primeiro carregamento significativo da página e
  2. Mover a lógica de autorização e autenticação para o lado do servidor do app para aumentar a segurança.

Instale o suporte do Angular Universal usando o @ng-toolkit com o seguinte comando:

ng add @ng-toolkit/universal

O caminho /login que foi executado no front-end do navegador do app precisa ser substituído por um endpoint /auth/login o back-end do servidor. Substitua o código no arquivo /angular-twilio-authy/server.ts pelo seguinte:

import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {enableProdMode} from '@angular/core';
import {ngExpressEngine} from '@nguniversal/express-engine';
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';

import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';
import * as compression from 'compression';

enableProdMode();

export const app = express();

app.use(compression());
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');

app.engine('html', ngExpressEngine({
 bootstrap: AppServerModuleNgFactory,
 providers: [
   provideModuleMap(LAZY_MODULE_MAP)
 ]
}));

app.set('view engine', 'html');
app.set('views', './dist/browser');

app.post('/auth/login', (req, res) => {
 if (req.body.login === 'foo' && req.body.password === 'bar') {
   res.status(200).send({login: 'foo'});
 } else {
   res.status(401).send('Bad credentials');
 }
});

app.get('*.*', express.static('./dist/browser', {
 maxAge: '1y'
}));

app.get('/*', (req, res) => {
 res.render('index', {req, res}, (err, html) => {
   if (html) {
     res.send(html);
   } else {
     console.error(err);
     res.send(err);
   }
 });
});

Essas alterações criam um endpoint HttpPost/auth/login, que valida as credenciais fornecidas pelo usuário em relação aos valores conhecidos pelo app, como mostrado abaixo. Embora as credenciais de login sejam codificadas aqui, em um app de produção, elas normalmente são validadas contra um armazenamento de dados persistente usando uma consulta.

app.post('/auth/login', (req, res) => {
 if (req.body.login === 'foo' && req.body.password === 'bar') {
   res.status(200).send({login: 'foo'});
 } else {
   res.status(401).send('Bad credentials');
 }
});

AuthService no app consumirá o /auth/login endpoint do servidor. Para alterar a implementação de AuthService, substitua o código no arquivo src/app/auth.service.ts pelo seguinte:

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { Observable } from 'rxjs';

@Injectable({
 providedIn: 'root'
})
export class AuthService {

 private authenticated = false;
 private redirectUrl: string;

 constructor(private router: Router, private http: HttpClient) { }

 public setRedirectUrl(url: string) {
   this.redirectUrl = url;
 }

 public auth(login: string, password: string): Observable<any> {
   return this.http.post<any>('/auth/login', {login: login, password: password}).pipe(
     tap( () => {
       this.authenticated = true;
       this.redirectUrl = this.redirectUrl === undefined ? '/' : this.redirectUrl;
       this.router.navigate([this.redirectUrl]);
     })
   );
 }

 public isAuthenticated(): boolean {
   return this.authenticated;
 }
}

Como o método auth não retorna mais void e agora retorna Observable o LoginComponent precisa se inscrever nele. Altere a implementação do método onSubmit() no arquivo src/app/login-page/login-page.component.ts da seguinte forma:

(Nota: reticências ("...") em um bloco de código representa uma seção de código elaborada para brevidade.)

...
 public onSubmit(): void {
   this.authService.auth(
     this.loginForm.get('login').value,
     this.loginForm.get('password').value
   ).subscribe();
 }

Agora seria um bom momento para verificar se o app funciona como esperado. Compile e execute o servidor com os seguintes comandos:

npm run build:prod
npm run server

Abra as ferramentas do desenvolvedor (F12) no navegador e selecione a guia Rede para que você possa ver a comunicação entre o navegador e o servidor. Navegue até http://localhost:8080. O app de página única (SPA) será fornecido a partir do back-end. Anote os recursos carregados quando a página for inicialmente fornecida, conforme mostrado na ilustração abaixo.

Insira as credenciais de login e observe as alterações na guia Rede das ferramentas do desenvolvedor. Você verá que a SPA em execução no navegador executou uma chamada REST para o endpoint /auth/login e retornou um código de status de 200 (OK).

GIF mostrando o login do app Angular na página protegida

Se quiser acompanhar essa etapa, execute os seguintes comandos:

git clone https://github.com/maciejtreder/angular-twilio-authy.git
cd angular-twilio-authy
git co step2
npm install 
npm run build:prod
npm run server

(Observe que o comando para executar o app é diferente da última vez que você executou.)

Implemente a autenticação de dois fatores no Angular

O app agora tem um sistema de autenticação seguro no servidor. Mas ele depende de um único fator, o ID de usuário e a senha que estão em posse de um usuário. Este é um fator que o usuário conhece. É amplamente conhecido que essas credenciais são vulneráveis ao comprometimento e uso indevido de várias maneiras. Então, como o app pode ser mais seguro?

Uma das melhores maneiras é implementar um segundo fator de autenticação.

Além de algo que o usuário conhece, a autenticação de dois fatores também requer algo que o usuário possui (posse) ou é (herança).

A autenticação biométrica implementa a herança exigindo que as características físicas exclusivas de um usuário, como uma impressão digital ou um padrão de retina, estabeleçam sua identidade. Esse método é muito seguro, mas pode exigir hardware caro e de finalidade especial.

Twilio Authy para autenticação de dois fatores

Twilio Authy fornece um segundo fator por meio de posse, algo que quase todas as pessoas tem: um telefone celular (ou outro dispositivo).

Se o telefone usar reconhecimento facial ou uma impressão digital para fazer o desbloqueio, os aspectos da autenticação baseada em herança também serão implementados: o usuário precisa saber sua ID e senha, possuir seu telefone e (opcionalmente) ser o usuário com o rosto ou dedo necessário para desbloquear o telefone ou o computador. Um agente mal-intencionado pode comprometer o ID de login e a senha de um usuário, mas é muito mais difícil para ele conseguir as credenciais, o dispositivo e os dados biométricos associados a um usuário, pelo menos por um período significativo.

O Authy tem vários recursos que aprimoram simultaneamente a experiência do usuário e fornecem opções flexíveis de autenticação:

  • Notificações por push
  • Tokens de software (códigos de uso único)
  • Códigos de segurança de SMS e voz
  • Aplicativos Authy para iOS, Android, Windows e macOS

Esta parte do projeto mostrará a rapidez com que você pode adicionar a 2FA a um app do Angular com o app Authy. Você precisará de uma conta da Twilio para concluir essas etapas. Você pode se inscrever em uma conta de avaliação gratuita em alguns minutos.

Depois de ter uma conta da Twilio, faça login e navegue até a seção do Authy no Console da Twilio e execute as etapas a seguir. Você estará pronto em alguns minutos.

  1. Na seção Authy do console da Twilio, crie um novo app.
  2. Copie a Production API Key (chave da API de produção) do app em um local seguro. (Você pode encontrar a chave nas Configurações do app se você a perder.)
  3. No app que você criou, registre-se como um novo usuário usando seu endereço de e-mail e número de celular preferidos.
  4. Copie o Authy ID do usuário que você acabou de criar em um local seguro.
  5. Instale o app Authy no seu telefone celular. Você deve ter recebido uma notificação por texto com um link para receber os códigos e concluir a instalação.

Quando tiver concluído com êxito as etapas anteriores, já será possível implementar a autenticação de dois fatores. O diagrama a seguir ilustra o fluxo de comunicação entre o navegador, o servidor e a Twilio:

Autenticação de dois fatores com diagrama de fluxo Authy
  1. O cliente faz a solicitação GET
  2. O servidor gera a página de login
  3. O cliente envia o POST do /auth/login
  4. O servidor solicita autenticação de autenticação de dois fatores do Authy
  5. O Authy responde com request_id
  6. O servidor passa request_id para o navegador
  7. O servidor pergunta ao navegador (repetidamente) se o usuário autorizou com dois fatores
  8. O servidor solicita o status para a Twilio
  9. A Twilio responde com status de autorização
  10. O servidor passa o status para o navegador

Retorne à linha de comando no diretório do projeto e instale as dependências necessárias no projeto executando os seguintes comandos:

npm install authy
npm install cookie-parser

A implementação da autenticação de dois fatores no servidor exigirá dois novos endpoints e a modificação do endpoint /auth/login existente:

  • /auth/status envia o status da resposta de um usuário à solicitação do Authy fornecida (aprovadorejeitado, sem resposta).
  • /auth/isLogged envia ao navegador um cookie criptografado para indicar que o usuário está conectado.
  • /auth/login recebe o resultado do processo do Authy, além de verificar as credenciais de login.

Substitua o código no server.ts pelo seguinte:

import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {enableProdMode} from '@angular/core';
import {ngExpressEngine} from '@nguniversal/express-engine';
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';

import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as cors from 'cors';
import * as compression from 'compression';
import * as cookieParser from 'cookie-parser';

const API_KEY = 'Production API Key';
const authy = require('authy')(API_KEY);

enableProdMode();

export const app = express();

app.use(compression());
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());

const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');

app.engine('html', ngExpressEngine({
 bootstrap: AppServerModuleNgFactory,
 providers: [
   provideModuleMap(LAZY_MODULE_MAP)
 ]
}));

app.set('view engine', 'html');
app.set('views', './dist/browser');

app.post('/auth/login', (req, res) => {
 if (req.body.login === 'foo' && req.body.password === 'bar') {
   authy.send_approval_request('Authy ID', {
       message: 'Request to login to Angular two factor authentication with Twilio'
     }, null, null,  function(err, authResponse) {
       if (err) {
         res.status(400).send('Bad Request');
       } else {
         res.status(200).send({token: authResponse.approval_request.uuid});
       }
   });
 } else {
   res.status(401).send('Bad credentials');
 }
});

app.get('/auth/status', (req, res) => {
 authy.check_approval_status(req.headers.token, (err, authResponse) => {
   if (err) {
     res.status(400).send('Bad Request.');
   } else {
     if (authResponse.approval_request.status === 'approved') {
       res.cookie('authentication', 'super-encrypted-value-indicating-that-user-is-authenticated!', {
         maxAge: 5 * 60 * 60 * 60,
         httpOnly: true
       });
     }
     res.status(200).send({status: authResponse.approval_request.status});
   }
 });
});

app.get('/auth/isLogged', (req, res) => {
 res.status(200).send({authenticated: req.cookies.authentication === 'super-encrypted-value-indicating-that-user-is-authenticated!'});
});

app.get('*.*', express.static('./dist/browser', {
 maxAge: '1y'
}));

app.get('/*', (req, res) => {
 res.render('index', {req, res}, (err, html) => {
   if (html) {
     res.send(html);
   } else {
     console.error(err);
     res.send(err);
   }
 });
});

Agora substitua os espaços reservados pelos valores copiados do console da Twilio ao configurar o Authy. Coloque a Production API Key na seção de declarações:

const API_KEY = 'Production API Key';
const authy = require('authy')(API_KEY);

Para simplificar esta demonstração, codificamos por hardware os valores para login (ID de usuário) e password. Em um app de produção, você normalmente verificaria os valores fornecidos em relação a um armazenamento de dados persistente.

Se o usuário tiver fornecido credenciais válidas, a próxima etapa na 2FA será acionar uma mensagem do Twilio Authy para o dispositivo do usuário usando o ID do usuário no Authy. Assim como ignoramos o processo de registro de um usuário em nosso app, também estamos ignorando o processo de registro programático de um usuário com o Authy. Para tornar esta demonstração mais simples, estamos codificando o ID do Authy criado ao se inscrever como usuário diretamente no Authy.  

No código do endpoint /auth/login, substitua o espaço reservado pelo valor do Authy ID salvo quando você criou um usuário:

app.post('/auth/login', (req, res) => {
 if (req.body.login === 'foo' && req.body.password === 'bar') {
   authy.send_approval_request('Authy ID', {
       message: 'Request to login to Angular two factor authentication with Twilio'
     }, null, null,  function(err, authResponse) {
       if (err) {
         res.status(400).send('Bad Request');
       } else {
         res.status(200).send({token: authResponse.approval_request.uuid});
       }
   });
 } else {
   res.status(401).send('Bad credentials');
 }
});

Quando um usuário aprova a autorização com o app, o servidor envia o cookie contendo o valor supercriptografado. O cliente poderá determinar se o usuário está conectado ou não chamando o endpoint/auth/isLogged.

Dê uma olhada no código do endpoint /auth/isLogged mostrado abaixo. Embora o valor do cookie seja codificado, em um app de produção, você deve gerar um valor exclusivo e criptografado.

app.get('/auth/isLogged', (req, res) => {
 res.status(200).send({authenticated: req.cookies.authentication === 'super-encrypted-value-indicating-that-user-is-authenticated!'});
});

Porque o cookie tem um atributo httpOnly, ele é inacessível no navegador. O cookie é um ótimo lugar para usar um JSON Web Token (JWT) contendo o escopo de autorização do usuário ou outros dados confidenciais. O cookie criptografado também fornece proteção contra o roubo de dados de token em um ataque cross-site scripting (XSS).

Agora que os novos endpoints estão no servidor, eles podem ser consumidos no AuthService. Substitua o código existente em src/app/auth.service.ts pelo seguinte:

import { Injectable, Inject, PLATFORM_ID, Optional } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { map, flatMap } from 'rxjs/operators';
import { Observable, timer, of, Subscription, Subject } from 'rxjs';
import { isPlatformServer } from '@angular/common';
import { REQUEST } from '@nguniversal/express-engine/tokens';

@Injectable({
 providedIn: 'root'
})
export class AuthService {

 private redirectUrl: string;

 constructor(
   private router: Router,
   private http: HttpClient,
   @Inject(PLATFORM_ID) private platformId: any,
   @Optional() @Inject(REQUEST) private request: any
 ) { }

 public setRedirectUrl(url: string) {
   this.redirectUrl = url;
 }

 public auth(login: string, password: string): Observable<any> {
   return this.http.post<any>('/auth/login', {login: login, password: password}).pipe(
     flatMap(response => this.secondFactor(response.token) )
   );
 }

 private secondFactor(token: string): Observable<any> {
   const httpOptions = {
     headers: new HttpHeaders({'Token':  token})
   };

   const tick: Observable<number> = timer(1000, 1000);
   return Observable.create(subject => {
     let tock = 0;
     const timerSubscription = tick.subscribe(() => {
       tock++;
       this.http.get<any>('/auth/status', httpOptions).subscribe( response => {
         if (response.status === 'approved') {
           this.redirectUrl = this.redirectUrl === undefined ? '/' : this.redirectUrl;
           this.router.navigate([this.redirectUrl]);

           this.closeSecondFactorObservables(subject, true, timerSubscription);
         } else if (response.status === 'denied') {
           this.closeSecondFactorObservables(subject, false, timerSubscription);
         }
       });
       if (tock === 60) {
         this.closeSecondFactorObservables(subject, false, timerSubscription);
       }
     });
   });
 }

 public isAuthenticated(): Observable<boolean> {
   if (isPlatformServer(this.platformId)) {
     return of(this.request.cookies.authentication === 'super-encrypted-value-indicating-that-user-is-authenticated!');
   }
   return this.http.get<any>('/auth/isLogged').pipe(map(response => response.authenticated));
 }


 private closeSecondFactorObservables(subject: Subject<any>, result: boolean, timerSubscription: Subscription): void {
   subject.next(result);
   subject.complete();
   timerSubscription.unsubscribe();
 }
}

Isso é muito código. O que mudou?

Primeiro, o método "auth" usa a autenticação de dois fatores com o Authy. Quando a chamada observable do endpoint /auth/login responde com uma mensagem positiva, o método secondFactor() é chamado usando o token da API do Authy como um argumento:

 public auth(login: string, password: string): Observable<any> {
   return this.http.post<any>('/auth/login', {login: login, password: password}).pipe(
     flatMap(response => this.secondFactor(response.token) )
   );
 }

A função secondFactor() usa o método timer da biblioteca rxjs para emitir um número a cada segundo enquanto aguarda uma resposta do endpoint /auth/status no servidor. Se a resposta for approved, o usuário é redirecionado para o URL fornecido para a função auth. Este URL é o caminho para o qual o usuário tentou originalmente navegar antes de ser solicitado a fazer login.

Se a resposta de /auth/status for denied, ou não houver resposta após 60 segundos, o observable retorna false e fecha.

 private secondFactor(token: string): Observable<any> {
   const httpOptions = {
     headers: new HttpHeaders({
       'Token':  token
     })
   };
   const tick: Observable<number> = timer(1000, 1000);
   return Observable.create(subject => {
     let tock = 0;
     const timerSubscription = tick.subscribe(() => {
       tock++;
       this.http.get<any>('/auth/status', httpOptions).subscribe( response => {
         if (response.status === 'approved') {
           this.redirectUrl = this.redirectUrl === undefined ? '/' : this.redirectUrl;
           this.router.navigate([this.redirectUrl]);
           this.closeSecondFactorObservables(subject, true, timerSubscription);
         } else if (response.status === 'denied') {
           this.closeSecondFactorObservables(subject, false, timerSubscription);
         }
       });
       if (tock === 60) {
         this.closeSecondFactorObservables(subject, false, timerSubscription);
       }
     });
   });
 }

O método canActivate de AuthGuardService precisa consumir o AuthService por um URL observable e retornar o URL de redirecionamento em vez de um booleano. Faça essas alterações copiando o seguinte código no arquivo src/app/auth-guard.service.ts:

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from './auth.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({
 providedIn: 'root'
})
export class AuthGuardService implements CanActivate {

 constructor(private authService: AuthService, private router: Router) { }

 public canActivate(): Observable<boolean> {
   return this.authService.isAuthenticated().pipe(map(isAuth => {
     if (!isAuth) {
       this.authService.setRedirectUrl(this.router.url);
       this.router.navigate(['login']);
     }
     return isAuth;
   }));
 }
}

LoginPageComponent deve manter o usuário informado sobre o status do processo de login. Isso pode ser feito com um rxjs BehaviorSubject, que é um tipo observable que permite que o LoginPageComponent envie os valores de login e password para o método auth e receba o status da tentativa de autenticação.

Substitua o conteúdo do arquivo src/app/login-page/login-page.component.ts pelo seguinte:

import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
import { AuthService } from '../auth.service';
import { BehaviorSubject, Subject, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Component({
selector: 'app-login-page',
templateUrl: './login-page.component.html',
styleUrls: ['./login-page.component.css']
})
export class LoginPageComponent {

 public message: Subject<string> = new BehaviorSubject('');
 public loginForm: FormGroup = new FormGroup({
   login: new FormControl(''),
   password: new FormControl('')
 });

 constructor(private authService: AuthService) { }

 public onSubmit(): void {
   this.message.next('Waiting for second factor.');
   this.authService.auth(
     this.loginForm.get('login').value,
     this.loginForm.get('password').value
   ).pipe(
   catchError(() => {
     this.message.next('Bad credentials.');
     return throwError('Not logged in!');
   })

   )
   .subscribe(response => {
     if (!response) {
       this.message.next('Request timed out or not authorized');
     }
   });
 }
}

O valor da message do BehaviorSubject pode ser exibido na página de login. Adicione o seguinte código na parte inferior do código existente no arquivo  src/app/login-page/login-page.component.html:

...
<h1>{{message | async}}</h1>

Execute e teste a autenticação de dois fatores com o Authy

Recompile e execute o app digitando os seguintes comandos:

npm run build:prod
npm run server

Abra as ferramentas do desenvolvedor (F12) no navegador e selecione a guia Rede para que você possa assistir a comunicação entre o navegador e o servidor. Navegue até http://localhost:8080 e digite as credenciais de login (login: foo, senha: bar) e clique em Enviar

Duas coisas devem acontecer em sucessão rápida:

  1. A guia Rede exibirá uma série de eventos de "status" como resultado de o LoginPageComponent receber a notificação por push uma vez por segundo do secondFactor observable no AuthService. Esses eventos continuarão a ocorrer até que o Authy receba uma resposta do app em seu telefone ou até que 60 segundos tenham se passado.
  2. Você deve receber uma solicitação de envio como a mostrada abaixo no seu dispositivo móvel.

Notificação push do Authy na tela inicial do telefone
Demonstração da solicitação de login no Authy com as opções &#x27;Deny&#x27; (Negar) e &#x27;Approve&#x27; (Aprovar)

Aprove a solicitação de autenticação. O app móvel transmitirá com segurança a aprovação para a infraestrutura do Authy do Twilio, que enviará uma notificação por push ao processo do servidor.

O SPA do Angular em execução no navegador receberá a resposta positiva do endpoint /auth/status no servidor com o cookie de autenticação. Na ilustração abaixo, você pode ver a resposta isLogged e o cookie:

Página protegida do Angular assim que a solicitação de autenticação de dois fatores do Authy é aceita

Se quiser acompanhar essa etapa, execute os seguintes comandos:

git clone https://github.com/maciejtreder/angular-twilio-authy.git
cd angular-twilio-authy
git co step3
npm install 
npm run build:prod
npm run server

Preparando o Angular e o Authy para produção

Como mostra este estudo de caso, você pode implementar facilmente a autenticação de dois fatores completa e de alto desempenho em um app do Angular usando o Twilio Authy e o Angular Universal. Mas há outras considerações antes de seu app estar pronto para produção.

A mais importante é a implementação de TLS/SSL para a comunicação criptografada entre o navegador e o servidor. O processo completo de implementação de TLS/SSL está além do escopo deste tutorial e dependerá do seu sistema operacional e de outros fatores, mas você pode configurar facilmente o código mostrado acima para consumir um certificado OpenSSL autoassinado instalado em sua máquina de desenvolvimento.

Substitua o conteúdo do arquivo local.js pelo seguinte código e modifique os caminhos para localhost.key e localhost.cert conforme necessário para sua configuração:

// generated by @ng-toolkit/universal
const port = process.env.PORT || 8080;

const serverApp = require('./dist/server');

const https = require('https');
const fs = require('fs');

const options = {
 key: fs.readFileSync( './localhost.key' ),
 cert: fs.readFileSync( './localhost.cert' ),
 requestCert: false,
 rejectUnauthorized: false
};

const httpsServer = https.createServer( options, serverApp.app );

httpsServer.listen(port, () => {
   console.log("Listening on: https://localhost:" + port );
});

Esse código pode ser encontrado no branch step4 do repositório complementar.

Outras considerações de produção incluem:

  • Armazenamento de credenciais de usuário (ID de usuário e senha) de forma criptografada em um armazenamento de dados persistente (como um banco de dados)
  • Validação e armazenamento seguros de números de telefone associados a contas de usuário
  • Criar novos usuários no Authy e armazenar seu Authy ID com segurança no armazenamento de dados
  • Criptografia do cookie usado para validar a autenticação entre o navegador e o servidor com JSON Web tokens (JWT) ou outro técnica
  • Configuração de requisitos de ID de usuário e validação de endereços de e-mail
  • Definir requisitos de senha e lidar com redefinições de senha

Procure publicações sobre esses tópicos aqui no blog da Twilio.

Resumo: criando a autenticação de dois fatores com Angular e Authy

Falamos sobre um desafio importante para todos os aplicativos maduros: segurança com autenticação de dois fatores (2FA). Vimos como implementar a autenticação no servidor usando o Angular Universal, o que torna o processo rápido e seguro. E implementamos a 2FA com o Twilio Authy, um conjunto abrangente de ferramentas para autenticação, incluindo uma API, um console baseado na Web e aplicativos móveis.

Recursos adicionais do Angular Universal

Se quiser saber mais sobre as técnicas do Angular Universal, confira estas outras publicações no blog da Twilio:

O repositório para o código usado nesta publicação pode ser encontrado no GitHub.

Este artigo foi traduzido do original "Build Two-factor Authentication in Angular with Twilio Authy". Enquanto melhoramos nossos processos de tradução, adoraríamos receber seus comentários em help@twilio.com - contribuições valiosas podem render brindes da Twilio.