Создание веб-приложения с двухфакторной аутентификацией на 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» , подготовленная редакцией проекта.

Меню