unslothai/unsloth · warning · HTTPException

nonce must be base64url

Error message

nonce must be base64url

What it means

GET /auth/identity is a challenge-response endpoint that HMACs a caller-supplied nonce with the install identity secret. The nonce must be base64url-encoded; if `base64.urlsafe_b64decode` raises, the endpoint returns HTTP 400 with detail 'nonce must be base64url'. This guards the proof protocol against malformed input before any cryptographic work.

Source

Thrown at studio/backend/routes/auth.py:404

        # A successful login resets the IP's throttle, including any overflow it
        # accumulated during saturation (drop only this IP's entry, so a
        # shard-mate's throttle is untouched).
        _overflow_shard(ip).pop(ip, None)


# Sync def (not async): compute_identity_proof touches SQLite on the first call,
# so FastAPI runs it in the threadpool rather than blocking the event loop.
@router.get("/identity")
def identity(nonce: str, request: Request) -> dict:
    """Challenge-response proof this is the real local Unsloth: caller sends a nonce,
    gets HMAC(install identity secret, nonce, connection address + port).
    Unauthenticated and side-effect free; a process that can't read the same-user
    secret can't forge a proof, and binding to the address/port the connection
    landed on stops a squatter relaying a proof from the real Unsloth elsewhere."""
    try:
        raw = base64.urlsafe_b64decode(nonce)
    except Exception:
        raise HTTPException(
            status_code = status.HTTP_400_BAD_REQUEST, detail = "nonce must be base64url"
        )
    if not 16 <= len(raw) <= 128:
        raise HTTPException(
            status_code = status.HTTP_400_BAD_REQUEST, detail = "nonce must decode to 16-128 bytes"
        )
    # The address + port the connection actually landed on, from the socket
    # (request.scope is getsockname, so it is the real local address even when
    # bound to 0.0.0.0), never the client-controlled Host header.
    server = request.scope.get("server") or ("", 0)
    host = server[0] or ""
    port = server[1] if server[1] is not None else 0
    return {"proof": storage.compute_identity_proof(raw, host, port)}


@router.get("/status", response_model = AuthStatusResponse)
async def auth_status() -> AuthStatusResponse:
    """Auth initialization state; ``default_username`` is exposed for first-boot UI prefill only."""

View on GitHub (pinned to 203007d190)

Solutions

  1. Encode the nonce with URL-safe base64: `base64.urlsafe_b64encode(os.urandom(32)).decode().rstrip('=')` on the client
  2. If sending standard base64, convert it: replace '+' with '-' and '/' with '_' before sending
  3. Verify the nonce string contains only [A-Za-z0-9_-] characters before making the request

Example fix

# before
import secrets
resp = client.get("/auth/identity", params={"nonce": secrets.token_hex(32)})
# after
import base64, os
resp = client.get("/auth/identity", params={"nonce": base64.urlsafe_b64encode(os.urandom(32)).decode().rstrip("=")})
Defensive patterns

Strategy: validation

Validate before calling

import base64, re

def is_base64url(s: str) -> bool:
    return bool(re.fullmatch(r"[A-Za-z0-9_-]+={0,2}", s)) and _can_decode(s)

def _can_decode(s: str):
    try:
        base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
        return True
    except Exception:
        return False

Try / catch

r = client.get("/auth/identity", params={"nonce": nonce})
if r.status_code == 400 and "base64url" in r.json()["detail"]:
    nonce = base64.urlsafe_b64encode(os.urandom(32)).decode().rstrip("=")
    r = client.get("/auth/identity", params={"nonce": nonce})

Prevention

When it happens

Trigger: Calling GET /identity with a `nonce` query parameter that is not valid base64url: raw hex strings, plain ASCII text, standard-base64 with `+`/`/` characters, or truncated padding (e.g. a 17-byte nonce encoded then chopped).

Common situations: Client libraries encoding the nonce with the wrong alphabet (base64 instead of base64url), hand-crafted curl tests using arbitrary strings, or non-ASCII bytes injected into the query string.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/8ac31574f14187d2. Report an issue: GitHub.