{"record":{"id":"25f2f9660302a7e4","repo":"nextcloud/server","slug":"cannot-authenticate-over-ajax-calls-25f2f9","errorCode":null,"errorMessage":"Cannot authenticate over ajax calls","messagePattern":"Cannot authenticate over ajax calls","errorType":"http","errorClass":"Sabre\\DAV\\Exception\\NotAuthenticated","httpStatus":401,"severity":"error","filePath":"apps/dav/lib/Connector/LegacyPublicAuth.php","lineNumber":84,"sourceCode":"\n\t\t\\OC_User::setIncognitoMode(true);\n\n\t\t// check if the share is password protected\n\t\tif ($share->isPasswordProtected()) {\n\t\t\tif ($share->getShareType() === IShare::TYPE_LINK\n\t\t\t\t|| $share->getShareType() === IShare::TYPE_EMAIL\n\t\t\t\t|| $share->getShareType() === IShare::TYPE_CIRCLE) {\n\t\t\t\tif ($this->shareManager->checkPassword($share, $password)) {\n\t\t\t\t\treturn true;\n\t\t\t\t} elseif ($this->session->exists(PublicAuth::DAV_AUTHENTICATED)\n\t\t\t\t\t&& $this->session->get(PublicAuth::DAV_AUTHENTICATED) === $share->getId()) {\n\t\t\t\t\treturn true;\n\t\t\t\t} else {\n\t\t\t\t\tif (in_array('XMLHttpRequest', explode(',', $this->request->getHeader('X-Requested-With')))) {\n\t\t\t\t\t\t// do not re-authenticate over ajax, use dummy auth name to prevent browser popup\n\t\t\t\t\t\thttp_response_code(401);\n\t\t\t\t\t\theader('WWW-Authenticate: DummyBasic realm=\"' . $this->realm . '\"');\n\t\t\t\t\t\tthrow new \\Sabre\\DAV\\Exception\\NotAuthenticated('Cannot authenticate over ajax calls');\n\t\t\t\t\t}\n\n\t\t\t\t\t$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t} elseif ($share->getShareType() === IShare::TYPE_REMOTE) {\n\t\t\t\treturn true;\n\t\t\t} else {\n\t\t\t\t$this->throttler->registerAttempt(self::BRUTEFORCE_ACTION, $this->request->getRemoteAddress());\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t\treturn true;\n\t}\n\n\tpublic function getShare(): IShare {\n\t\tassert($this->share !== null);\n\t\treturn $this->share;","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/nextcloud/server/blob/ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3/apps/dav/lib/Connector/LegacyPublicAuth.php#L66-L102","documentation":"Raised in LegacyPublicAuth::auth while authenticating a public DAV share (password-protected link/email/circle share): the supplied password failed checkPassword(), the session is not already authenticated for this specific share id (PublicAuth::DAV_AUTHENTICATED), and the request carries X-Requested-With containing XMLHttpRequest. The server responds 401 with a DummyBasic WWW-Authenticate header (to suppress the browser's native basic-auth popup) and then throws Sabre\\DAV\\Exception\\NotAuthenticated. It exists so AJAX callers get a clean 401 instead of a popup storm.","triggerScenarios":"Browser JS (marked X-Requested-With: XMLHttpRequest) hitting remote.php/dav/public-files/<token> for a password-protected share with a wrong or missing password while the session has not authenticated that share. Non-AJAX callers with a bad password instead get a bruteforce-throttled false return and a standard Basic 401 challenge.","commonSituations":"Share password changed after the page loaded; JS retries with a stale/empty password; custom web apps embedding public share DAV endpoints without first completing the share-password step; password-protected email shares opened via XHR.","solutions":["Supply the correct share password (basic auth username is ignored for link shares; only the password matters)","Authenticate once through the web UI share page so the session stores DAV_AUTHENTICATED for that share, then retry the DAV call","In custom JS, complete the share password flow before issuing DAV requests and treat the DummyBasic 401 as terminal instead of re-prompting in a loop","If calls are not intentionally AJAX, remove/adjust the X-Requested-With header to get the regular Basic challenge path"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// JS: verify share password through the web UI share endpoint BEFORE any DAV call\nconst check = await fetch(`/s/${token}/authenticate`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'requesttoken': oc_requesttoken },\n  body: 'password=' + encodeURIComponent(password),\n});\nif (!check.ok) {\n  showPasswordPrompt(); // never proceed to DAV with a bad password\n}","typeGuard":null,"tryCatchPattern":"try {\n    await davFetch(`/public-files/${token}/file.txt`);\n} catch (e) {\n    if (e instanceof Error && e.message.includes('Cannot authenticate over ajax calls')) {\n        // DummyBasic 401: password wrong/missing and session not authorized for this share.\n        // Terminal — prompt the user, do NOT retry with the same credentials.\n        showSharePasswordPrompt(token);\n    } else {\n        throw e;\n    }\n}","preventionTips":["Complete the share-password step once so the session carries DAV_AUTHENTICATED for that share id","Never retry DAV requests in a loop after a DummyBasic 401; it will not succeed and feeds bruteforce throttling","Send the correct share password via basic auth (username is ignored for link shares)","Watch for changed share passwords after page load in long-lived SPAs"],"tags":["dav","public-share","authentication","ajax","http-401"],"backgroundTag":"public-share-auth-failed","analyzedSha":"ecdeb153ffdf227235c9a7e2d13dbe0f9c817bc3","analyzedAt":"2026-08-17T01:36:13.386Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}