Backend Token Verification
When enableTokens is true in server.json, the mod appends a signed token to every URL it opens:
https://your-hud.example.com?webgui_token=<token>Your backend can verify this token to confirm the request came from a genuine WebGUI client.
Token format
The token has two parts separated by .:
<base64url-payload>.<base64url-signature>| Part | Content |
|---|---|
| payload | UTF-8 string 1|<playerUUID>|<expiresAtEpochSeconds>, encoded as base64url without padding |
| signature | HMAC-SHA256 of the raw payload bytes signed with the shared secret, encoded as base64url without padding |
The secret is the base64-decoded bytes of tokenSecretBase64 from server.json.
Reading the token in your SPA
Use the useWebGUIToken hook from @webgui/react to read the token from the URL before sending it to your backend:
tsx
import { useWebGUIToken } from '@webgui/react'
export function App() {
const token = useWebGUIToken() // reads ?webgui_token=... from the URL
useEffect(() => {
if (!token) return
fetch('/api/data?webgui_token=' + token)
.then(r => r.json())
.then(setData)
}, [token])
}Python backend
Requires only the standard library — no third-party packages needed.
python
import base64
import hashlib
import hmac
import time
import uuid
def _b64url_decode(s: str) -> bytes:
padding = '=' * (-len(s) % 4)
return base64.urlsafe_b64decode(s + padding)
def verify_webgui_token(token: str, secret_b64: str) -> dict | None:
"""
Verifies a WebGUI signed token.
Returns {'player_uuid': str, 'expires_at': int} on success,
or None when the token is invalid or expired.
secret_b64 — the value of tokenSecretBase64 from server.json.
"""
if not token:
return None
dot = token.find('.')
if dot <= 0 or dot >= len(token) - 1:
return None
try:
payload_bytes = _b64url_decode(token[:dot])
sig_bytes = _b64url_decode(token[dot + 1:])
except Exception:
return None
secret = base64.b64decode(secret_b64)
expected = hmac.new(secret, payload_bytes, hashlib.sha256).digest()
if not hmac.compare_digest(expected, sig_bytes):
return None
try:
parts = payload_bytes.decode('utf-8').split('|', 2)
except UnicodeDecodeError:
return None
if len(parts) != 3 or parts[0] != '1':
return None
try:
player_uuid = str(uuid.UUID(parts[1]))
exp = int(parts[2])
except (ValueError, AttributeError):
return None
if time.time() > exp:
return None
return {'player_uuid': player_uuid, 'expires_at': exp}FastAPI example
python
import os
from fastapi import FastAPI, Query, HTTPException
app = FastAPI()
SECRET_B64 = os.environ['WEBGUI_TOKEN_SECRET'] # tokenSecretBase64 from server.json
@app.get('/api/data')
async def get_data(webgui_token: str = Query(...)):
data = verify_webgui_token(webgui_token, SECRET_B64)
if not data:
raise HTTPException(status_code=401, detail='Invalid or expired token')
# data['player_uuid'] contains the verified Minecraft player UUID
return {'player': data['player_uuid']}Flask example
python
import os
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
SECRET_B64 = os.environ['WEBGUI_TOKEN_SECRET']
@app.get('/api/data')
def get_data():
token = request.args.get('webgui_token', '')
data = verify_webgui_token(token, SECRET_B64)
if not data:
abort(401)
return jsonify({'player': data['player_uuid']})Node.js backend
Requires only the built-in node:crypto module.
js
const crypto = require('node:crypto');
/**
* Verifies a WebGUI signed token.
*
* Returns { playerUuid: string, expiresAt: number } on success,
* or null when the token is invalid or expired.
*
* @param {string} token - Token from the ?webgui_token query parameter.
* @param {string} secretB64 - tokenSecretBase64 value from server.json.
*/
function verifyWebGuiToken(token, secretB64) {
if (!token) return null;
const dot = token.indexOf('.');
if (dot <= 0 || dot >= token.length - 1) return null;
let payloadBytes, sigBytes;
try {
payloadBytes = Buffer.from(token.slice(0, dot), 'base64url');
sigBytes = Buffer.from(token.slice(dot + 1), 'base64url');
} catch {
return null;
}
const secret = Buffer.from(secretB64, 'base64');
const expected = crypto.createHmac('sha256', secret).update(payloadBytes).digest();
if (expected.length !== sigBytes.length || !crypto.timingSafeEqual(expected, sigBytes)) {
return null;
}
const parts = payloadBytes.toString('utf8').split('|');
if (parts.length !== 3 || parts[0] !== '1') return null;
const exp = parseInt(parts[2], 10);
if (isNaN(exp) || Date.now() / 1000 > exp) return null;
return { playerUuid: parts[1], expiresAt: exp };
}Express example
js
const express = require('express');
const app = express();
const SECRET = process.env.WEBGUI_TOKEN_SECRET; // tokenSecretBase64 from server.json
app.get('/api/data', (req, res) => {
const result = verifyWebGuiToken(req.query.webgui_token, SECRET);
if (!result) return res.status(401).json({ error: 'Invalid or expired token' });
// result.playerUuid contains the verified Minecraft player UUID
res.json({ player: result.playerUuid });
});Hono / Edge example
ts
import { Hono } from 'hono'
import { verifyWebGuiToken } from './verify' // paste the function above
const app = new Hono()
const SECRET = process.env.WEBGUI_TOKEN_SECRET!
app.get('/api/data', (c) => {
const result = verifyWebGuiToken(c.req.query('webgui_token') ?? '', SECRET)
if (!result) return c.json({ error: 'Invalid or expired token' }, 401)
return c.json({ player: result.playerUuid })
})Security notes
- The
tokenSecretBase64is auto-generated on first server start. Copy it fromconfig/webgui/server.jsonto your backend as an environment variable — never hard-code it in source. - Always use constant-time comparison (
hmac.compare_digest/crypto.timingSafeEqual) to prevent timing attacks. - The
queryParamNameinserver.jsondefaults towebgui_token. Pass the matching name touseWebGUIToken(paramName)and use it in your backend route. - Use
tokenTtlSecondsto control how long a token stays valid (default: 900 s / 15 min). Short-lived tokens reduce the window of a stolen-token replay attack.