Identica
Расширение

Написание провайдера

Как добавить способ аутентификации в виде jar-файла провайдера, который Identica загружает из своей папки провайдеров.

Провайдер — это один из способов аутентификации игроков, например пароль или лицензионный аккаунт. Identica загружает каждый провайдер из отдельного jar-файла в plugins/identica/providers. Провайдер решает, может ли игрок продолжить; Identica создаёт аккаунт, привязывает его к провайдеру, открывает сессию и направляет игрока дальше.

В репозитории есть examples/provider — полноценный провайдер: игрок регистрируется и входит, вводя кодовую фразу, которую выбрал администратор. На этой странице он разобран по шагам. Его live-тесты загружают его на настоящем прокси, поэтому он соответствует текущему API.

Jar-файл

В корне jar-файла лежит provider.json:

{
  "id": "example",
  "name": "Example",
  "version": "1.0.0",
  "main": "me.whereareiam.identica.example.provider.ExampleProvider",
  "supportedPlatforms": ["VELOCITY", "BUNGEECORD"]
}

id, name, version и main обязательны. Файлы провайдера размещаются в plugins/identica/providers/<name>. Компилируйте провайдер против API с compileOnly, а любые библиотеки, которых Identica не предоставляет, встраивайте в jar-файл и релоцируйте — так делают официальные провайдеры.

Класс провайдера

Класс из main наследует me.whereareiam.identica.provider.IdenticaProvider. Identica создаёт его дважды: сначала через конструктор без параметров, чтобы прочитать то, что он объявляет, а затем через собственный Guice-инжектор провайдера — с сервисами, которые он использует:

@NoArgsConstructor
@AllArgsConstructor(onConstructor_ = @Inject)
public class ExampleProvider extends IdenticaProvider {
    private CommandService commandService;
    private PipelineExtensionRegistry pipelineExtensionRegistry;
    private ExamplePipelineExtension pipelineExtension;
    private ExamplePassphraseCommand passphraseCommand;
    private MessageService messageService;

    private final List<MessageService.Registration> messageRegistrations = new ArrayList<>();

    @Override
    public List<Module> modules() {
        return List.of(new ExampleModule());
    }

    @Override
    public void onEnable() {
        messageRegistrations.add(messageService.register(ExampleMessageDefinitions.all()));
        pipelineExtensionRegistry.register(pipelineExtension);
        commandService.registerCommandInstances(Map.of("example-passphrase", passphraseDefinition()), passphraseCommand);
    }

    @Override
    public void onDisable() {
        commandService.unregisterCommands(Set.of("example-passphrase"));
        pipelineExtensionRegistry.unregister(pipelineExtension.id());
        messageRegistrations.forEach(MessageService.Registration::remove);
        messageRegistrations.clear();
    }
}

Модуль связывает документы провайдера и SubjectResolver. Субъект обозначает аккаунт игрока у этого провайдера, и по нему Identica узнаёт вернувшегося игрока. ExampleMessageDefinitions определяет, что провайдер сообщает игрокам; см. Сообщения игрокам.

Ожидание ввода игрока

Шаг в этапе провайдера при регистрации и аутентификации ждёт, пока игрок введёт кодовую фразу. Он читает попытку из состояния текущего прохода конвейера, продолжает ждать с сообщением о кодовой фразе, пока попытки нет, и завершает этап, если фраза верна:

public CompletableFuture<StepResult> execute(ScenarioContext context, PipelineState state) {
    ExamplePassphraseAttempt attempt = state.get(ExamplePassphraseAttempt.KEY).orElse(null);
    if (attempt == null)
        return CompletableFuture.completedFuture(StepResult.waiting(ExampleMessageDefinitions.PASSPHRASE.create(messages.get())));

    state.remove(ExamplePassphraseAttempt.KEY);
    if (!settings.get().getPassphrase().equals(attempt.getPassphrase()))
        return CompletableFuture.completedFuture(StepResult.waiting(ExampleMessageDefinitions.PASSPHRASE_INVALID.create(messages.get())));

    return CompletableFuture.completedFuture(StepResult.complete(context));
}

Шаг выполняется как можно раньше, пока игрок ещё входит, поэтому при первом запуске этот шаг не находит попытки и задаёт вопрос. Затем прохождение отправляет игрока на сервер шага и запускает шаг там снова, чтобы запрос дошёл до игрока, а потом — каждый раз, когда игрок отвечает. Шаг никогда не объявляет заранее, общается ли он с игроком: об этом при каждом запуске говорит то, что он возвращает. Шаг, который решает только по данным входа и сохранённым данным, например распознаёт вернувшегося игрока, завершается до того, как прокси выберет первый сервер игрока.

Оба сообщения несут запрос example:passphrase, который называет поле, заполняемое игроком, и команду, которая на него отвечает. Identica показывает их в чате; их идентификаторы, example:passphrase и example:passphrase-invalid, позволяют аддону показывать их по-другому, например диалогом.

Ожидающее прохождение продолжается с того шага, который ждёт. Если следующий ответ игрока относится к более раннему шагу того же этапа, например новый пароль после того, как повтор не совпал, верните StepResult.waitingAt(step, message) с именем этого шага: прохождение продолжится с него, когда игрок ответит. Если этап не выполняет шаг с таким именем раньше ожидающего, прохождение остаётся на ожидающем шаге.

Команда передаёт попытку в ожидающее прохождение через ConnectionCoordinator.advance(...) и показывает ответ, если прохождение продолжает ждать. Сама она ожидающее прохождение не ищет: это делает конвейер в очереди соединения, после того как завершились все более ранние запуски этого соединения, поэтому кодовая фраза, введённая до подсказки, дожидается запуска, который эту подсказку показывает. Если ничего не ожидается, решение — NO_PENDING, и команда показывает своё сообщение. Ввод, который подходит не каждому ожидающему прохождению, переопределяет PipelineInput.accepts(...); отклонённый ввод завершается так же. Пометьте команду аннотацией @AvailableWhilePending, чтобы аддоны, которые не пропускают команды до аутентификации игрока, пропускали её.

Способы

Способ — это один из путей, которыми ваш провайдер предлагает пройти сценарий. Identica не знает, что делает способ: она предлагает способы всех подходящих провайдеров как варианты, выбирает один сама или даёт выбрать игроку (настройка choice сценария) и выполняет его шаги. Пример провайдера предлагает один способ — кодовую фразу — для регистрации и аутентификации:

@Override
public void apply(PipelineExtensionBuilder builder) {
    for (PipelineType scenario : List.of(PipelineType.AUTHENTICATION, PipelineType.REGISTRATION))
        builder.registerMethod(ProviderMethod.builder()
                .providerId(ExampleProvider.ID)
                .name("passphrase")
                .scenario(scenario)
                .step(passphraseStep)
                .build());
}

Провайдер с несколькими путями регистрирует по одному способу на каждый путь в том порядке, в котором их предпочитает, и даёт каждому label для выбора игроком. Способ, который подходит только некоторым игрокам, задаёт availability — Identica проверяет её, прежде чем предложить или попробовать этот способ:

builder.registerMethod(ProviderMethod.builder()
        .providerId("website")
        .name("quick")
        .scenario(PipelineType.MIGRATION)
        .availability(context -> accounts.exists(context.getUsername()))
        .step(lookupStep)
        .build());
builder.registerMethod(ProviderMethod.builder()
        .providerId("website")
        .name("guided")
        .scenario(PipelineType.MIGRATION)
        .label(() -> messages.get().getGuidedLabel())
        .step(lookupStep)
        .step(setupStep)
        .build());
  • Шаги, которые провайдер регистрирует через builder.registerStep(...) без способа, например шаг проверки, выполняются в каждом его способе и упорядочиваются вместе с собственными шагами способа по их order().
  • Провайдер, который не регистрирует для сценария ни одного способа, выполняет там все свои шаги как один вариант.
  • Шаг, который завершается неудачей, когда его способ не может пройти сценарий, позволяет прохождению попробовать следующий вариант: следующий способ того же провайдера, затем следующего провайдера. Отказ завершает прохождение.
  • Выполняемый способ — это ScenarioContext.getProvider().getMethod(), и ProviderSelectedEvent называет его.
  • Администраторы могут убирать или переставлять способы для каждого сценария через methods в записи вашего провайдера; опишите для них названия своих способов.
  • Игрок выбирает способ как <provider>:<method>, например /identica enroll website:guided. Провайдер с одним вариантом выбирается просто по его ID.

Заявки на подключения

Identica выбирает провайдера для входа, ранжируя причины — заявки: ожидающая миграция, привязка аккаунта к подтверждённому субъекту, имя хоста, подтверждённый субъект без аккаунта, выбор игрока, совпадение ника и обнаружение, от самой сильной к самой слабой. Собственные источники заявок Identica уже покрывают ваш провайдер: субъект, который находит ваш SubjectResolver, — это обнаружение, а если вы пометите его verified — подтверждённый субъект. Если ваш провайдер знает больше, например личность, которую клиент доказал другим способом, добавьте ProviderClaimSource в модуле провайдера:

Multibinder.newSetBinder(binder(), ProviderClaimSource.class)
        .addBinding()
        .to(ExampleClaimSource.class);

Источник возвращает заявки для подключения из ProviderClaimRequest, где есть его личность, этап подготовки, субъекты, найденные провайдерами, и выбор на данный момент. Источник никогда не решает, кто победит: Identica ранжирует все заявки вместе. Код, который добавляет заявку позже, например команда, использует ProviderSelector.claim(...): заявки ранжируются заново, а победитель не перезаписывается. Выбор для подключения — это ScenarioContext.getSelection(), и его происхождение всегда совпадает с происхождением победившей заявки.

Перенос аккаунтов

Игрок может перенести свой аккаунт на ваш провайдер или с него. Правило, по которому перенос никогда не снижает защиту, не называет ни одного провайдера, поэтому ваш провайдер участвует в нём через свой SubjectResolver:

  • Возвращайте SubjectResolution.builder()...verified(true), только если ваш провайдер или платформа аутентифицировали субъект для этого подключения ещё до того, как найден профиль. Пример — UUID онлайн-входа.
  • Не задавайте verified для субъекта, который вы выводите из того, что заявляет клиент, например из его ника, даже если один из ваших шагов потом проверяет секрет. То, что игрок пришёл с таким субъектом, ничего не доказывает, поэтому перенос по нему никогда не продолжается.
  • Пока перенос ожидает завершения, продолжайте требовать свой вход в политике рукопожатия. Identica не ослабляет её за вас.

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

Конфигурация

Каждый документ — это ConfigProvider с рабочей папкой провайдера, которая внедряется как @Named("workingPath") Path. При первом запуске документ записывает значения по умолчанию и перезагружается по /identica admin reload. Документ сообщений реализует MessageDocument, поэтому аддоны находят его через identica.getConfigCatalog().

Включение провайдера

Identica загружает провайдер, только если в providers/providers.yml для него есть запись с enabled: true:

providers:
  - id: "example"
    enabled: true
    priority: 75

Когда игрок заходит, первыми пробуются провайдеры с более высоким приоритетом.

На этой странице