Authentication
Use repeatable offline players or an explicitly configured private online account for local tests.
Offline players are the default for automated plugin tests. Use an online account only when a local journey specifically depends on authenticated Minecraft identity. Keep real account sign-in out of CI.
Add a local account
Use the installed MCProtocolLib library, whose ID is
mcprotocol. Accounts are signed in through one library: the engine's
anvil { engine { protocolLibrary("mcprotocol") } }, or the sole installed library that offers
authentication.
Use the Anvil IntelliJ panel's Accounts action to authenticate. Follow the library's device-code
instructions in your browser; Anvil retrieves the Minecraft username and UUID and stores the account
file under the configured account directory. You can also import an account file that the library
generated elsewhere. MCProtocolLib's library stores one account per JSON file and restricts files to
the owner on filesystems supporting POSIX permissions.
Without IntelliJ IDEA, sign in from the project that applies the Anvil plugin:
./gradlew anvilLogin --account=mainThe task runs the same library workflow in a separate JVM and prints the device-code instructions.
It stores the account in the directory configured by anvil { engine { accountsDirectory } }, which
defaults to ~/.anvil/accounts. Remove an account with ./gradlew anvilLogout --account=main. Sign in
on developer machines only; the stored file holds refresh tokens.
When a scenario references an account that is not stored, the run fails with the account ID and the directory that was searched.
The global account directory defaults to ~/.anvil/accounts. Configure another directory in
Settings → Tools → Anvil → Accounts. Project-specific accounts and pools are managed from the
Accounts action in the Anvil tool window; each project's directory defaults to a folder under
~/.anvil/projects, outside the project tree, so credentials are not committed with the source.
Create and connect an online player
Configure .onlineMode(true) on the direct server or entry proxy being tested. Keep forwarded
backends configured through Anvil's forwarding negotiation.
Place this helper at src/anvil/java/com/example/test/AuthenticatedPlayers.java. It assumes the scenario
entrypoint accepts online authentication and the Session capability is installed:
package com.example.test;
import me.whereareiam.anvil.api.model.player.PlayerLogin;
import me.whereareiam.anvil.api.model.player.PlayerOptions;
import me.whereareiam.anvil.api.player.SimulatedPlayer;
import me.whereareiam.anvil.api.scenario.ScenarioContext;
import me.whereareiam.anvil.api.type.AuthenticationMode;
import me.whereareiam.anvil.capability.session.Session;
public final class AuthenticatedPlayers {
public static SimulatedPlayer connect(ScenarioContext anvil) {
var player = anvil.players().create(PlayerOptions.builder()
.name("OnlinePlayer")
.login(PlayerLogin.account(AuthenticationMode.ONLINE, "main"))
.build());
var session = player.capability(Session.class);
session.connect();
session.connected();
return player;
}
}PlayerLogin groups how a player logs in, and each factory creates one valid combination:
| Factory | Login |
|---|---|
PlayerLogin.offline() | Offline, as the player's name; the default |
PlayerLogin.offline(username) | Offline, as another username that several players may share |
PlayerLogin.account(mode, accountId) | A stored account; its profile supplies the username |
PlayerLogin.session(mode, identity) | An identity verified by a session server the scenario chose |
PlayerLogin.leased(mode) | The account of a lease passed to create(options, lease) |
The account modes are ONLINE and ON_REQUEST; the account factories refuse OFFLINE, and every factory
refuses a blank value. A login therefore never carries a username next to an account, or an account ID next
to a session identity.
The account must be stored by the protocol library selected for the player, and that library's release for the player's Minecraft version must support online authentication; otherwise creation fails before the player connects.
Call AuthenticatedPlayers.connect(anvil) from a local test in the same package and run that class with
./gradlew anvilTest --tests 'your.package.YourOnlineTest'. A connected session establishes that
the account authenticated at the target. Ask the target's Players capability
for the profile the platform sees; the authenticated identity is controlled by the account.
Authenticate on an offline-mode entrypoint
Some plugins keep the proxy or server in offline mode and require online authentication themselves for
selected connections, such as login plugins that recognize premium usernames. AuthenticationMode.ONLINE
refuses an offline-mode entrypoint. Use AuthenticationMode.ON_REQUEST for such a player: it signs in with
its account and authenticates when the entrypoint asks for it.
var player = anvil.players().create(PlayerOptions.builder()
.name("OnlinePlayer")
.login(PlayerLogin.account(AuthenticationMode.ON_REQUEST, "main"))
.build());A connection that the entrypoint does not challenge joins unauthenticated, so assert what the plugin and the platform report rather than relying on the mode.
For several real accounts, create a pool from local account IDs. A leased or directly selected account is exclusive across every scenario of the engine until its player is destroyed or the lease is closed:
var testers = anvil.accounts().pool(java.util.List.of("alice", "bob", "charlie"));
var first = testers.lease();
var second = testers.lease();
var one = anvil.players().create("player-1", first);
var two = anvil.players().create("player-2", second);
// The two players use different authenticated accounts. Player cleanup releases the leases.create(name, lease) signs in with ONLINE. When the player needs more than a name, for example
ON_REQUEST or a connection, declare a leased login; the lease supplies its account:
var premium = anvil.players().create(PlayerOptions.builder()
.name("premium")
.login(PlayerLogin.leased(AuthenticationMode.ON_REQUEST))
.build(), testers.lease());A leased login is refused without a lease, and a lease is refused, without being claimed, for any other login.
Account IDs belong to the protocol library that stores them. A pool holds one account per ID and refuses
an ID that several libraries store, so a lease always names the account its player signs in with. A player
created from a lease uses that account's library, whatever protocolLibrary the scenario or engine selects.
Creation fails before the player connects when that library cannot serve the player's Minecraft version, and
the lease returns to the pool.
A named pool keeps account IDs out of scenario code, so each machine can supply its own accounts.
Declare it in pools.properties in the account directory. The IntelliJ account manager writes this
file; on other machines, create it by hand:
schemaVersion=1
pool.testers=alice,bob,charlieEach pool.<name> entry lists distinct account IDs separated by commas; leases follow that order.
Resolve the pool by name with anvil.accounts().pool("testers"). An undeclared pool, a repeated
account ID, or another schemaVersion fails the lookup.
Sign in through another session server
An online-mode login involves a session server twice: the client reports the login to it, and the proxy or
server asks it to verify the login. By default that server is Mojang's and the player signs in with a stored
account. Both sides can name another server that speaks the same protocol, Yggdrasil, such as a self-hosted
one. The contract is its base address with two endpoints: POST <base>/join from the client and
GET <base>/hasJoined?username=...&serverId=... from the process.
Declare the address on the process that verifies logins, and give the player an identity that server accepts:
URI sessionServer = URI.create("http://127.0.0.1:25580/session/minecraft");
MinecraftProxy proxy = MinecraftProxy.builder()
.name("proxy")
.platform(Platforms.VELOCITY)
.distribution(Distribution.remote("3.5.1", "615"))
.onlineMode(true)
.sessionServer(sessionServer)
.server("server")
.build();var player = anvil.players().create(PlayerOptions.builder()
.name("Alice")
.login(PlayerLogin.session(AuthenticationMode.ONLINE, SessionIdentity.builder()
.username("Alice")
.uniqueId(aliceId)
.accessToken(aliceToken)
.sessionServer(sessionServer)
.build()))
.build());A session identity replaces the account ID and works with ONLINE and ON_REQUEST. Anvil refuses the
player before it connects unless the process it joins declares the same session server, because any other
server would not verify it. Only Velocity can be redirected today: planning refuses sessionServer on any
other platform, and BungeeCord has no setting for it.
Test without a real account
The optional yggdrasil-mock artifact is a test-only session server, so online-mode behavior can be tested
without a real account, and in CI:
dependencies {
add("anvilImplementation", anvil.module("yggdrasil-mock"))
}Start it before declaring the scenario, pass yggdrasil.sessionServer() to the process, and register the
accounts that exist. Registration returns the identity a player signs in with:
YggdrasilMock yggdrasil = YggdrasilMock.start();
SessionIdentity alice = yggdrasil.register("Alice");A username that is not registered fails verification, as an unpaid account does. The mock also answers the
profile lookup that login plugins use to recognize paid usernames, at yggdrasil.profileLookup() followed by
the username: status 200 for a registered username and 404 otherwise. Usernames match whatever their letter
case, as at Mojang.
A name change keeps the account's UUID and token. rename returns the identity that signs in under the new
name; from then on the lookup and login verification answer to the new name only, and the old one is free:
SessionIdentity alicia = yggdrasil.rename(alice.getUniqueId(), "Alicia");Renaming to a name another account holds fails; a change of letter case alone is allowed.
yggdrasil.available(false) makes every request fail with status 503, as during an outage. Close the mock when the tests that use it are done.
- The mock runs in the JVM that runs the tests and listens on loopback. A process in a container reaches it
only when you start it with
YggdrasilMock.start(address)on an address the container can route to. - It signs nothing and serves no skins, so behavior that depends on signed profile properties is not covered.
Keep the account store private
Treat the authentication directory as credentials. Exclude it from source control, diagnostic uploads, and shared cache archives. Access and refresh tokens do not belong in Gradle inputs, CLI arguments, environment variables, system properties, or scenario files. MCProtocol sends the access token to its worker through private stdin. Agent session credentials are separate per-run values.
Authentication is an optional service of a protocol library. A library without that service can still support offline players; selecting it does not enable MCProtocolLib's account workflow.