{"record":{"id":"a171a6426900c43a","repo":"unslothai/unsloth","slug":"nonce-must-decode-to-16-128-bytes","errorCode":null,"errorMessage":"nonce must decode to 16-128 bytes","messagePattern":"nonce must decode to 16-128 bytes","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"studio/backend/routes/auth.py","lineNumber":408,"sourceCode":"\n\n# Sync def (not async): compute_identity_proof touches SQLite on the first call,\n# so FastAPI runs it in the threadpool rather than blocking the event loop.\n@router.get(\"/identity\")\ndef identity(nonce: str, request: Request) -> dict:\n    \"\"\"Challenge-response proof this is the real local Unsloth: caller sends a nonce,\n    gets HMAC(install identity secret, nonce, connection address + port).\n    Unauthenticated and side-effect free; a process that can't read the same-user\n    secret can't forge a proof, and binding to the address/port the connection\n    landed on stops a squatter relaying a proof from the real Unsloth elsewhere.\"\"\"\n    try:\n        raw = base64.urlsafe_b64decode(nonce)\n    except Exception:\n        raise HTTPException(\n            status_code = status.HTTP_400_BAD_REQUEST, detail = \"nonce must be base64url\"\n        )\n    if not 16 <= len(raw) <= 128:\n        raise HTTPException(\n            status_code = status.HTTP_400_BAD_REQUEST, detail = \"nonce must decode to 16-128 bytes\"\n        )\n    # The address + port the connection actually landed on, from the socket\n    # (request.scope is getsockname, so it is the real local address even when\n    # bound to 0.0.0.0), never the client-controlled Host header.\n    server = request.scope.get(\"server\") or (\"\", 0)\n    host = server[0] or \"\"\n    port = server[1] if server[1] is not None else 0\n    return {\"proof\": storage.compute_identity_proof(raw, host, port)}\n\n\n@router.get(\"/status\", response_model = AuthStatusResponse)\nasync def auth_status() -> AuthStatusResponse:\n    \"\"\"Auth initialization state; ``default_username`` is exposed for first-boot UI prefill only.\"\"\"\n    return AuthStatusResponse(\n        initialized = storage.is_initialized(),\n        default_username = storage.DEFAULT_ADMIN_USERNAME,\n        requires_password_change = storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME)","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/auth.py#L390-L426","documentation":"After decoding, the /identity nonce must be between 16 and 128 bytes; anything shorter or longer returns HTTP 400 'nonce must decode to 16-128 bytes'. The floor prevents trivially guessable nonces (replay/forgery risk) and the ceiling caps HMAC input size. Length is checked on the decoded bytes, not the encoded string length.","triggerScenarios":"Sending a base64url nonce that decodes to fewer than 16 bytes (e.g. a 8-byte random value) or more than 128 bytes (e.g. a UUID concatenated with a timestamp and extra entropy).","commonSituations":"Clients using short fixed strings or 1-2 word tokens as the nonce; over-enthusiastic clients concatenating multiple entropy sources past 128 bytes; tests reusing a hardcoded tiny nonce.","solutions":["Generate the nonce from 16-128 raw bytes: `base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip('=')`","If the nonce check fails, log the decoded byte length client-side to confirm it is in range","For tests, use a fixed but in-range nonce (e.g. 32 bytes) rather than a short placeholder"],"exampleFix":"# before\nresp = client.get(\"/auth/identity\", params={\"nonce\": base64.urlsafe_b64encode(b\"1234567\").decode()})  # 7 bytes\n# after\nresp = client.get(\"/auth/identity\", params={\"nonce\": base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip(\"=\")})","handlingStrategy":"validation","validationCode":"import base64, os, secrets\n\ndef make_nonce(nbytes: int = 32) -> str:\n    assert 16 <= nbytes <= 128\n    return base64.urlsafe_b64encode(secrets.token_bytes(nbytes)).decode().rstrip(\"=\")","typeGuard":null,"tryCatchPattern":"r = client.get(\"/auth/identity\", params={\"nonce\": nonce})\nif r.status_code == 400 and \"16-128 bytes\" in r.json()[\"detail\"]:\n    nonce = make_nonce(32)\n    r = client.get(\"/auth/identity\", params={\"nonce\": nonce})","preventionTips":["Standardize on 32-byte nonces client-side","Check decoded length, not encoded string length, when debugging"],"tags":["http","auth","nonce","validation"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}