Anvil
Building blocksPlayers

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=main

The 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:

FactoryLogin
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,charlie

Each 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.

On this page