1. Project Overview
MinecraftAuth provides a Java library for authenticating Minecraft Java and Bedrock editions through Microsoft accounts. It simplifies login flows, token management, and basic Realms interactions behind a single, extensible API.
What MinecraftAuth Solves
- Centralizes Microsoft-account authentication for Minecraft clients and servers
- Manages OAuth2 device code, credential, and embedded-WebView flows
- Automates token refresh, serialization, and secure storage
- Offers helpers for listing and joining Realms worlds
Supported Editions
- Java Edition (official launcher compatibility)
- Bedrock Edition (Windows 10, consoles, mobile via XBL)
Typical Use Cases
- Custom launchers integrating Microsoft‐based login
- Server proxies handling upstream authentication
- Educational tools demonstrating OAuth2 and Minecraft APIs
- Build automation that needs pre-authenticated sessions
Key Features
Pluggable Login Flows
Choose built-in flows or implement your own:
MinecraftAuth auth = MinecraftAuth.builder()
.clientId("your-client-id")
.flow(MicrosoftAuthFlows.deviceCode()) // Or .credentials(username, password), .webview()
.build();
MicrosoftToken token = auth.login();
Token Refresh & Serialization
- Automatic token refresh on expiry
- Serialize/deserialize tokens for persistent login
// Save token to disk
Files.writeString(path, gson.toJson(token));
// Restore token
MicrosoftToken token = gson.fromJson(Files.readString(path), MicrosoftToken.class);
auth.setToken(token);
Realms Helper API
List and join Realms with minimal setup:
RealmsClient realms = auth.realms(clientVersion);
List<Realm> worlds = realms.listWorlds();
realms.joinWorld(worlds.get(0).id);
Logging Hooks
Plug in SLF4J or java.util.logging to trace HTTP requests, token events, and flow steps:
MinecraftAuth auth = MinecraftAuth.builder()
.logger(new Slf4jLogger())
.build();
Licensing and Obligations
MinecraftAuth is distributed under GNU Lesser General Public License v3 (LGPL-3). When you:
- Modify or distribute the library, include a copy of the LGPL-3 license
- Document changes and make source code available under the same license
- Link with proprietary code without relicensing your own modules (dynamic linking allowed)
Refer to LICENSE for full terms and ensure compliance when embedding or extending this library.
2. Installation & Quick Start
This section shows how to add MinecraftAuth to your project and authenticate a Minecraft session in under 20 lines of code using the default device-code flow. You’ll also see how to include the JavaFX-stub for headless environments.
Prerequisites
- Java 17 or higher
- Gradle 7.x (Groovy DSL) or Kotlin DSL
2.1 Gradle Setup
Maven Central
Add the library to your build.gradle
:
plugins {
id 'java'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
// Core library
implementation "com.raphimc:minecraft-auth:{{latest.version}}"
// Optional: JavaFX stub for headless environments
// This replaces the real JavaFX dependency so you can run on servers
implementation "com.raphimc:minecraft-auth:{{latest.version}}:javafx-stub"
}
Replace {{latest.version}}
with the current release (see Maven Central).
GitHub Packages (if not on Maven Central)
repositories {
maven {
url = uri("https://maven.pkg.github.com/RaphiMC/MinecraftAuth")
credentials {
username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")
password = project.findProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN")
}
}
}
2.2 Quick Start: Device-Code Flow
Below is the absolute minimal Java program to obtain an authenticated Minecraft session. It uses the default device-code flow and blocks until you complete login in the browser.
import com.raphimc.minecraftauth.MinecraftAuth;
import com.raphimc.minecraftauth.model.MinecraftSession;
public class QuickStart {
public static void main(String[] args) throws Exception {
// Build with default device-code flow (prints URL & user code to console)
MinecraftAuth auth = MinecraftAuth.builder().build();
// Perform login (blocks until you authorize in browser)
MinecraftSession session = auth.login().get();
// Use tokens to call Minecraft or Realms APIs
System.out.println("Access Token: " + session.getAccessToken());
System.out.println("UUID: " + session.getProfile().getId());
System.out.println("Username: " + session.getProfile().getName());
}
}
Key points:
MinecraftAuth.builder().build()
Configures the default device-code flow; it opens a link and prints a code to the console.login().get()
Returns a fully authenticatedMinecraftSession
.- Tokens persist in memory; use
session.getAccessToken()
andsession.getRefreshToken()
for further calls or serialization.
That’s all you need to start making authenticated calls to the Minecraft and Realms APIs.
3. Core Concepts & Architecture
This section explains the library’s internal building blocks: the step‐based authentication engine, HTTP response handlers, time synchronization utility, and the high‐level MinecraftAuth
API for building authentication flows.
3.1 The Step Engine and AbstractStep
The authentication process is modeled as a chain of AbstractStep<S, R>
instances. Each step:
- Executes HTTP calls or local logic (
execute
) - Validates and wraps results in a
StepResult
with expiration - Serializes to/from JSON for refresh and persistence
- Supplies OAuth parameters (client ID, scopes, redirect URI)
Anatomy of AbstractStep
execute(ILogger, HttpClient, S input)
– perform step logicboolean shouldRefresh(R prevResult)
– decide if refresh is neededR fromJson(JsonObject)
/JsonObject toJson(R result)
– (de)serialization
Example: Custom OAuth Step
package net.raphimc.minecraftauth.step.custom;
import com.google.gson.JsonObject;
import net.lenni0451.commons.httpclient.HttpClient;
import net.raphimc.minecraftauth.step.AbstractStep;
import net.raphimc.minecraftauth.util.logging.ILogger;
// S = previous result type (e.g. credentials), R = custom token result
public class CustomTokenStep
extends AbstractStep<CredentialStep.Result, CustomTokenStep.TokenResult> {
public CustomTokenStep(CredentialStep credStep) {
super("customToken", credStep);
// set OAuth client details
this.clientId = "your-client-id";
this.scopes.add("XboxLive.signin");
this.redirectUri = "https://your.redirect/uri";
}
@Override
protected TokenResult execute(ILogger logger,
HttpClient httpClient,
CredentialStep.Result prev) {
logger.info("Requesting custom token");
// perform HTTP POST to token endpoint...
JsonObject response = httpClient
.post("https://login.example.com/oauth2/token")
.form(Map.of(
"client_id", this.clientId,
"grant_type", "authorization_code",
"code", prev.code,
"redirect_uri", this.redirectUri
))
.execute(new MsaResponseHandler());
return new TokenResult(response.get("access_token").getAsString());
}
@Override
public TokenResult fromJson(JsonObject json) {
return new TokenResult(json.get("access_token").getAsString());
}
@Override
public JsonObject toJson(TokenResult result) {
JsonObject json = new JsonObject();
json.addProperty("access_token", result.accessToken);
return json;
}
public static class TokenResult implements StepResult {
public final String accessToken;
public TokenResult(String token) { this.accessToken = token; }
@Override public long expiresAt() { return System.currentTimeMillis() + 3500_000; }
@Override public JsonObject toJson() {
JsonObject j = new JsonObject();
j.addProperty("access_token", this.accessToken);
return j;
}
}
}
3.2 Merging Step Chains: BiMergeStep
BiMergeStep<A, B, R>
combines results from two parallel chains, preserving each branch’s refresh logic. Implement:
execute(logger, client, resultA, resultB)
fromJson
/toJson
for your combined result
Example: Combine Token + Profile
public class CombineTokenProfileStep
extends BiMergeStep<TokenStep.Result, ProfileStep.Result, CombineTokenProfileStep.Result> {
public CombineTokenProfileStep(TokenStep tokenStep, ProfileStep profileStep) {
super("combineTokenProfile", tokenStep, profileStep);
}
@Override
protected Result execute(ILogger logger,
HttpClient client,
TokenStep.Result tok,
ProfileStep.Result prof) {
logger.info("Merging token and profile");
return new Result(tok, prof);
}
@Override
public Result fromJson(JsonObject json) {
TokenStep.Result tok = new TokenStep.Result(json.getAsJsonObject("token"));
ProfileStep.Result prof = new ProfileStep.Result(json.getAsJsonObject("profile"));
return new Result(tok, prof);
}
@Override
public JsonObject toJson(Result result) {
JsonObject j = new JsonObject();
j.add("token", result.token.toJson());
j.add("profile", result.profile.toJson());
return j;
}
public static class Result implements StepResult {
public final TokenStep.Result token;
public final ProfileStep.Result profile;
public Result(TokenStep.Result t, ProfileStep.Result p) {
this.token = t; this.profile = p;
}
@Override public long expiresAt() {
return Math.min(token.expiresAt(), profile.expiresAt());
}
@Override public JsonObject toJson() { /* same as toJson above */ return null; }
}
}
3.3 High‐Level API: MinecraftAuth
MinecraftAuth
exposes builders for common flows:
MicrosoftAuthBuilder
(device code, credentials, webview)XboxLiveAuthBuilder
BedrockAuthBuilder
Webview Login Example
import net.raphimc.minecraftauth.MinecraftAuth;
var auth = MinecraftAuth.builder()
.clientId("your-ms-client-id")
.redirectUri("https://your.app/redirect")
.scopes("XboxLive.signin", "offline_access")
.useWebview() // launches embedded browser
.build();
auth.authenticate()
.thenAccept(session -> {
System.out.println("Access Token: " + session.accessToken());
// session.profile(), session.xboxTokens(), etc.
})
.exceptionally(err -> {
err.printStackTrace(); return null;
});
Device Code Flow Example
var auth = MinecraftAuth.builder()
.clientId("your-client-id")
.useDeviceCode() // prints code+URL to console
.build();
auth.authenticate()
.thenAccept(session -> { /* ... */ });
3.4 JSON Response Handling
JsonHttpResponseHandler
parses JSON and delegates error extraction:
- 204 No Content → returns
null
- Non‐2xx empty → throws
InformativeHttpRequestException("Empty response")
- Wrong Content‐Type → throws
InformativeHttpRequestException("Wrong content type")
- 2xx → parses
JsonObject
- ≥300 → calls subclass
handleJsonError(...)
Common Handlers
// Minecraft API:
new MinecraftResponseHandler() // throws MinecraftRequestException on {error, errorMessage}
// Microsoft OAuth:
new MsaResponseHandler() // throws MsaRequestException on {error, error_description}
Usage Example
JsonObject profile = httpClient
.get("https://api.minecraft.net/user/profile")
.header("Authorization", "Bearer " + session.accessToken())
.execute(new MinecraftResponseHandler());
3.5 Time Synchronization: TimeUtil
TimeUtil.getClientTimeOffset()
computes and caches the offset between local clock and Microsoft’s server time. Steps and signature generation use this offset to:
- Align
expiresAt()
calculations - Build Windows‐epoch timestamps for ECDSA signatures
Usage in Signing
long offset = TimeUtil.getClientTimeOffset();
long signedAt = System.currentTimeMillis() + offset;
Developers can rely on TimeUtil
to avoid clock‐drift issues during token refresh and request signing.
4. Authentication Workflows
This section walks through each supported Microsoft-account login method, shows when to pick each, and gives concrete code snippets built on MinecraftAuth.Builder
. It also covers token refresh, session JSON serialization, and merging intermediate results into full Java or Bedrock sessions.
4.1 Choosing a Login Method
• Device Code Flow – Headless or CLI clients; user copies a code into a browser.
• Credentials Flow – Automated server‐side login for non-MFA accounts.
• WebView Flow – Desktop apps display an embedded browser for interactive login.
4.2 Common Setup
All examples share this boilerplate:
import net.raphimc.minecraftauth.MinecraftAuth;
import net.raphimc.minecraftauth.ApplicationDetails;
import org.apache.hc.client5.http.classic.HttpClient;
import net.raphimc.minecraftauth.logger.ConsoleLogger;
ApplicationDetails appDetails = new ApplicationDetails(
"your-client-id",
URI.create("https://login.microsoftonline.com/common/oauth2/nativeclient"),
OAuthEnvironment.LIVE,
Map.of("response_type", "code", "scope", "XboxLive.signin openid profile")
);
ConsoleLogger logger = new ConsoleLogger();
HttpClient httpClient = HttpClient.createDefault();
MinecraftAuth auth = MinecraftAuth.builder()
.applicationDetails(appDetails)
.logger(logger)
.httpClient(httpClient)
.build();
4.3 Device Code Flow (StepMsaDeviceCode)
When: CLI apps or headless services without embedded browser.
import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode;
import net.raphimc.minecraftauth.step.msa.MsaDeviceCode;
StepMsaDeviceCode.MsaDeviceCodeCallback callback =
new StepMsaDeviceCode.MsaDeviceCodeCallback(deviceCode -> {
System.out.println("User code: " + deviceCode.getUserCode());
System.out.println("Verify at: " + deviceCode.getDirectVerificationUri());
// openBrowser(deviceCode.getDirectVerificationUri());
});
StepMsaDeviceCode deviceCodeStep = new StepMsaDeviceCode(appDetails);
// execute() blocks until the device‐code response arrives
MsaDeviceCode deviceCode = deviceCodeStep.execute(logger, httpClient, callback);
// Later, poll using StepTokenMsaDeviceCode (not shown) at deviceCode.getIntervalMs()
// and check deviceCode.isExpired() to abort if needed.
Practical tips
• Always display userCode
+ verificationUri
as soon as callback fires.
• Use intervalMs
when polling the token‐endpoint.
• Check isExpired()
before each poll to avoid wasted requests.
4.4 Credentials Flow (StepCredentialsMsaCode)
When: Back-end services with non-MFA accounts.
import net.raphimc.minecraftauth.step.msa.StepCredentialsMsaCode;
import net.raphimc.minecraftauth.step.msa.MsaCode;
import net.raphimc.minecraftauth.step.msa.MsaRequestException;
// Prepare credentials
StepCredentialsMsaCode.MsaCredentials creds =
new StepCredentialsMsaCode.MsaCredentials("user@example.com", "P@ssw0rd!");
// Instantiate and execute
StepCredentialsMsaCode credStep = new StepCredentialsMsaCode(appDetails);
try {
MsaCode msaCode = credStep.execute(logger, httpClient, creds);
String authorizationCode = msaCode.getCode();
logger.info("Got MSA code: " + authorizationCode);
// exchange code for tokens via StepTokenMsaCode…
} catch (MsaRequestException e) {
logger.error("MSA Error: " + e.getError() + " – " + e.getErrorDescription());
}
Practical tips
• Ensure your redirect_uri
matches the registered app.
• Handle MsaRequestException
for invalid credentials or account issues.
• This flow cannot bypass MFA or CAPTCHA.
4.5 WebView Flow (StepJfxWebViewMsaCode)
When: Desktop apps need a full UI login.
import net.raphimc.minecraftauth.step.msa.StepJfxWebViewMsaCode;
import net.raphimc.minecraftauth.step.msa.JavaFxWebView;
import net.raphimc.minecraftauth.step.msa.MsaCode;
// Optional: customize open/close behavior
Consumer<JFrame> openWin = frame -> {
frame.setTitle("Login to Microsoft");
frame.setSize(800, 600);
frame.setVisible(true);
};
Consumer<JFrame> closeWin = JFrame::dispose;
JavaFxWebView customView = new JavaFxWebView(openWin, closeWin);
// Create and execute
StepJfxWebViewMsaCode webViewStep =
new StepJfxWebViewMsaCode(appDetails, 120_000); // 2-minute timeout
MsaCode msaCode = webViewStep.execute(logger, httpClient, customView);
// use msaCode.getCode() for token exchange
Practical tips
• The first JFXPanel
call initializes JavaFX runtime.
• Catch UserClosedWindowException
to detect cancellation.
• Keep UI calls on the JavaFX thread via Platform.runLater
.
4.6 Token Refresh
After exchanging an authorization code you get MsaTokenResponse
with accessToken
and refreshToken
. To refresh:
import net.raphimc.minecraftauth.step.msa.StepTokenMsaRefresh;
import net.raphimc.minecraftauth.step.msa.MsaTokenResponse;
// Assume you have oldTokens from the code‐exchange step
String oldRefreshToken = oldTokens.getRefreshToken();
StepTokenMsaRefresh refreshStep = new StepTokenMsaRefresh(appDetails);
MsaTokenResponse newTokens =
refreshStep.execute(logger, httpClient, oldRefreshToken);
logger.info("New access token expires in " + newTokens.getExpiresIn() + "s");
Practical tips
• Schedule refresh before expiry (e.g. at 75% of expiresIn
).
• Persist the latest refreshToken
—it may rotate after each call.
4.7 Full-Session Helpers & JSON Serialization
After you obtain all intermediate tokens/profiles, merge them into a single session object for easy reuse and caching.
4.7.1 Java Edition Session (StepFullJavaSession)
import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession;
import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession.FullJavaSession;
// You already ran:
StepMCProfile.MCProfile profile = profileStep.execute(logger, httpClient);
StepPlayerCertificates.PlayerCertificates certs = certStep.execute(logger, httpClient);
// Merge
StepFullJavaSession javaSessionStep =
new StepFullJavaSession(profileStep, certStep);
FullJavaSession javaSession =
javaSessionStep.execute(logger, httpClient, profile, certs);
// Access merged data
profile = javaSession.prevResult();
certs = javaSession.prevResult2();
// Serialize to JSON
JsonObject sessionJson = javaSessionStep.toUnoptimizedJson(javaSession);
// Restore from JSON later
FullJavaSession restored =
javaSessionStep.fromUnoptimizedJson(sessionJson);
4.7.2 Bedrock Edition Session (StepFullBedrockSession)
import net.raphimc.minecraftauth.step.bedrock.session.StepFullBedrockSession;
import net.raphimc.minecraftauth.step.bedrock.session.StepFullBedrockSession.FullBedrockSession;
// You already ran:
StepMCChain.MCChain mcChain = chainStep.execute(logger, httpClient);
StepPlayFabToken.PlayFabToken pf = playFabStep.execute(logger, httpClient);
StepXblXstsToken.XblXsts<?> xsts = xstsStep.execute(logger, httpClient);
// Merge
StepFullBedrockSession bedrockStep =
new StepFullBedrockSession(chainStep, playFabStep, xstsStep);
FullBedrockSession bedrockSession =
bedrockStep.execute(logger, httpClient, mcChain, pf, xsts);
// Access merged tokens
mcChain = bedrockSession.prevResult();
pf = bedrockSession.prevResult2();
xsts = bedrockSession.prevResult3();
// Serialize
JsonObject bedrockJson = bedrockStep.toUnoptimizedJson(bedrockSession);
// Restore
FullBedrockSession restoredBedrock =
bedrockStep.fromUnoptimizedJson(bedrockJson);
Practical tips
• Cache JSON between app restarts to skip re-authentication.
• Full-session steps propagate errors from underlying steps.
• Always run prerequisite steps first to feed valid inputs into the full-session merger.
This completes the hands-on guide to Microsoft-account authentication flows in MinecraftAuth.
5. Realms API Integration
Enable listing Realms worlds, joining sessions, managing invites and Terms-of-Service for both Java and Bedrock editions. Handle authentication tokens and version pitfalls.
5.1 Listing Available Worlds
Java Edition
import net.raphimc.minecraftauth.service.realms.JavaRealmsService;
import net.raphimc.minecraftauth.service.realms.model.RealmsWorld;
import java.util.List;
public class JavaRealmsExample {
public static void main(String[] args) {
// Initialize with Mojang session credentials
JavaRealmsService service = new JavaRealmsService(
"<accessToken>", "<clientToken>", "<uuid>", "<username>"
);
// Fetch and print available worlds
service.listWorlds()
.thenAccept(worlds -> worlds.forEach(w ->
System.out.printf("[%s] %s (v%s) – %s%n",
w.getId(), w.getName(), w.getVersion(), w.getState()
)
))
.join();
}
}
Bedrock Edition
import net.raphimc.minecraftauth.service.realms.BedrockRealmsService;
import net.raphimc.minecraftauth.service.realms.model.RealmsWorld;
public class BedrockRealmsExample {
public static void main(String[] args) {
// Initialize with Xbox Live token
BedrockRealmsService service = new BedrockRealmsService("<xboxAuthToken>");
service.listWorlds()
.thenAccept(worlds -> worlds.forEach(w ->
System.out.printf("[%s] %s – Owner: %s%n",
w.getId(), w.getName(), w.getOwner()
)
))
.join();
}
}
5.2 Joining a World
Java Edition
service.joinWorld("<worldId>")
.thenAccept(addr ->
System.out.printf("Connect to %s:%d%n", addr.getHost(), addr.getPort())
)
.exceptionally(ex -> {
System.err.println("Join failed: " + ex.getMessage());
return null;
}).join();
Bedrock Edition
service.joinWorld("<worldId>")
.exceptionally(ex -> {
System.err.println("Join failed: " + ex.getMessage());
return null;
}).join();
5.3 Managing Invites
// Accept an invite (Java & Bedrock)
service.acceptInvite("<inviteId>").join();
// (Bedrock only) Leave an invited Realm
service.leaveInvitedRealm("<worldId>").join();
5.4 Accepting Terms of Service (Java Edition)
// Must call before joining if TosException is thrown
service.acceptTos().join();
5.5 Authentication Requirements
- JavaRealmsService
• accessToken, clientToken, uuid, username from Mojang auth - BedrockRealmsService
• Xbox Live token forAuthorization: Bearer <xboxAuthToken>
5.6 Version Pitfalls
- Java Edition blocks snapshot worlds. Filter before joining:
worlds.stream() .filter(w -> !w.getVersion().contains("snapshot")) .forEach(...);
- Always catch
TosException
and callacceptTos()
if prompted.
5.7 Custom Request Headers
Extend AbstractRealmsService to add headers before requests:
service.setCustomHeaders(Map.of(
"X-Custom-Header", "value",
"User-Agent", "MyLauncher/1.0"
));
Use these hooks to integrate proxies, logging or additional auth.
6. Logging & Advanced Utilities
This section covers optional helper modules that enhance integration by providing standardized logging. You can disable logs, write to the console, bridge to SLF4J or plug in your own ILogger
implementation.
6.1 ILogger Interface
ILogger
defines three methods for structured logging. Each method accepts a message and optional context (AbstractStep
).
package net.raphimc.minecraftauth.util.logging;
import net.raphimc.minecraftauth.step.AbstractStep;
public interface ILogger {
void info(String message, AbstractStep<?>... context);
void warn(String message, AbstractStep<?>... context);
void error(String message, Throwable throwable, AbstractStep<?>... context);
}
6.2 NoOpLogger
Suppress all logging calls. Useful for silent mode or tests.
import net.raphimc.minecraftauth.util.logging.NoOpLogger;
import net.raphimc.minecraftauth.client.Authenticator;
NoOpLogger silent = new NoOpLogger();
// Example: disable logging on Authenticator
Authenticator auth = new Authenticator("clientId", "clientSecret")
.setLogger(silent);
auth.authenticate(); // runs without emitting any log
6.3 PlainConsoleLogger
Writes info/warn to stdout and errors to stderr. Configurable prefix and warning-as-error flag.
Constructors:
PlainConsoleLogger()
PlainConsoleLogger(String prefix, boolean warnAsError)
import net.raphimc.minecraftauth.util.logging.PlainConsoleLogger;
import net.raphimc.minecraftauth.client.Authenticator;
// Prefix each line with "[Auth]" and treat warnings as errors
PlainConsoleLogger consoleLogger = new PlainConsoleLogger("[Auth]", true);
Authenticator auth = new Authenticator("id", "secret")
.setLogger(consoleLogger);
auth.authenticate();
// Output:
// [Auth] INFO: Starting authentication...
// [Auth] ERROR: Received unexpected response (warnings are escalated)
6.4 Slf4jConsoleLogger
Bridges logs to SLF4J. Requires an SLF4J implementation (Logback, Log4j, etc.) on the classpath.
import net.raphimc.minecraftauth.util.logging.Slf4jConsoleLogger;
import net.raphimc.minecraftauth.client.Authenticator;
// SLF4J logs will use the class name "MinecraftAuth"
Slf4jConsoleLogger slf4jLogger = new Slf4jConsoleLogger();
Authenticator auth = new Authenticator("id", "secret")
.setLogger(slf4jLogger);
auth.authenticate();
// Logs appear via your SLF4J configuration
6.5 Implementing a Custom ILogger
You can integrate any logging framework or route logs elsewhere by implementing ILogger
.
import net.raphimc.minecraftauth.util.logging.ILogger;
import net.raphimc.minecraftauth.step.AbstractStep;
import java.io.FileWriter;
import java.io.IOException;
import java.time.Instant;
public class FileLogger implements ILogger {
private final FileWriter writer;
public FileLogger(String path) throws IOException {
this.writer = new FileWriter(path, true);
}
private void log(String level, String msg) {
try {
writer.write(Instant.now() + " " + level + ": " + msg + "\n");
writer.flush();
} catch (IOException ignored) {}
}
@Override
public void info(String message, AbstractStep<?>... context) {
log("INFO", message);
}
@Override
public void warn(String message, AbstractStep<?>... context) {
log("WARN", message);
}
@Override
public void error(String message, Throwable throwable, AbstractStep<?>... context) {
log("ERROR", message + " - " + throwable.getMessage());
}
}
6.6 Plugging Your Logger into the System
Most high-level components accept an ILogger
. For example, when building an authentication client:
import net.raphimc.minecraftauth.client.Authenticator;
import net.raphimc.minecraftauth.util.logging.FileLogger;
public class App {
public static void main(String[] args) throws Exception {
FileLogger fileLogger = new FileLogger("auth.log");
Authenticator auth = new Authenticator("clientId", "clientSecret")
.setLogger(fileLogger);
auth.authenticate()
.thenAccept(token -> System.out.println("Authenticated: " + token))
.exceptionally(err -> { err.printStackTrace(); return null; });
}
}
If a component doesn’t expose a setLogger(ILogger)
method, check its builder or factory methods for a logger(...)
option. Custom loggers give you full control over log formatting, destinations and filtering.
7. Development & Contribution Guide
This guide covers project structure, conventions, build tasks, CI setup, publishing flow, and running unit/integration tests (including Microsoft-credential-based tests).
7.1 Project Structure
.
├── .github/
│ ├── dependabot.yml # Dependabot config for Gradle & Actions updates
│ └── workflows/
│ └── build.yml # CI pipeline (build, test, artifact upload)
├── src/
│ ├── main/java # Production code
│ └── test/java # Unit tests
├── build.gradle # Build logic, plugins, dependencies
├── gradle.properties # Performance flags, project metadata
├── settings.gradle # Plugin repositories, Gradle plugin version, project name
└── README.md
7.2 Conventions & Plugins
- Java Library: Applies
java-library
, sets Java 21 compatibility. - Custom Conventions: Enforces code style, versioning and publishing defaults.
- JavaFX Stub Plugin: Provides lightweight JavaFX stubs for headless environments.
- Dependency Management: Core libraries include
java.net.http
, Jackson, JWT-handling, SLF4J.
All plugin versions and conventions live in settings.gradle
and build.gradle
. Update them there for cross-project consistency.
7.3 Build Tasks
Common Gradle tasks:
./gradlew assemble
Compiles main sources, packages JAR../gradlew check
Runstest
,jacocoTestReport
, and any static analysis../gradlew test
Executes unit tests../gradlew integrationTest
Runs integration tests (requires Microsoft credentials)../gradlew clean
Cleans build outputs.
Example: Full Build
# Compile, test, generate reports
./gradlew clean check
Example: Run Only Integration Tests
export MICROSOFT_CLIENT_ID=...
export MICROSOFT_CLIENT_SECRET=...
export MICROSOFT_USERNAME=...
export MICROSOFT_PASSWORD=...
./gradlew integrationTest
7.4 Continuous Integration
CI pipeline: .github/workflows/build.yml
- Triggers:
push
,pull_request
, manual dispatch. - Environment: Ubuntu latest, JDK 21.
- Steps:
- Checkout repository
- Validate Gradle wrapper
- Set up JDK 21
- Run
./gradlew clean check
- Upload artifacts (build logs, test results, JAR)
Dependabot config (.github/dependabot.yml
) updates Maven dependencies daily and Actions workflows weekly.
7.5 Publishing Flow
The project uses Gradle’s maven-publish
plugin with signing:
- Configure Credentials in
~/.gradle/gradle.properties
:signing.keyId=... signing.password=... signing.secretKeyRingFile=~/.gnupg/secring.gpg ossrhUsername=... ossrhPassword=...
- Publish Snapshot:
./gradlew publishToMavenLocal
- Publish Release:
./gradlew publish
Artifacts go to Maven Central (via OSSRH), with sources and Javadoc JARs signed automatically.
7.6 Testing with Microsoft Credentials
Integration tests against Microsoft OAuth require environment variables:
MICROSOFT_CLIENT_ID
MICROSOFT_CLIENT_SECRET
MICROSOFT_USERNAME
MICROSOFT_PASSWORD
Running Integration Tests
# Set credentials (or store in ~/.gradle/gradle.properties as system props)
export MICROSOFT_CLIENT_ID=your-client-id
export MICROSOFT_CLIENT_SECRET=your-client-secret
export MICROSOFT_USERNAME=your-email
export MICROSOFT_PASSWORD=your-password
# Execute integration tests
./gradlew integrationTest
Tests skip or fail fast if credentials are missing. Ensure secure storage of secrets and never commit them to the repository.