Skip to content

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>
PartContent
payloadUTF-8 string 1|<playerUUID>|<expiresAtEpochSeconds>, encoded as base64url without padding
signatureHMAC-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 tokenSecretBase64 is auto-generated on first server start. Copy it from config/webgui/server.json to 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 queryParamName in server.json defaults to webgui_token. Pass the matching name to useWebGUIToken(paramName) and use it in your backend route.
  • Use tokenTtlSeconds to control how long a token stays valid (default: 900 s / 15 min). Short-lived tokens reduce the window of a stolen-token replay attack.