Chat about this codebase

AI-powered code exploration

Online

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 authenticated MinecraftSession.
  • Tokens persist in memory; use session.getAccessToken() and session.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 logic
  • boolean shouldRefresh(R prevResult) – decide if refresh is needed
  • R 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 for Authorization: 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 call acceptTos() 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
    Runs test, 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:
    1. Checkout repository
    2. Validate Gradle wrapper
    3. Set up JDK 21
    4. Run ./gradlew clean check
    5. 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:

  1. Configure Credentials in ~/.gradle/gradle.properties:
    signing.keyId=...
    signing.password=...
    signing.secretKeyRingFile=~/.gnupg/secring.gpg
    ossrhUsername=...
    ossrhPassword=...
    
  2. Publish Snapshot:
    ./gradlew publishToMavenLocal
    
  3. 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.