diff --git a/MODRINTH_README.md b/MODRINTH_README.md index d71b9f2..3b6d2af 100644 --- a/MODRINTH_README.md +++ b/MODRINTH_README.md @@ -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 | @@ -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 | @@ -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 diff --git a/README.md b/README.md index a22c892..c121eb6 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 | @@ -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", @@ -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. @@ -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 --- diff --git a/gradle.properties b/gradle.properties index a82dca1..0daae81 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 diff --git a/src/main/java/net/natxo/mcrestapi/config/ApiConfig.java b/src/main/java/net/natxo/mcrestapi/config/ApiConfig.java index 8d66b58..acefbe6 100644 --- a/src/main/java/net/natxo/mcrestapi/config/ApiConfig.java +++ b/src/main/java/net/natxo/mcrestapi/config/ApiConfig.java @@ -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 keys = new ArrayList<>(); @@ -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; @@ -213,6 +217,16 @@ public List getKeys() { } } + // --- Auth --- + + public AuthConfig getAuth() { + return auth; + } + + public boolean isAuthEnabled() { + return auth.isEnabled(); + } + // --- CORS --- public CorsConfig getCors() { @@ -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 { diff --git a/src/main/java/net/natxo/mcrestapi/endpoints/admin/SettingsAdminEndpoint.java b/src/main/java/net/natxo/mcrestapi/endpoints/admin/SettingsAdminEndpoint.java index de5e688..1dd16ab 100644 --- a/src/main/java/net/natxo/mcrestapi/endpoints/admin/SettingsAdminEndpoint.java +++ b/src/main/java/net/natxo/mcrestapi/endpoints/admin/SettingsAdminEndpoint.java @@ -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)); } @@ -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); diff --git a/src/main/java/net/natxo/mcrestapi/http/ApiServer.java b/src/main/java/net/natxo/mcrestapi/http/ApiServer.java index 8044395..3f13095 100644 --- a/src/main/java/net/natxo/mcrestapi/http/ApiServer.java +++ b/src/main/java/net/natxo/mcrestapi/http/ApiServer.java @@ -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()); @@ -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() { diff --git a/src/main/java/net/natxo/mcrestapi/http/middleware/AuthMiddleware.java b/src/main/java/net/natxo/mcrestapi/http/middleware/AuthMiddleware.java index f3a5962..3bc516d 100644 --- a/src/main/java/net/natxo/mcrestapi/http/middleware/AuthMiddleware.java +++ b/src/main/java/net/natxo/mcrestapi/http/middleware/AuthMiddleware.java @@ -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) { diff --git a/src/main/java/net/natxo/mcrestapi/http/middleware/MasterKeyMiddleware.java b/src/main/java/net/natxo/mcrestapi/http/middleware/MasterKeyMiddleware.java index 61e23f6..c524e03 100644 --- a/src/main/java/net/natxo/mcrestapi/http/middleware/MasterKeyMiddleware.java +++ b/src/main/java/net/natxo/mcrestapi/http/middleware/MasterKeyMiddleware.java @@ -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 ")) { diff --git a/src/main/resources/assets/mcrestapi/admin/css/layout.css b/src/main/resources/assets/mcrestapi/admin/css/layout.css index e4be44f..a0c6c3b 100644 --- a/src/main/resources/assets/mcrestapi/admin/css/layout.css +++ b/src/main/resources/assets/mcrestapi/admin/css/layout.css @@ -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; } diff --git a/src/main/resources/assets/mcrestapi/admin/favicon.png b/src/main/resources/assets/mcrestapi/admin/favicon.png index 2117f5a..61742ce 100644 Binary files a/src/main/resources/assets/mcrestapi/admin/favicon.png and b/src/main/resources/assets/mcrestapi/admin/favicon.png differ diff --git a/src/main/resources/assets/mcrestapi/admin/index.html b/src/main/resources/assets/mcrestapi/admin/index.html index 16941c9..37b9c26 100644 --- a/src/main/resources/assets/mcrestapi/admin/index.html +++ b/src/main/resources/assets/mcrestapi/admin/index.html @@ -30,7 +30,7 @@

MCRestAPI

+
+
+
Require API Key
+
When off, ALL endpoints (including admin) are open. Only disable this behind a reverse proxy that handles authentication.
+
+ +
diff --git a/src/main/resources/assets/mcrestapi/admin/js/settings.js b/src/main/resources/assets/mcrestapi/admin/js/settings.js index 260851d..3ed293c 100644 --- a/src/main/resources/assets/mcrestapi/admin/js/settings.js +++ b/src/main/resources/assets/mcrestapi/admin/js/settings.js @@ -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 = ''; diff --git a/src/main/resources/assets/mcrestapi/openapi.json b/src/main/resources/assets/mcrestapi/openapi.json index 4890eb1..d6914ec 100644 --- a/src/main/resources/assets/mcrestapi/openapi.json +++ b/src/main/resources/assets/mcrestapi/openapi.json @@ -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": [