Создание типа двухфакторной аутентификации¶
Тип двухфакторной аутентификации используется для настройки двухфакторной аутентификации (2FA) и предоставляет дополнительный уровень безопасности при входе в систему. Он определяет, какие проверки выполняются системой после выполнения первого фактора аутентификации пользователя - пользователь корректно указал свои учётные данные, например, логин/пароль или логин/Kerberos и т.д.
Доступные по умолчанию типы 2FA перечислены в классе TwoFactorAuthDefaultTypes
. В этом разделе рассмотрено, как добавить новый тип и определить для него логику проверки второго фактора для успешной аутентификации.
В примере реализован тип 2FA “Кодовое слово” с возможностью его настройки в диалоге настроек типов 2FA. При использовании данного типа 2FA, после успешного прохождения первого фактора аутентификации необходимо будет ввести кодовое слово, заданное в настройках пользователя для успешного входа в систему.
Схема данных¶
Создайте библиотеку схемы AbSolution
, в которой будут содержаться все изменения в схеме, связанные с проектом. Здесь Ab
- это короткий префикс проектного решения, который также будет использован в именах типов далее в этом примере. Для вашего решения укажите любой свой префикс.
Все доступные типы 2FA должны быть указаны в таблице-перечислении TwoFactorAuthTypes
. Откроем эту таблицу и добавим в неё строку со значениями в колонках:
-
идентификатор
47a35c38-8ccd-4d86-b3f6-1a3fd8ee436b
, -
имя
Кодовое слово
, -
библиотека
AbSolution
.
Tip
Рекомендуется указывать имя для типа 2FA строкой локализации вида $Enum_TwoFactorAuthTypes_CodeWord
, которую надо добавить в проектную библиотеку локализации. Это позволит локализовать на разные языки значение, отображаемое в диалоге настроек 2FA для сотрудника, а также переименовывать его без изменения схемы и необходимости обновления в колонках таблиц, которые используют тип 2FA в качестве ссылочного значения.
Сохраните схему данных.
Тип диалога¶
Чтобы добавить настройку кодового слова для нового типа 2FA, необходимо создать тип диалога с именем TwoFactorAuthCodeWordSettings
в группе TwoFactorAuth
.
В виртуальной схеме добавьте строковую таблицу TwoFactorAuthCodeWordSettings
со строковым полем CodeWord
.
Добавьте вкладку TwoFactorAuthCodeWordSettings
с блоком Основная информация
, контролом-строка Кодовое слово
с настройками, как указаны ниже.
Сохраните тип диалога.
Серверные расширения¶
В проекте Tessa.Extensions.Server
создайте подпапку Login
, в которую добавьте класс AbTwoFactorAuthTypes
со статическим полем, которое описывает тип 2FA с таким же идентификатором и именем, как и в схеме:
using Tessa.Platform.Runtime;
namespace Tessa.Extensions.Server.Login
{
public static class AbTwoFactorAuthTypes
{
public static readonly TwoFactorAuthType CodeWord = new(
new(0x47a35c38, 0x8ccd, 0x4d86, 0xb3, 0xf6, 0x1a, 0x3f, 0xd8, 0xee, 0x43, 0x6b),
"Кодовое слово");
}
}
В той же подпапке создайте класс AbTwoFactorAuthCodeWordHandler
серверного обработчика для нового типа 2FA:
using System;
using System.Threading;
using System.Threading.Tasks;
using Tessa.Platform;
using Tessa.Platform.Runtime;
using Tessa.Platform.Storage;
using Tessa.Platform.Validation;
namespace Tessa.Extensions.Server.Login
{
public sealed class AbTwoFactorAuthCodeWordHandler :
ITwoFactorAuthHandler
{
public TimeSpan AttemptTimeout { get; } = TimeSpan.Zero;
public async Task HandleStartAsync(TwoFactorAuthContext context, CancellationToken cancellationToken = default)
{
ThrowIfNull(context);
// Проверка наличия кодового слова в настройках пользователя
var codeWord = context.Settings.TryGet<string?>("CodeWord");
if (string.IsNullOrEmpty(codeWord))
{
context.ValidationResult.AddError(this, "Отсутствуют настройки для данного типа двухфакторной аутентификации");
return;
}
// Добавление сообщения, выводимого в форме входа в систему для выбранного способа 2FA
context.Response["Message"] = "Введите кодовое слово";
}
public Task HandleCommandAsync(TwoFactorAuthContext context, CancellationToken cancellationToken = default)
{
// Здесь может располагаться дополнительная логика для обработки команды, полученной от клиента.
// Например, повторная генерация кода подтверждения.
return Task.CompletedTask;
}
public async Task HandleCheckAsync(TwoFactorAuthContext context, CancellationToken cancellationToken = default)
{
ThrowIfNull(context);
// Проверка наличия кодового слова в настройках пользователя
var codeWord = context.Settings.TryGet<string?>("CodeWord");
if (string.IsNullOrEmpty(codeWord))
{
context.ValidationResult.AddError(this, "Отсутствуют настройки для данного типа двухфакторной аутентификации");
return;
}
// Получение кодового слова, которое было введено клиентом и его проверка
if (codeWord != context.Request.Get<string>("Code"))
{
context.ValidationResult.AddError(this, "Введено неверное кодовое слово");
return;
}
}
}
}
Обработчик может получать необходимые зависимости из Unity-контейнера. Используйте их, если необходимо реализовать дополнительную логику.
В контексте TwoFactorAuthContext
обработчика 2FA в свойстве Request
доступна дополнительная, сериализованная в виде хэш-таблицы, информация, переданная с клиента. Для отправки дополнительных данных на клиент (например, отображения информационного сообщения) используется свойство Response
. В свойстве Data
содержатся данные по текущей попытке входа, которые хранятся в Redis.
Зарегистрируйте в коде C# тип 2FA и его обработчик. Такая регистрация будет использоваться на сервере в веб-сервисе web
. Класс регистратора Registrator
добавьте в ту же подпапку:
using Tessa.Platform;
using Tessa.Platform.Runtime;
using Unity;
namespace Tessa.Extensions.Server.Login
{
[Registrator]
public sealed class Registrator : RegistratorBase
{
public override void RegisterUnity() => NotNullOrThrow(this.UnityContainer)
.RegisterSingleton<AbTwoFactorAuthCodeWordHandler>();
public override void FinalizeRegistration() => NotNullOrThrow(this.UnityContainer)
.TryResolve<ITwoFactorAuthContainer>()?
.RegisterHandler<AbTwoFactorAuthCodeWordHandler>(AbTwoFactorAuthTypes.CodeWord);
}
}
Tip
Иногда требуется модифицировать сохранённые настройки пользователя для типа 2FA при их отображении в диалоге настроек типов 2FA. Например, для типа 2FA с одноразовым кодом на основе времени (TOTP) выполняется генерация секретного ключа, а при следующем отображении диалога с настройками он скрывается. Скрытие происходит на сервере путём изменения данных настроек типа 2FA на серверной стороне, чтобы в ответе на клиент пришли уже модифицированные настройки. Если необходимо реализовать аналогичное поведение, то создайте конфигуратор для модификации настроек типа 2FA и зарегистрируйте его по аналогии с тем, как это сделано в классе Tessa.Extensions.Default.Server.Login.Registrator
.
Расширения web-клиента¶
В папке проектных расширений web-клиента solution
(в сборке она расположена в WebClient SDK/src/solution
) создайте подпапку login
. Добавьте в неё файл abTwoFactorAuthTypes.ts
с константой CodeWord
:
export namespace AbTwoFactorAuthTypes {
export const CodeWord = '47a35c38-8ccd-4d86-b3f6-1a3fd8ee436b';
}
В этой же папке добавим файлы abTwoFactorAuthCodeWordViewModel.ts
и abTwoFactorAuthCodeWordComponent.tsx
, которые содержат модель представления и React-компонент, отображаемый на форме входа в систему при использовании двухфакторной аутентификации с использованием кодового слова.
import { injectable } from '@tessa/application';
import { TwoFactorAuthViewModel } from 'tessa/ui/login/component/twoFactor/twoFactorAuthViewModel';
/** Модель представления компонента для двухфакторной аутентификации с использованием кодового слова. */
@injectable()
export class AbTwoFactorAuthCodeWordViewModel extends TwoFactorAuthViewModel {
public async initialize(): Promise<void> {
await super.initialize();
// В базовой модели уже существуют преднастроенные поля и кнопки, которые можно использовать в большинстве
// случаев для реализации логики необходимого типа 2FA. Поэтому, конфигурируем необходимыми компонентами.
this.fields.push(this.codeField);
this.buttons.push(this.checkButton, this.retryButton, this.changeButton, this.cancelButton);
}
}
import React from 'react';
import { observer } from 'mobx-react';
import { FieldType, StorageHelper } from '@tessa/core';
import { LoginFields, LoginButtons, LoginMessage, LoginHeader, LoginInfo } from 'tessa/ui/login/form/loginForm';
import { ILoginComponentProps } from 'tessa/ui/login/component/loginComponentViewModel';
import { AbTwoFactorAuthCodeWordViewModel } from './twoFactorAuthEmailViewModel';
/* Компонент для двухфакторной аутентификации с использованием кодового слова. */
export const AbTwoFactorAuthCodeWordComponent: React.FC<
ILoginComponentProps<AbTwoFactorAuthCodeWordViewModel>
> = observer(({ viewModel }) => {
const info = viewModel.result?.info;
// Информационное сообщение, которое было добавлено в серверном расширении в context.Response["Message"]
const messages = StorageHelper.tryGetValue(info, 'Message', FieldType.String)?.split('\n');
return (
<>
<LoginInfo>
<LoginHeader text={viewModel.header} />
{messages && <LoginMessage message={messages} />}
<LoginFields fields={viewModel.fields} />
<LoginMessage
message={viewModel.message}
localize={(alias, defaultValue) => viewModel.localize(alias, defaultValue)}
/>
</LoginInfo>
<LoginButtons buttons={viewModel.buttons} />
</>
);
});
В той же папке добавьте конфигуратор для управления настройками типа 2FA:
import { FieldStorageMap, FieldType, IStorage, StorageAccessor, TypedField } from '@tessa/core';
import { injectable } from '@tessa/application';
import { Card } from '@tessa/platform';
import { ICardModel, IFormViewModelBase } from 'tessa/ui/cards';
import { ITwoFactorAuthConfigurator } from 'tessa/defaultExtensions/platform/login/twoFactorAuthConfigurator';
@injectable()
export class AbTwoFactorAuthCodeWordConfigurator implements ITwoFactorAuthConfigurator {
private _fields: FieldStorageMap | null;
public settingsName: string | null = 'TwoFactorAuthCodeWordSettings';
public async setTypeSettings(card: Card, storage: IStorage | null): Promise<void> {
// Получение виртуальной секции из типа диалога
this._fields = card.sections.get('TwoFactorAuthCodeWordSettings')?.fields;
if (!this._fields) {
return;
}
// Получение кодового слова из сериализованных настроек пользователя
const codeWord = new StorageAccessor(storage ?? {}).tryGetString('CodeWord');
// Запись кодового слова в виртуальное поля карточки
this._fields.set('CodeWord', codeWord, FieldType.String);
}
public async modifySettingsModel(form: IFormViewModelBase, cardModel: ICardModel): Promise<void> {
// Получение контрола с кодовым словом
const control = cardModel.controls.get('CodeWord');
if (!control) {
return;
}
// Тут можно выполнить модификацию UI, подписаться на события изменения данных и др.
}
public async getTypeSettings(): Promise<IStorage | null> {
const codeWord = this._fields?.tryGetString('CodeWord');
return codeWord ? { ['CodeWord']: TypedField.createString(codeWord) } : null;
}
public async validateTypeSettings(): Promise<ValidationResult> {
return this._fields?.tryGetString('CodeWord')
? ValidationResult.empty
: ValidationResult.fromText('Необходимо указать кодовое слово', ValidationResultType.Error);
}
public dispose(): void {
this._fields = null;
}
}
Зарегистрируем конфигуратор и его расширение для компонента типа 2FA с использованием кодового слова. Создайте файл registrator.ts
в подпапке login
с содержимым:
import React from 'react';
import { ExtensionRegistrator } from '@tessa/application';
import { ComponentRegistry } from 'tessa/ui/cards/componentRegistry';
import { ILoginComponentViewModel$ } from 'tessa/ui/login/component/loginComponentViewModel';
import { ITwoFactorAuthConfigurator$ } from 'tessa/ui/login/component/twoFactor/twoFactorAuthConfigurator';
import { AbTwoFactorAuthTypes } from './abTwoFactorAuthTypes';
import { AbTwoFactorAuthCodeWordConfigurator } from './abTwoFactorAuthCodeWordConfigurator';
import { AbTwoFactorAuthCodeWordLoginComponent } from './abTwoFactorAuthCodeWordComponent';
import { AbTwoFactorAuthCodeWordViewModel } from './abTwoFactorAuthCodeWordViewModel';
export const AbLoginRegistrator: ExtensionRegistrator = {
async registerTypes(container) {
container
.bind(ILoginComponentViewModel$)
.to(AbTwoFactorAuthCodeWordViewModel)
.inRequestScope()
.whenTargetNamed(AbTwoFactorAuthTypes.CodeWord);
container
.bind(ITwoFactorAuthConfigurator$)
.to(AbTwoFactorAuthCodeWordConfigurator)
.inRequestScope()
.whenTargetNamed(AbTwoFactorAuthTypes.CodeWord);
ComponentRegistry.instance.register(
AbTwoFactorAuthTypes.CodeWord,
(viewModel: AbTwoFactorAuthCodeWordViewModel) => {
return React.createElement(AbTwoFactorAuthCodeWordComponent, { viewModel }, null);
}
);
}
};
Пропишите класс регистратора AbLoginRegistrator
в файле bundleRegistrator.ts
в папке solution
:
import { Application } from 'tessa/application';
import { AbLoginRegistrator } from './login/registrator';
Application.instance.registerBundle({
name: 'Tessa.Extensions.Solution.js',
buildTime: process.env.BUILD_TIME!,
registry: [
// add registrators here
AbLoginRegistrator
]
});
Проверка типа “Кодовое слов
Зайдите в систему от имени администратора и в карточке Настройки сервера на вкладке Безопасность установите для поля Режим двухфакторной двухфакторной аутентификации значение Включено, необязательно
или Включено, обязательно
, а в поле Типы двухфакторной аутентификации добавьте тип Кодовое слово
.
Для текущего сотрудника в диалоге Мои настройки или в карточке сотрудника (для другого сотрудника, которому необходимо настроить двухфакторную аутентификацию) откройте диалог с настройками типов 2FA по кнопке Настройки двухфакторной аутентификации. В таблице добавьте тип 2FA со значением Кодовое слово
и введите слово для его последующей проверки при входе в систему.
Сохраните настройки и выйдите из системы.
Зайдите под пользователем, для которого были произведены настройки типа 2FA, описанные выше. Убедитесь, что после прохождения первого фактора аутентификации (корректные логин/пароль и др.) отображается окно для подтверждения кодового слова с корректной проверкой.
После введения верного кодового слова будет осуществлён успешный вход в систему.