Написание провайдера
Как добавить способ аутентификации в виде 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Когда игрок заходит, первыми пробуются провайдеры с более высоким приоритетом.
Написание аддона
Как не пропускать чат и команды, пока игрок не прошёл аутентификацию, читать сообщения Identica и следить за перезагрузками.
Сообщения игрокам
Как определить сообщения, которые ваш провайдер или функция показывает игрокам, и показывать любое сообщение не только в чате, например диалогом или в строке действий.
