๐ฅ๏ธ AuthCore Web Panel
A lightweight, single-page admin dashboard built into AuthCore. It serves read-only stats, player lists, login history, and a small set of admin actions over HTTP(S) with bearer-token authentication.
โจ Features
๐ Overview
- Server version, registered/online/lobby/locked/premium account counts
- Live TPS
- Active database dialect and Redis status
- Auto-refreshing dashboard (every 5 s)
๐ฅ Players
- Searchable table of registered accounts, fetched directly from the database (bounded to the 500 most recent by username order, no in-memory cache scan, so it stays fast with 100k+ registered users), sorted by username
- Per-player: mode (premium/offline), status (online/lobby/offline), lock badge, risk score, last IP and country
- Live status badges and risk coloring (risk โฅ 60 shown in red)
๐ History
- Per-player login history (last 20 entries), including IP, country, mode, result and risk
โก Actions
- Kick, disconnect an online player (admin reason message)
- Logout, end an active session
- Unlock, clear an account lock
- Delete, permanently delete an account (with in-browser confirmation)
- Set password, force-reset a password (hashed with the configured algorithm)
- Reload, hot-reload configuration and restart the panel
Every action is written to the security log (security.log) with a WEB_* event tag (the
link action additionally writes a DISCORD_LINK entry).
๐ ๏ธ Setup
1. Generate a token
openssl rand -hex 16
2. Configure settings.conf
HTTP (local only)
session {
web-panel {
enabled = true
host = "127.0.0.1"
port = 25570
https-enabled = false
token = "REPLACE_WITH_YOUR_HEX_TOKEN"
}
}
HTTPS (self-signed)
session {
web-panel {
enabled = true
host = "127.0.0.1"
https-enabled = true
https-port = 25571
token = "REPLACE_WITH_YOUR_HEX_TOKEN"
}
}
On first start with HTTPS enabled, AuthCore generates a self-signed RSA-2048 certificate
(config/authcore/panel-keystore.p12), prints a warning to the console, and displays the
certificate's SHA-256 fingerprint:
Generated a self-signed certificate for the web panel: ...panel-keystore.p12 - add an exception in your browser!
Certificate fingerprint (SHA-256): 3A:9B:...:EF
Verify the fingerprint when connecting to avoid MITM attacks. To use your own certificate,
point https-keystore at a PKCS12 (.p12/.pfx) or JKS (.jks) file and set
https-keystore-password.
Optional: token file
Instead of putting the token inline, set token-file to a path relative to config/authcore/
containing the raw token. The file contents win over the inline token:
session {
web-panel {
enabled = true
host = "127.0.0.1"
port = 25570
token-file = "web-panel-token.txt" # wins over "token"
}
}
openssl rand -hex 16 > config/authcore/web-panel-token.txt
This keeps the secret out of settings.conf (useful for CI pipelines and secret managers).
3. Reload
/authcore reload
The panel starts automatically on server boot when enabled = true and a token is set.
Without a token the panel logs a warning and will not start.
Optional: read-only token
Add a second, read-only token (readonly-token) for viewers who should see stats, players,
and history but never run actions (kick, delete, set-password, linkโฆ). Requests
authenticated with it are answered normally for GET endpoints but receive
403 { "success": false, "error": "Read-only token cannot run actions" } on
POST /api/action:
session {
web-panel {
enabled = true
host = "127.0.0.1"
port = 25570
token = "REPLACE_WITH_YOUR_HEX_TOKEN" # full access (admin)
readonly-token = "REPLACE_WITH_A_READ_ONLY_TOKEN" # view-only (e.g. mod dashboards)
}
}
Generate a second token the same way (openssl rand -hex 16). The read-only token works only
when the panel has a valid full-access token too.
๐ Security Recommendations
- Always set a strong token, the panel refuses to start without one.
- Bind to
127.0.0.1, never expose0.0.0.0to the public internet. - Tunnel instead of exposing, use an SSH tunnel (
ssh -L 25570:127.0.0.1:25570 user@server) or a reverse proxy (Caddy/Nginx) with TLS and rate limiting in front. - Prefer HTTPS with the self-signed certificate + fingerprint verification, or your own CA.
- Rotate the token periodically; anyone with the full token can kick, logout, unlock, delete
accounts and reset passwords. Hand out a separate
readonly-tokento anything that only needs to view data. - The panel runs on a daemon thread and never blocks the server thread.
๐ก API Reference
Base URL: http://127.0.0.1:25570 (or https://127.0.0.1:25571)
Authentication: every request must include:
Authorization: Bearer <token>
Unauthorized requests receive 401 { "success": false, "error": "Unauthorized - send 'Authorization: Bearer <token>'" }.
| Method | Endpoint | Description |
|---|---|---|
GET |
/ |
Dashboard HTML page |
GET |
/metrics |
Prometheus text-format metrics (token-protected) |
GET |
/api/overview |
Server + auth stats |
GET |
/api/players |
Account list (database-backed, bounded to 500) |
GET |
/api/history?uuid=<uuid> |
Login history for a player (last 20 entries) |
POST |
/api/action |
Admin action (kick/logout/unlock/delete/set-password/reload/link) |
GET /, Dashboard
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:25570/
Returns the single-page HTML dashboard (dark theme, no external resources).
GET /metrics, Prometheus Metrics
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:25570/metrics
Returns AuthCore metrics in Prometheus text format (Content-Type: text/plain; version=0.0.4),
so you can scrape the panel from Prometheus/Grafana. The endpoint is token-protected like every
other route (401 without a valid token). A read-only token may also scrape it.
GET /api/overview, Overview
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:25570/api/overview
Response:
{
"version": "1.0.0",
"registered": 42,
"online": 7,
"inLobby": 2,
"locked": 1,
"premium": 31,
"tps": 19.8,
"database": "SQLITE",
"redis": false
}
| Field | Type | Description |
|---|---|---|
version |
string | AuthCore mod version |
registered |
int | Registered accounts (have a password) |
online |
int | Players currently connected |
inLobby |
int | Players restricted in the auth lobby |
locked |
int | Accounts currently locked |
premium |
int | Premium (online-mode) accounts |
tps |
double | Current TPS (1 decimal) |
database |
string | Active dialect: SQLITE, MYSQL, or POSTGRESQL |
redis |
boolean | Whether Redis sync is enabled |
GET /api/players, Player List
The list is database-backed (bounded query via User.fetchPlayersPublic(500, null)) instead
of iterating the in-memory cache, it stays fast on servers with 100k+ registered users and
returns up to 500 accounts per call.
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:25570/api/players
Response:
{
"players": [
{
"username": "Steve",
"uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5",
"nickname": "Stevey",
"premium": true,
"registered": true,
"online": true,
"inLobby": false,
"locked": false,
"risk": 20,
"ip": "127.0.0.1",
"country": "US"
}
],
"count": 1
}
| Field | Type | Description |
|---|---|---|
username |
string | Account name |
uuid |
string | Player UUID (canonical format) |
nickname |
string | Display nickname set via /account nickname (empty string when unset) |
premium |
boolean | Premium (online-mode) account |
registered |
boolean | Has a password set |
online |
boolean | Currently connected |
inLobby |
boolean | Currently in the auth lobby |
locked |
boolean | Currently locked |
risk |
int | Last computed risk score (0โ100) |
ip |
string | Last login IP (may be empty) |
country |
string | GeoIP country code (may be empty) |
count |
int | Number of players in the array |
GET /api/history?uuid=<uuid>, Login History
curl -H "Authorization: Bearer $TOKEN" \
"http://127.0.0.1:25570/api/history?uuid=069a79f4-44e9-4726-a5be-fca90e38aaf5"
Response (history is an array of formatted strings; invalid/absent UUID โ empty array):
{
"history": [
"2026-08-08 12:00:00 | Steve | 127.0.0.1 | US | online | SUCCESS | risk=20"
]
}
POST /api/action, Admin Actions
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action": "kick", "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5"}' \
http://127.0.0.1:25570/api/action
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
action |
string | yes | One of: kick, logout, unlock, delete, set-password, reload, link |
uuid |
string | for all except reload |
Player UUID to act on |
value |
string | for set-password and link |
New password (for set-password); 6-char link code or raw discordId (for link) |
| Action | Effect |
|---|---|
kick |
Disconnects the player with the admin-kick message (fails if offline) |
logout |
Ends the player's active session |
unlock |
Clears the account lock |
delete |
Kicks + permanently deletes the account |
set-password |
Hashes value with the configured algorithm and stores it |
reload |
Reloads configuration and restarts the panel (no uuid needed) |
link |
Discord account linking (used by Discord bots): value is a 6-char link code ([A-Z2-9]{6}, resolved via Redis and consumed, single-use) or a raw discordId stored on the uuid account; writes USERS.discordId, logs DISCORD_LINK, sends a webhook confirmation |
๐ Discord linking, players run
/discord linkin-game to get a 6-char code (published to the webhook + stored in Redis for 10 minutes) and send it to your Discord bot. The bot completes the pairing with this action. The bot never touches the database, every write is executed by the backend through this API; the bot talks to the backend over Redis (link codes, theauthcore:discord:<id>mapping and theauthcore:eventspub/sub bus) plus this API. See API.md โ Discord Account Linking for the full flow.
Success response:
{ "success": true, "message": "Kicked Steve" }
Error response (examples: Invalid JSON body, Missing 'action', Missing 'uuid',
Invalid UUID, User not found, User is not online, Unknown action: x):
{ "success": false, "error": "User not found" }
Per-action curl examples
TOKEN=REPLACE_WITH_YOUR_HEX_TOKEN
BASE=http://127.0.0.1:25570
UUID=069a79f4-44e9-4726-a5be-fca90e38aaf5
# Kick
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"action\":\"kick\",\"uuid\":\"$UUID\"}" $BASE/api/action
# Logout
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"action\":\"logout\",\"uuid\":\"$UUID\"}" $BASE/api/action
# Unlock
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"action\":\"unlock\",\"uuid\":\"$UUID\"}" $BASE/api/action
# Delete
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"action\":\"delete\",\"uuid\":\"$UUID\"}" $BASE/api/action
# Set password (requires value)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"action\":\"set-password\",\"uuid\":\"$UUID\",\"value\":\"NewPassw0rd1\"}" $BASE/api/action
# Reload (no uuid needed)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"action":"reload"}' $BASE/api/action
# Link a Discord account via a 6-char code from /discord link (uuid is ignored, code wins)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"action":"link","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","value":"AB12CD"}' $BASE/api/action
# Link a Discord account directly by discordId (stored on the uuid account, no Redis needed)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"action\":\"link\",\"uuid\":\"$UUID\",\"value\":\"123456789012345678\"}" $BASE/api/action
๐ก๏ธ Behavior Notes & Compatibility
๐ Database Migration Suspension
If AuthCore's automatic schema migration fails at startup (Database.migrationBlocked = true), the server suspends registration and login and prints fix instructions (back up +
delete the DB file to recreate it, run the manual ALTER TABLE statements from the log, or
restore a backup). The web panel stays up so you can inspect the damage:
/api/overviewand/api/playerscontinue to work (they read what the database can still return), but counts may be partial.- Actions that need a writable account state (
set-password,delete,link) fail with a clear error until the schema is repaired, no half-applied writes. - The panel itself never triggers the migration; fix the database, then
/authcore reload.
๐ง Encrypter Argon2 Fallback
Password hashing never silently degrades: an unknown password-hash-algorithm value (or a
hashing exception) logs a warning and falls back to Argon2id so set-password and
/register never store an unusable hash. md5 is mapped to SHA-256 internally and flagged as
weak. See SECURITY.md for
details.
๐งฉ Version-Fragile Mixins (required:false)
The server mixins that target version-sensitive classes (ServerHandshakeNetworkHandlerMixin,
ServerLoginNetworkHandlerMixin, ServerPlayNetworkHandlerChatMixin) are declared
required:false in authcore.server.mixins.json. If a future Minecraft version changes a
method signature, the mixin does not apply instead of crashing the server, the panel and
the rest of the mod keep working, and CI's multi-version matrix reports the mismatch.
๐จ Client Companion (Bundled)
Every range jar ships environment: "*" with the client login-screen companion built in
(1.20.2+ clients; older clients load safely and skip the screen), no separate build flag.
๐ฎ Multi-Version Workspace & Host-Compatibility Matrix
- The Stonecraft 1.10 + Stonecutter 0.9 workspace compiles one merged source tree
(
src/main/java, Mojang mappings) per version-group ร loader variant (:1.18.2-fabric,:1.18.2-forge,:1.21.11-fabric,:1.21.11-forge,:1.21.11-neoforge,:26.2-fabric,:26.2-neoforge; active1.21.11-fabric) into the seven range jars (authcore-<range>-<loader>-<v>.jar); Java toolchains are 17 / 21 / 25 per group. - The host-compatibility harness (
tools/host-tests/run-host-tests.ps1) boots each range jar on its verify versions in Docker, 1.16.5 / 1.17.1 / 1.18.2, 1.19.4 / 1.20.6 / 1.21.11, 26.1.2 / 26.2, with live log streaming and report pruning (reports/latest.md|html|json). Full matrix: 8/8 endpoints ร 7/7 loader targets PASS (2026-08-10).
๐งพ Self-Signed Certificate Details
- Location:
config/authcore/panel-keystore.p12 - Algorithm: RSA 2048, SHA256withRSA, valid 10 years
- Keystore password:
authcore(internal;https-keystore-passwordis ignored for the auto-generated store) - On startup the console prints the certificate SHA-256 fingerprint, compare it against the certificate your browser/curl receives to confirm you are talking to your own server:
# Linux/macOS
openssl s_client -connect 127.0.0.1:25571 -servername 127.0.0.1 </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256
# or with curl
curl -kv https://127.0.0.1:25571/api/overview -H "Authorization: Bearer $TOKEN" 2>&1 \
| grep -A1 "Server certificate"
๐ Brute-force protection
- Token comparisons are constant-time (no timing side-channels).
- After 5 failed token attempts from one source IP, the panel returns 429 for a 60-second lockout window (tracking is bounded - max 2048 IPs, stale entries pruned).
- Internal errors are masked (generic
500) - details go to the server log only.