{"record":{"id":"8ac31574f14187d2","repo":"unslothai/unsloth","slug":"nonce-must-be-base64url","errorCode":null,"errorMessage":"nonce must be base64url","messagePattern":"nonce must be base64url","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"studio/backend/routes/auth.py","lineNumber":404,"sourceCode":"        # A successful login resets the IP's throttle, including any overflow it\n        # accumulated during saturation (drop only this IP's entry, so a\n        # shard-mate's throttle is untouched).\n        _overflow_shard(ip).pop(ip, None)\n\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.\"\"\"","sourceCodeStart":386,"sourceCodeEnd":422,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/auth.py#L386-L422","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Encode the nonce with URL-safe base64: `base64.urlsafe_b64encode(os.urandom(32)).decode().rstrip('=')` on the client","If sending standard base64, convert it: replace '+' with '-' and '/' with '_' before sending","Verify the nonce string contains only [A-Za-z0-9_-] characters before making the request"],"exampleFix":"# before\nimport secrets\nresp = client.get(\"/auth/identity\", params={\"nonce\": secrets.token_hex(32)})\n# after\nimport base64, os\nresp = client.get(\"/auth/identity\", params={\"nonce\": base64.urlsafe_b64encode(os.urandom(32)).decode().rstrip(\"=\")})","handlingStrategy":"validation","validationCode":"import base64, re\n\ndef is_base64url(s: str) -> bool:\n    return bool(re.fullmatch(r\"[A-Za-z0-9_-]+={0,2}\", s)) and _can_decode(s)\n\ndef _can_decode(s: str):\n    try:\n        base64.urlsafe_b64decode(s + \"=\" * (-len(s) % 4))\n        return True\n    except Exception:\n        return False","typeGuard":null,"tryCatchPattern":"r = client.get(\"/auth/identity\", params={\"nonce\": nonce})\nif r.status_code == 400 and \"base64url\" in r.json()[\"detail\"]:\n    nonce = base64.urlsafe_b64encode(os.urandom(32)).decode().rstrip(\"=\")\n    r = client.get(\"/auth/identity\", params={\"nonce\": nonce})","preventionTips":["Always generate nonces with base64.urlsafe_b64encode on os.urandom/secrets","Never hand-type nonces in tests; use a fixed in-range encoded value"],"tags":["http","auth","base64","validation"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}