Создание веб-приложения с двухфакторной аутентификацией на Angular 7

В этой статье мы узнаем, как интегрировать двухфакторную аутентификацию Google Authenticator  в приложение Angular 7, используя Node JS на стороне сервера. Изучив это руководство, вы сможете создать приложение с простой формой для регистрации и авторизации с помощью двухфакторной аутентификации.

Репозиторий с исходным кодом на GitHub

Содержание

Ресурсы

  1. Node JS (LTS) — [Скачать].
  2. Google Authenticator — [Скачать: Android] [Скачать: iOS]

После установки перечисленных выше ресурсов займемся созданием API для приложения.

Шаг 1: серверное приложение

Для создания API-сервисов мы будем использовать небольшой фреймворк для Node.js, который называется Express.js. Создадим папку ‘back-end’ для нашего серверного приложения. Затем перейдём в неё в терминале командной строки и установим необходимые зависимости.

> mkdir back-end
> cd back-end
> npm init -y
> npm install --save express body-parser cors qrcode speakeasy

Мы создали папку ‘back-end’ и инициализировали проект Node.js, установив следующие зависимости:

  1. express — это небольшой настраиваемый фреймворк для создания API сервисов.
  2. body-parser — для анализа методов HTTP.
  3. cors —  пакет используется для интеграции клиентской части веб-приложения с API сервисами.
  4. qrcode — отвечает за генерацию QR-код в виде изображений base64.
  5. speakeasy— генератор секретных ключей по алгоритму T-OTP, который использует Google Authenticator.

Теперь создадим несколько API-сервисов, в которых главным исполняемым файлом будет app.js. Для упрощения изучения материала мы опустим код, отвечающий за взаимодействие с базой данных.

Структура папок для серверной части

API-сервисы реализуют функционал входа в систему, регистрации и TFA (двухфакторную аутентификацию):

Сервис входа в систему

Включает в себя базовую функциональность для входа с помощью логина, пароля и кода аутентификации.

const express = require('express');
const speakeasy = require('speakeasy');
const commons = require('./commons');
const router = express.Router();

router.post('/login', (req, res) => {
    console.log(`DEBUG: Received login request`);

    if (commons.userObject.uname && commons.userObject.upass) {
        if (!commons.userObject.tfa || !commons.userObject.tfa.secret) {
            if (req.body.uname == commons.userObject.uname && req.body.upass == commons.userObject.upass) {
                console.log(`DEBUG: Login without TFA is successful`);

                return res.send({
                    "status": 200,
                    "message": "success"
                });
            }
            console.log(`ERROR: Login without TFA is not successful`);

            return res.send({
                "status": 403,
                "message": "Invalid username or password"
            });

        } else {
            if (req.body.uname != commons.userObject.uname || req.body.upass != commons.userObject.upass) {
                console.log(`ERROR: Login with TFA is not successful`);

                return res.send({
                    "status": 403,
                    "message": "Invalid username or password"
                });
            }
            if (!req.headers['x-tfa']) {
                console.log(`WARNING: Login was partial without TFA header`);

                return res.send({
                    "status": 206,
                    "message": "Please enter the Auth Code"
                });
            }
            let isVerified = speakeasy.totp.verify({
                secret: commons.userObject.tfa.secret,
                encoding: 'base32',
                token: req.headers['x-tfa']
            });

            if (isVerified) {
                console.log(`DEBUG: Login with TFA is verified to be successful`);

                return res.send({
                    "status": 200,
                    "message": "success"
                });
            } else {
                console.log(`ERROR: Invalid AUTH code`);

                return res.send({
                    "status": 206,
                    "message": "Invalid Auth Code"
                });
            }
        }
    }

    return res.send({
        "status": 404,
        "message": "Please register to login"
    });
});

module.exports = router;

В этой статье мы не будем использовать базу данных для хранения данных пользователей. Поэтому реализуем это на стороне сервера.

let userObject = {};

module.exports = {
    userObject
};

Сервис регистрации

Регистрация пользователя в приложении будет заключаться в добавлении логина и пароля в объект userObject. А также в удалении существующей в нем информации. Модули входа и регистрации создавались исключительно в демонстрационных целях, поэтому приложение будет поддерживать только одного пользователя.

const commons = require('./commons');
const router = express.Router();

router.post('/register', (req, res) => {
    console.log(`DEBUG: Received request to register user`);

    const result = req.body;

    if ((!result.uname && !result.upass) || (result.uname.trim() == "" || result.upass.trim() == "")) {
        return res.send({
            "status": 400,
            "message": "Username/ password is required"
        });
    }

    commons.userObject.uname = result.uname;
    commons.userObject.upass = result.upass;
    delete commons.userObject.tfa;

    return res.send({
        "status": 200,
        "message": "User is successfully registered"
    });
});

module.exports = router;

Сервис TFA

Сервис предназначен для реализации двухфакторной аутентификации наряду с верификацией кода T-OTP, сгенерированного Google Authenticator. Он будет включать в себя функциональность для получения настроек TFA, а также включения или отключения TFA для userObject.

const express = require('express');
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');
const commons = require('./commons');
const router = express.Router();

router.post('/tfa/setup', (req, res) => {
    console.log(`DEBUG: Received TFA setup request`);

    const secret = speakeasy.generateSecret({
        length: 10,
        name: commons.userObject.uname,
        issuer: 'NarenAuth v0.0'
    });
    var url = speakeasy.otpauthURL({
        secret: secret.base32,
        label: commons.userObject.uname,
        issuer: 'NarenAuth v0.0',
        encoding: 'base32'
    });
    QRCode.toDataURL(url, (err, dataURL) => {
        commons.userObject.tfa = {
            secret: '',
            tempSecret: secret.base32,
            dataURL,
            tfaURL: url
        };
        return res.json({
            message: 'TFA Auth needs to be verified',
            tempSecret: secret.base32,
            dataURL,
            tfaURL: secret.otpauth_url
        });
    });
});

router.get('/tfa/setup', (req, res) => {
    console.log(`DEBUG: Received FETCH TFA request`);

    res.json(commons.userObject.tfa ? commons.userObject.tfa : null);
});

router.delete('/tfa/setup', (req, res) => {
    console.log(`DEBUG: Received DELETE TFA request`);

    delete commons.userObject.tfa;
    res.send({
        "status": 200,
        "message": "success"
    });
});

router.post('/tfa/verify', (req, res) => {
    console.log(`DEBUG: Received TFA Verify request`);

    let isVerified = speakeasy.totp.verify({
        secret: commons.userObject.tfa.tempSecret,
        encoding: 'base32',
        token: req.body.token
    });

    if (isVerified) {
        console.log(`DEBUG: TFA is verified to be enabled`);

        commons.userObject.tfa.secret = commons.userObject.tfa.tempSecret;
        return res.send({
            "status": 200,
            "message": "Two-factor Auth is enabled successfully"
        });
    }

    console.log(`ERROR: TFA is verified to be wrong`);

    return res.send({
        "status": 403,
        "message": "Invalid Auth Code, verification failed. Please verify the system Date and Time"
    });
});

module.exports = router;

Упомянутые выше сервисы включены в один исполняемый файл ‘app.js’, расположенный в корневой папке. Этот код запустит HTTP-сервер, созданный с помощью express.js на локальном хосте с портом 3000.

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cors = require('cors');
const login = require('./routes/login');
const register = require('./routes/register');
const tfa = require('./routes/tfa');

app.use(bodyParser.json());
app.use(cors());

app.use(login);
app.use(register);
app.use(tfa);

app.listen('3000', () => {
    console.log('The server started running on http://localhost:3000');
});

Мы реализовали серверную часть кода веб-приложения.

Следующим шагом будет создание простого приложения, использующего эти сервисы на Angular 7.

Шаг 2: приложение на базе Angular 7

Сначала нужно установить Angular. После этого мы создадим приложение с названием ‘front-end’ и установим зависимость от ‘bootstrap’ (ссылки на bootstrap.min.css в styles.css), перейдя в папку front-end.

> npm install -g @angular/cli
> ng new front-end
> cd front-end
> npm install --save bootstrap
> ng serve

После этого создадим несколько компонентов и сервисов, которые требуются приложению.

Для целей демонстрации мы создадим LoginService и два guards  — ‘Auth Guard’ и ‘Login Guard.’

> ng g s services/login-service/login-service --spec=false
> ng g g guards/AuthGuard
> ng g g guards/Login

Guards, которые мы создаем, относятся к типу CanActivate. Сервис входа в систему будет включать в себя HTTP-запросы к сервисам, созданным на стороне сервера.

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class LoginServiceService {
  headerOptions: any = null

  _isLoggedIn: boolean = false

  authSub = new Subject<any>();

  constructor(private _http: HttpClient) {
  }

  loginAuth(userObj: any) {
    if (userObj.authcode) {
      console.log('Appending headers');
      this.headerOptions = new HttpHeaders({
        'x-tfa': userObj.authcode
      });
    }
    return this._http.post("http://localhost:3000/login", { uname: userObj.uname, upass: userObj.upass }, { observe: 'response', headers: this.headerOptions });
  }

  setupAuth() {
    return this._http.post("http://localhost:3000/tfa/setup", {}, { observe: 'response' })
  }

  registerUser(userObj: any) {
    return this._http.post("http://localhost:3000/register", { uname: userObj.uname, upass: userObj.upass }, { observe: "response" });
  }

  updateAuthStatus(value: boolean) {
    this._isLoggedIn = value
    this.authSub.next(this._isLoggedIn);
    localStorage.setItem('isLoggedIn', value ? "true" : "false");
  }

  getAuthStatus() {
    this._isLoggedIn = localStorage.getItem('isLoggedIn') == "true" ? true : false;
    return this._isLoggedIn
  }

  logoutUser() {
    this._isLoggedIn = false;
    this.authSub.next(this._isLoggedIn);
    localStorage.setItem('isLoggedIn', "false")
  }

  getAuth() {
    return this._http.get("http://localhost:3000/tfa/setup", { observe: 'response' });
  }

  deleteAuth() {
    return this._http.delete("http://localhost:3000/tfa/setup", { observe: 'response' });
  }

  verifyAuth(token: any) {
    return this._http.post("http://localhost:3000/tfa/verify", { token }, { observe: 'response' });
  }
}

AuthGuard ограничит навигацию пользователя только домашней страницей, если тот не вошел в систему.

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { LoginServiceService } from 'src/app/services/login-service/login-service.service';

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

  constructor(private _loginService: LoginServiceService, private _router: Router) {
  }
  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    if (this._loginService.getAuthStatus()) {
      return true;
    }

    this._router.navigate(['/login'])

    return false;
  }
}

LoginGuard не позволит пользователю заходить на страницу авторизации, если пользователь уже вошел в систему.

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { LoginServiceService } from 'src/app/services/login-service/login-service.service';

@Injectable({
  providedIn: 'root'
})
export class LoginGuard implements CanActivate {
  constructor(private _loginService: LoginServiceService, private _router: Router) {
  }
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot) {
    if (!this._loginService.getAuthStatus()) {
      return true;
    }

    this._router.navigate(['/home'])
    return false;
  }

}

Мы завершили создание основы для нашего приложения, создав службы и guards. Теперь разработаем несколько компонентов.

> ng g c components/header --spec=false
> ng g c components/home --spec=false
> ng g c components/login --spec=false
> ng g c components/register --spec=false

После создания необходимых компонентов приложения мы настроим маршрутизацию для приложения, связав соответствующие guards для активации маршрутов.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { LoginComponent } from './components/login/login.component';
import { RegisterComponent } from './components/register/register.component';
import { AuthGuardGuard } from './guards/AuthGuard/auth-guard.guard';
import { LoginGuard } from './guards/Login/login.guard';

const routes: Routes = [
  { path: "", redirectTo: '/login', pathMatch: 'full', canActivate: [LoginGuard] },
  { path: "login", component: LoginComponent, canActivate: [LoginGuard] },
  { path: "home", component: HomeComponent, canActivate: [AuthGuardGuard] },
  { path: "register", component: RegisterComponent, canActivate: [LoginGuard] },
  { path: "**", redirectTo: '/login', pathMatch: 'full', canActivate: [LoginGuard] }
];

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

Теперь удалим код, добавленный по умолчанию в app.component.html, и вставим компонент общего заголовка и вывод маршрутизатора.

<app-header></app-header>
<router-outlet></router-outlet>

Компонент заголовка

Это общий компонент для других компонентов, который включает в себя панель навигации приложения. Видимость ссылок в заголовке контролируется методом getAuthStatus() сервиса LoginService.

<nav class="navbar navbar-expand-sm navbar-dark bg-dark" style="z-index: 99999;">
  <a class="navbar-brand" [routerLink]="['/login']">NarenAuth v0.0</a>
  <button class="navbar-toggler d-lg-none" type="button" data-toggle="collapse" data-target="#collapsibleNavId"
    aria-controls="collapsibleNavId" aria-expanded="false" aria-label="Toggle navigation" (click)="toggleMenuBar()">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="collapsibleNavId">
    <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
      <li class="nav-item" [routerLinkActive]="['active']" *ngIf="!isLoggedIn">
        <a class="nav-link" [routerLink]="['/login']">Login</a>
      </li>
      <li class="nav-item" [routerLinkActive]="['active']" *ngIf="!isLoggedIn">
        <a class="nav-link" [routerLink]="['/register']">Register</a>
      </li>
    </ul>
    <ul class="navbar-nav ml-auto">
      <li class="nav-item" *ngIf="isLoggedIn">
        <a class="nav-link" (click)="logout()">Logout</a>
      </li>
    </ul>
  </div>
</nav>

В фоновом режиме мы также запросим файл *.ts для компонента заголовка.

import { Component, OnInit } from '@angular/core';
import { LoginServiceService } from 'src/app/services/login-service/login-service.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
  isLoggedIn: boolean = false
  constructor(private _loginService: LoginServiceService, private _router: Router) {
    this._loginService.authSub.subscribe((data) => {
      this.isLoggedIn = data
    })
  }

  ngOnInit() {
    this.isLoggedIn = this._loginService.getAuthStatus()
  }

  toggleMenuBar() {
    if(document.getElementById("collapsibleNavId").style.display == "block") {
      document.getElementById("collapsibleNavId").style.display = "none";
    } else {
      document.getElementById("collapsibleNavId").style.display = "block";
    }
  }

  logout() {
    this._loginService.logoutUser()
    this._router.navigate(['/login'])
  }
}

Компонент входа в систему

Предназначен для получения логина, пароля и кода AuthCode (если включен TFA) от пользователя и его проверки на стороне сервера. Если данные верны, то пользователь будет перемещен в HomeComponent.

<div class="container">
  <div class="card card-container">
    <img id="profile-img" class="profile-img-card" src="assets/images/avatar_2x.png" />
    <form class="form-signin" (ngSubmit)="loginUser()" #loginForm="ngForm">
      <input type="text" id="uname" class="form-control" name="uname" autocomplete="off" #uname="ngModel"
        [(ngModel)]="userObject.uname" placeholder="Username" title="Please enter the username" required autofocus>
      <input type="password" id="upass" class="form-control" name="upass" autocomplete="off" #upass="ngModel"
        [(ngModel)]="userObject.upass" placeholder="Password" title="Please enter the password" required>
      <input type="text" id="authcode" class="form-control" *ngIf="this.tfaFlag" name="authcode" autocomplete="off"
        #authcode="ngModel" [(ngModel)]="userObject.authcode" placeholder="Two-Factor Auth code"
        title="Please enter the code" required>
      <button class="btn btn-lg btn-primary btn-block btn-signin" type="submit"
        [disabled]="uname?.errors?.required || upass?.errors?.required || (this.tfaFlag && authcode?.errors?.required)">Sign
        in</button>
      <p style="text-align:center;">Want to reset login? <a [routerLink]="['/register']">Register
          here</a></p>
      <p class="text-danger" style="text-align:center;" *ngIf="errorMessage">{{errorMessage}}</p>
    </form>
  </div>
</div>

Мы также будем проверять статус, полученный от серверной части кода, для отображения сообщений пользователю.

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router'
import { LoginServiceService } from 'src/app/services/login-service/login-service.service';

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

  tfaFlag: boolean = false
  userObject = {
    uname: "",
    upass: ""
  }
  errorMessage: string = null
  constructor(private _loginService: LoginServiceService, private _router: Router) {
  }

  ngOnInit() {
  }

  loginUser() {
    this._loginService.loginAuth(this.userObject).subscribe((data) => {
      this.errorMessage = null;
      if (data.body['status'] === 200) {
        this._loginService.updateAuthStatus(true);
        this._router.navigateByUrl('/home');
      }
      if (data.body['status'] === 206) {
        this.tfaFlag = true;
      }
      if (data.body['status'] === 403) {
        this.errorMessage = data.body['message'];
      }
      if (data.body['status'] === 404) {
        this.errorMessage = data.body['message'];
      }
    })
  }
}

Компонент регистрации

Мы зарегистрируем одного пользователя во всем приложении.

<div class="container">
  <div class="card card-container">
    <img id="profile-img" class="profile-img-card" src="assets/images/avatar_2x.png" />
    <form class="form-signin" (ngSubmit)="registerUser()" #registerForm="ngForm">
      <input type="text" id="uname" class="form-control" name="uname" #uname="ngModel" [(ngModel)]="userObject.uname"
        placeholder="Username" title="Please enter the username" autocomplete="off" required autofocus>
      <input type="password" id="upass" class="form-control" name="upass" placeholder="Password"
        title="Please enter the password" #upass="ngModel" autocomplete="off" [(ngModel)]="userObject.upass" required>
      <input type="password" id="confirmpass" class="form-control" name="confirmpass" placeholder="Confirm password"
        title="Please re-enter the password" #uconfirmpass="ngModel" autocomplete="off" [(ngModel)]="confirmPass"
        required>
      <button class="btn btn-lg btn-primary btn-block btn-signin" type="submit"
        [disabled]="(uname?.errors?.required || upass?.errors?.required || uconfirmpass?.errors?.required) || (upass.value !== uconfirmpass.value)">Sign
        up</button>
        <p style="text-align:center;">Remember credentials? <a [routerLink]="['/login']">Login
          here</a></p>
      <p class="text-success" style="text-align:center;" *ngIf="errorMessage">{{errorMessage}}</p>
    </form>
  </div>
</div>

Если вы забудете логин и пароль для приложения, или секретный ключ TFA, просто введите новое имя пользователя и пароль на странице.

import { Component, OnInit } from '@angular/core';
import { LoginServiceService } from 'src/app/services/login-service/login-service.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {
  errorMessage: string = null
  userObject = {
    uname: "",
    upass: ""
  }
  confirmPass: string = ""

  constructor(private _loginService: LoginServiceService, private _router: Router) { }

  ngOnInit() {
  }

  registerUser() {
    if (this.userObject.uname.trim() !== "" && this.userObject.upass.trim() !== "" && (this.userObject.upass.trim() === this.confirmPass))
      this._loginService.registerUser(this.userObject).subscribe((data) => {
        const result = data.body
        if (result['status'] === 200) {
          this.errorMessage = result['message'];
          setTimeout(() => {
            this._router.navigate(['/login']);
          }, 2000);
        }
      });
  }
}

Страница входа и регистрации

Как только пользователь зарегистрировался и вошел в систему с логином и паролем, ему будет предоставлена возможность включения и отключения двухфакторной аутентификации в HomeComponent.

Компонент Home

Позволят пользователю настраивать и проверять TFA. Как только пользователь попадет на эту страницу, он сможет отсканировать QR-код в приложении Google Authenticator. После сканирования T-OTP (элемент TFA), связанный с userObject, будет включен в приложение Google Authenticator. AuthCode будет отображаться в приложении на временной основе. Тот же код необходим для проверки и включения TFA для userObject.

<div class="container">
  <div class="card card-container">
    <div *ngIf="this.tfa.secret">

      <h5 style="border-bottom: 1px solid #a8a8a8; padding-bottom: 5px;">Current Settings</h5>

      <img [src]="tfa.dataURL" alt="" class="img-thumbnail" style="display:block;margin:auto">

      <p>Secret Key - {{tfa.secret || tfa.tempSecret}}</p>

      <p>Auth Type - Time Based - OTP</p>

      <button class="btn btn-lg btn-danger btn-block btn-signin" (click)="disabledTfa()">Disable TFA</button>
    </div>
    <div *ngIf="!tfa.secret">

      <h5 style="border-bottom: 1px solid #a8a8a8; padding-bottom: 5px;">Setup TFA</h5>

      <span *ngIf="!!tfa.tempSecret">

        <p>Scan the QR code or enter the secret key in Google Authenticator</p>

        <img [src]="tfa.dataURL" alt="" class="img-thumbnail" style="display:block;margin:auto">

        <p>Secret Key - {{tfa.tempSecret}}</p>

        <p>Auth Type - Time Based - OTP</p>

        <form class="form-group" (ngSubmit)="confirm()" #otpForm="ngForm">
          <input name="authcode" type="number" #iauthcode="ngModel" class="form-control" maxlength="6"
            placeholder="Enter the Auth Code" id="authcode" autocomplete="off" [(ngModel)]="authcode" required>
          <br>
          <button type="Submit" class="btn btn-lg btn-primary btn-block btn-signin"
            [disabled]="iauthcode?.errors?.required">Enable TFA</button>
        </form>
        <p class="text-danger" style="text-align:center;" *ngIf="errorMessage">{{errorMessage}}</p>
      </span>
    </div>
  </div>
</div>

Если пользователь включил TFA, то будут отображены текущие настройки с QR-кодом и секретным ключом. А также опция отключения TFA, связанного с userObject.

import { Component, OnInit } from '@angular/core';
import { LoginServiceService } from 'src/app/services/login-service/login-service.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

  tfa: any = {};
  authcode: string = "";
  errorMessage: string = null;

  constructor(private _loginService: LoginServiceService) {
    this.getAuthDetails();
  }

  ngOnInit() {
  }

  getAuthDetails() {
    this._loginService.getAuth().subscribe((data) => {
      const result = data.body
      if (data['status'] === 200) {
        console.log(result);
        if (result == null) {
          this.setup();
        } else {
          this.tfa = result;
        }
      }
    });
  }

  setup() {
    this._loginService.setupAuth().subscribe((data) => {
      const result = data.body
      if (data['status'] === 200) {
        console.log(result);
        this.tfa = result;
      }
    });
  }

  confirm() {
    this._loginService.verifyAuth(this.authcode).subscribe((data) => {
      const result = data.body
      if (result['status'] === 200) {
        console.log(result);
        this.errorMessage = null;
        this.tfa.secret = this.tfa.tempSecret;
        this.tfa.tempSecret = "";
      } else {
        this.errorMessage = result['message'];
      }
    });
  }

  disabledTfa() {
    this._loginService.deleteAuth().subscribe((data) => {
      const result = data.body
      if (data['status'] === 200) {
        console.log(result);
        this.authcode = "";
        this.getAuthDetails();
      }
    });
  }

}

Установка и текущие настройки TFA

Теперь вы знаете, как легко интегрировать двухфакторную аутентификацию в приложение, созданное на основе Angular 7.

Данная публикация представляет собой перевод статьи «Create an Angular 7 + Google Authenticator + Node JS Web App with Two-Factor Authentication» , подготовленной дружной командой проекта Интернет-технологии.ру