Writing a provider
Add a way to authenticate as a provider jar that Identica loads from its providers directory.
A provider is one way for players to authenticate, such as a password or a premium account. Identica loads each provider from its own jar in plugins/identica/providers. The provider decides whether a player may continue; Identica creates the account, links it to the provider, opens the session and routes the player.
The repository's examples/provider is a complete provider: a player registers and logs in by typing a passphrase the administrator chose. This page walks through it. Its live tests load it on a real proxy, so it follows the current API.
The jar
The jar holds a provider.json at its root:
{
"id": "example",
"name": "Example",
"version": "1.0.0",
"main": "me.whereareiam.identica.example.provider.ExampleProvider",
"supportedPlatforms": ["VELOCITY", "BUNGEECORD"]
}id, name, version and main are required. The provider's files go to plugins/identica/providers/<name>. Compile against the API with compileOnly, and bundle and relocate any library Identica does not provide, as the official providers do.
The provider class
main extends me.whereareiam.identica.provider.IdenticaProvider. Identica creates it twice: once through a constructor without parameters to read what it declares, then through the provider's own Guice injector with the services it uses:
@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();
}
}The module binds the provider's documents and a SubjectResolver. The subject names a player's account at this provider, and Identica recognises a returning player by it. ExampleMessageDefinitions defines what the provider tells players; see Messages to players.
Waiting for the player
A step in the provider's stage of registration and authentication waits until the player has typed the passphrase. It reads the attempt from the state of the pipeline run, keeps waiting with the passphrase's message while there is none, and completes the stage when the passphrase is right:
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));
}A step runs as early as it can, while the player still logs in, so the first run of this step finds no attempt and asks. The journey then sends the player to the step's server and runs the step again there, so the prompt reaches the player, and again whenever the player answers. A step never declares whether it talks to the player: what it returns says so on each run. A step that only decides from the login and stored data, such as recognising a returning player, finishes before the proxy picks the player's first server.
Both messages carry the prompt example:passphrase, which names the field the player fills and the command that answers it. Identica shows them in chat; their IDs, example:passphrase and example:passphrase-invalid, let an addon show them another way, such as a dialog.
A waiting journey continues at the step that waits. When the player's next answer belongs to an earlier step of the same stage, such as a new password after its repetition did not match, return StepResult.waitingAt(step, message) with that step's name: the journey continues there once the player answers. A name the stage does not run before the waiting step keeps the journey where it waits.
The command hands the attempt to the pending journey with ConnectionCoordinator.advance(...) and shows the answer when the journey keeps waiting. It does not look for the pending journey itself: the pipeline does that in the connection's turn, after every earlier run of the connection has finished, so a passphrase typed before its prompt waits for the run that shows the prompt. When nothing is pending, the decision is NO_PENDING and the command shows its own message. An input that only fits some pending journeys overrides PipelineInput.accepts(...); a declined input ends the same way. Mark the command with @AvailableWhilePending, so addons that hold back commands until a player has authenticated let it through.
Methods
A method is one way your provider offers to get a scenario done. Identica does not know what a method does: it offers the methods of every eligible provider as options, picks one or lets the player pick (the scenario's choice), and runs its steps. The example provider offers one method, the passphrase, for registration and authentication:
@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());
}A provider with several ways registers one method per way, in the order it prefers them, and gives each a label for the player's choice. A method that only fits some players sets an availability, which Identica asks before offering or trying it:
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());- Steps a provider registers with
builder.registerStep(...)and no method, such as a validation step, run in each of its methods, ordered with the method's own steps by theirorder(). - A provider that registers no method for a scenario runs all its steps there as one option.
- A step that fails when its method cannot get the scenario done lets the journey try the next option: the next method of the same provider, then the next provider. A denial ends the journey.
- The method that runs is
ScenarioContext.getProvider().getMethod(), andProviderSelectedEventnames it. - Admins may leave out or reorder methods per scenario with
methodsin your provider's entry; document your method names for them. - The player picks a method as
<provider>:<method>, such as/identica enroll website:guided. A provider with one option is picked by its ID alone.
Claiming connections
Identica picks the provider of a join by ranking reasons, called claims: a pending migration, an account link of a verified subject, a hostname, a verified subject without an account, the player's choice, a matching name and discovery, strongest first. Its own claim sources already cover your provider: a subject your SubjectResolver resolves is discovery, or a verified subject when you mark it verified. When your provider knows more, such as an identity the client proved some other way, contribute a ProviderClaimSource from the provider's module:
Multibinder.newSetBinder(binder(), ProviderClaimSource.class)
.addBinding()
.to(ExampleClaimSource.class);A source returns claims for the connection in a ProviderClaimRequest, which carries its identity, the preparation stage, the subjects the providers resolved and the selection so far. It never decides the winner: Identica ranks every claim together. Code that adds a claim later, such as a command, goes through ProviderSelector.claim(...), which ranks again instead of replacing the winner. The pick of a connection is ScenarioContext.getSelection(), and its origin is always the winning claim's.
Moving accounts
A player can move their account to or from your provider. The rule that a move never lowers protection names no provider, so your provider takes part through its SubjectResolver:
- Return
SubjectResolution.builder()...verified(true)only when your provider or the platform authenticated the subject for this connection, before the profile is resolved. The UUID of an online login is an example. - Leave
verifiedunset for a subject you derive from what the client claims, such as its username, even when one of your steps checks a secret afterwards. Being seen with such a subject proves nothing, so it never continues a move. - Keep demanding your login in your handshake policy while a move is pending. Identica does not relax it for you.
A provider that proves ownership only in a step, such as a password prompt, cannot prove it again on the join after the confirmation. An account leaves such a provider only for a provider that verifies its own logins.
Configuration
Each document is a ConfigProvider with the provider's working directory, injected as @Named("workingPath") Path. It writes its defaults on first start and reloads with /identica admin reload. A messages document implements MessageDocument, so addons find it through identica.getConfigCatalog().
Enabling it
Identica loads a provider only when providers/providers.yml has an entry for it with enabled: true:
providers:
- id: "example"
enabled: true
priority: 75Providers with a higher priority are tried first when a player joins.
