Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions MODRINTH_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ The configuration file is located at `config/mcrestapi.json` and is generated au
| `maxConnections` | integer | `50` | Maximum concurrent HTTP connections |
| `swagger` | boolean | `true` | Enable/disable Swagger UI and OpenAPI spec |
| `masterKeyHash` | string | (generated) | PBKDF2 hash of the master key |
| `auth` | object | (enabled) | Set `auth.enabled` to `false` to delegate auth to a reverse proxy |
| `keys` | array | (generated) | List of API keys with permissions |
| `cors` | object | (disabled) | CORS configuration |

Expand All @@ -73,6 +74,16 @@ Authorization: Bearer mcsapi_xxxxxxxxxxxxxxxx

The **master key** is generated on first launch and grants access to admin endpoints (`/api/admin/*`) and the dashboard (`/admin`). It also has wildcard permissions for all data endpoints.

### Disabling authentication

If a reverse proxy already handles auth (basic auth, OIDC/forward-auth, etc.), set `auth.enabled` to `false` in the config:

```json
{ "auth": { "enabled": false } }
```

This disables **all** authentication, including the admin endpoints — the reverse proxy becomes the only trust boundary. Only do this when bound to `127.0.0.1` behind a proxy that enforces access control. The mod logs a warning on startup (louder if the bind address is not loopback). You can also toggle it live from the dashboard (**Settings → Require API Key**), no restart required.

### Permissions

| Permission | Description | Endpoints |
Expand Down Expand Up @@ -162,3 +173,4 @@ Built-in web dashboard at `/admin` (requires master key).
- Master key stored separately from API keys
- For remote access, use a reverse proxy (nginx, Caddy) with HTTPS/TLS
- Create API keys with minimal permissions needed for each use case
- Only disable authentication when bound to `127.0.0.1` behind a trusted reverse proxy that enforces auth
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ A Fabric mod for Minecraft 1.21.11 that exposes a REST API and real-time event s
- [Authentication](#authentication)
- [API Keys](#api-keys)
- [Master Key](#master-key)
- [Disabling Authentication](#disabling-authentication-reverse-proxy-setups)
- [Permissions](#permissions)
- [API Endpoints](#api-endpoints)
- [Public Endpoints](#public-endpoints-no-authentication)
Expand Down Expand Up @@ -100,6 +101,7 @@ The configuration file is located at `config/mcrestapi.json` and is generated au
| `maxConnections` | integer | `50` | Maximum concurrent HTTP connections |
| `swagger` | boolean | `true` | Enable/disable Swagger UI and OpenAPI spec |
| `masterKeyHash` | string | (generated) | PBKDF2 hash of the master key |
| `auth` | object | (enabled) | Authentication settings. Set `auth.enabled` to `false` to delegate auth to a reverse proxy |
| `keys` | array | (generated) | List of API keys with permissions |
| `cors` | object | (disabled) | CORS configuration |

Expand All @@ -112,6 +114,9 @@ The configuration file is located at `config/mcrestapi.json` and is generated au
"maxConnections": 50,
"swagger": true,
"masterKeyHash": "pbkdf2$...",
"auth": {
"enabled": true
},
"keys": [
{
"id": "dk_a1b2c3d4",
Expand Down Expand Up @@ -152,6 +157,28 @@ The master key is a special key generated on first launch that grants access to

The master key is separate from API keys and cannot be managed through the admin interface.

### Disabling Authentication (reverse proxy setups)

If you already terminate authentication at a reverse proxy (e.g. HTTP basic auth, OIDC/forward-auth via Authelia, Authentik, oauth2-proxy, etc.), the built-in API key check is redundant. You can turn it off:

```json
{
"auth": {
"enabled": false
}
}
```

When `auth.enabled` is `false`:

- **All** endpoints become open, including the admin/key-management endpoints (`/api/admin/*`) and the admin dashboard. The reverse proxy becomes the single trust boundary — there is no second layer behind it.
- The mod logs a prominent warning on startup. If `bindAddress` is **not** loopback (e.g. `0.0.0.0`) it logs a louder warning, because the API is then reachable without any authentication.
- Existing clients that still send `Authorization: Bearer ...` keep working — the token is simply ignored.

**Safe pattern:** bind to `127.0.0.1` (the default) and let only your reverse proxy reach the port, with the proxy enforcing authentication. Never expose an unauthenticated instance directly to the internet.

You can toggle this at runtime from the admin dashboard (**Settings → Require API Key**) — the change applies immediately, no restart required. Existing installs that don't have the `auth` block in their config yet can either add the snippet above manually or just flip the toggle once (which writes the block to `config/mcrestapi.json`).

### Permissions

Each API key is assigned a list of permissions that control which endpoints it can access.
Expand Down Expand Up @@ -652,6 +679,7 @@ The master key is generated on first launch and its hash is stored separately fr
- Create API keys with minimal permissions needed for each use case
- Rotate API keys periodically by revoking and creating new ones
- Enable CORS only if browser-based clients need access, and restrict origins to specific domains
- Only [disable authentication](#disabling-authentication-reverse-proxy-setups) when the API is bound to `127.0.0.1` behind a trusted reverse proxy that enforces auth — never expose an unauthenticated instance directly

---

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ loader_version=0.18.4
loom_version=1.15-SNAPSHOT

# Mod Properties
mod_version=1.1.0
mod_version=1.2.0
maven_group=net.natxo.mcrestapi
archives_base_name=mcrestapi

Expand Down
30 changes: 30 additions & 0 deletions src/main/java/net/natxo/mcrestapi/config/ApiConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public class ApiConfig {
private int maxConnections = 50;
private boolean swagger = true;
private String masterKeyHash = "";
private AuthConfig auth = new AuthConfig();
private CorsConfig cors = new CorsConfig();
private List<ApiKey> keys = new ArrayList<>();

Expand Down Expand Up @@ -62,6 +63,9 @@ private static ApiConfig load(Path file) {
if (config.cors == null) {
config.cors = new CorsConfig();
}
if (config.auth == null) {
config.auth = new AuthConfig();
}

MCRestAPI.LOGGER.info("[MCRestAPI] Config loaded from {}", file);
return config;
Expand Down Expand Up @@ -213,6 +217,16 @@ public List<ApiKey> getKeys() {
}
}

// --- Auth ---

public AuthConfig getAuth() {
return auth;
}

public boolean isAuthEnabled() {
return auth.isEnabled();
}

// --- CORS ---

public CorsConfig getCors() {
Expand Down Expand Up @@ -241,6 +255,22 @@ public void setSwaggerEnabled(boolean swagger) {
this.swagger = swagger;
}

// --- Auth config inner class ---

public static class AuthConfig {
// volatile: the flag is read by request-handling virtual threads and may be
// flipped live from the admin settings endpoint on a different thread.
private volatile boolean enabled = true;

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

// --- CORS config inner class ---

public static class CorsConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ private void handleGet(HttpExchange exchange) throws IOException {
response.put("bind_address", config.getBindAddress());
response.put("max_connections", config.getMaxConnections());
response.put("swagger_enabled", config.isSwaggerEnabled());
response.put("auth_enabled", config.isAuthEnabled());
HttpUtil.sendJson(exchange, 200, GSON.toJson(response));
}

Expand All @@ -49,6 +50,10 @@ private void handleUpdate(HttpExchange exchange) throws IOException {
config.setSwaggerEnabled(json.get("swagger_enabled").getAsBoolean());
}

if (json.has("auth_enabled")) {
config.getAuth().setEnabled(json.get("auth_enabled").getAsBoolean());
}

config.save();

handleGet(exchange);
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/net/natxo/mcrestapi/http/ApiServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ public class ApiServer {

private final HttpServer httpServer;
private final boolean swaggerEnabled;
private final ApiConfig config;

public ApiServer(ApiConfig config, TpsCollector tpsCollector, PlayerTracker playerTracker, EventCollector eventCollector, DedicatedServer server) throws IOException {
this.config = config;
InetSocketAddress address = new InetSocketAddress(config.getBindAddress(), config.getPort());
this.httpServer = HttpServer.create(address, config.getMaxConnections());
this.httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
Expand Down Expand Up @@ -72,6 +74,31 @@ public void start() {
MCRestAPI.LOGGER.info("[MCRestAPI] Swagger UI at http://{}:{}/api/docs",
httpServer.getAddress().getHostString(), httpServer.getAddress().getPort());
}
logSecurityWarnings();
}

private void logSecurityWarnings() {
if (config.isAuthEnabled()) {
return;
}

String bind = config.getBindAddress();
MCRestAPI.LOGGER.warn("[MCRestAPI] ============================================================");
MCRestAPI.LOGGER.warn("[MCRestAPI] AUTHENTICATION IS DISABLED (auth.enabled = false)");
MCRestAPI.LOGGER.warn("[MCRestAPI] All endpoints, including admin/key management, are OPEN.");
if (isLoopback(bind)) {
MCRestAPI.LOGGER.warn("[MCRestAPI] Bound to {} (loopback). Make sure your reverse proxy", bind);
MCRestAPI.LOGGER.warn("[MCRestAPI] enforces authentication (basic auth, OIDC, etc.).");
} else {
MCRestAPI.LOGGER.warn("[MCRestAPI] Bind address is {} (NOT loopback): the API is exposed", bind);
MCRestAPI.LOGGER.warn("[MCRestAPI] WITHOUT authentication. Ensure a reverse proxy or firewall");
MCRestAPI.LOGGER.warn("[MCRestAPI] restricts access, or anyone who reaches this port has full control.");
}
MCRestAPI.LOGGER.warn("[MCRestAPI] ============================================================");
}

private static boolean isLoopback(String addr) {
return "127.0.0.1".equals(addr) || "::1".equals(addr) || "localhost".equalsIgnoreCase(addr);
}

public void stop() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ public AuthMiddleware(ApiConfig config, String requiredPermission, HttpHandler n

@Override
public void handle(HttpExchange exchange) throws IOException {
// Auth can be delegated to a reverse proxy: when disabled, skip all token checks.
// Read live on every request so the admin toggle takes effect without a restart.
if (!config.isAuthEnabled()) {
next.handle(exchange);
return;
}

String rawKey = extractBearerToken(exchange);

if (rawKey == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ public MasterKeyMiddleware(ApiConfig config, HttpHandler next) {

@Override
public void handle(HttpExchange exchange) throws IOException {
// When auth is disabled the reverse proxy is the trust boundary, so admin
// endpoints are open too (consistent with the regular API layer).
if (!config.isAuthEnabled()) {
next.handle(exchange);
return;
}

String authHeader = exchange.getRequestHeaders().getFirst("Authorization");

if (authHeader == null || !authHeader.startsWith("Bearer ")) {
Expand Down
10 changes: 5 additions & 5 deletions src/main/resources/assets/mcrestapi/admin/css/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@
text-overflow: ellipsis;
}

.sidebar-header .dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent);
.sidebar-header .sidebar-logo {
width: 26px;
height: 26px;
object-fit: cover;
border-radius: 6px;
flex-shrink: 0;
}

Expand Down
Binary file modified src/main/resources/assets/mcrestapi/admin/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion src/main/resources/assets/mcrestapi/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ <h1>MCRestAPI</h1>
<div id="app" class="app-layout hidden">
<aside class="sidebar">
<div class="sidebar-header">
<span class="dot"></span>
<img class="sidebar-logo" src="favicon.png" alt="MCRestAPI">
<h2>MCRestAPI</h2>
</div>
<nav class="sidebar-nav">
Expand Down Expand Up @@ -191,6 +191,16 @@ <h3>Server Settings</h3>
<span class="toggle-slider"></span>
</label>
</div>
<div class="toggle-row">
<div>
<div class="toggle-label">Require API Key</div>
<div class="toggle-desc">When off, ALL endpoints (including admin) are open. Only disable this behind a reverse proxy that handles authentication.</div>
</div>
<label class="toggle">
<input type="checkbox" id="auth-toggle">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="card">
<div class="card-header">
Expand Down
32 changes: 32 additions & 0 deletions src/main/resources/assets/mcrestapi/admin/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,44 @@ var SettingsPage = (function () {
}
});
}

var authToggle = document.getElementById('auth-toggle');
if (authToggle && !authToggle._bound) {
authToggle._bound = true;
authToggle.addEventListener('change', async function () {
if (!authToggle.checked) {
var ok = confirm(
'Disable API key authentication?\n\n' +
'ALL endpoints, including admin and key management, will be OPEN to anyone ' +
'who can reach this server. Only do this if a reverse proxy or firewall ' +
'enforces access control.'
);
if (!ok) {
authToggle.checked = true;
return;
}
}
try {
await Api.request('/api/admin/settings', 'PUT', { auth_enabled: authToggle.checked });
Utils.toast(
authToggle.checked
? 'Authentication enabled — API key now required'
: 'Authentication disabled — all endpoints are open',
authToggle.checked ? 'success' : 'error'
);
} catch (e) {
authToggle.checked = !authToggle.checked;
Utils.toast('Failed to update settings: ' + e.message, 'error');
}
});
}
}

async function loadSettings() {
try {
var data = await Api.request('/api/admin/settings', 'GET');
document.getElementById('swagger-toggle').checked = data.swagger_enabled;
document.getElementById('auth-toggle').checked = data.auth_enabled;

document.getElementById('page-badge').textContent = '';

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/assets/mcrestapi/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "MCRestAPI",
"description": "REST API for monitoring and controlling Minecraft servers",
"description": "REST API for monitoring and controlling Minecraft servers. Authentication uses a Bearer API key by default; it can be disabled (auth.enabled = false in the config) to delegate authentication to a reverse proxy.",
"version": "1.0.0"
},
"servers": [
Expand Down
Loading