Перейти к содержанию

Создание типа двухфакторной аутентификации

Тип двухфакторной аутентификации используется для настройки двухфакторной аутентификации (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, описанные выше. Убедитесь, что после прохождения первого фактора аутентификации (корректные логин/пароль и др.) отображается окно для подтверждения кодового слова с корректной проверкой.

После введения верного кодового слова будет осуществлён успешный вход в систему.

Back to top