REST API broken since last update
-
S shrey marked this topic as a question
-
S shrey marked this topic as a regular topic
-
@nebulon
Compiled with assistance from Claude:
You're right that the auth code path is unchanged. Nothing about how Cubby resolves a credential changed. What changed is how long a token survives, and that's enough to break Bearer-authenticated API use.
Answering your questions directly first.
What I was actually doing, since that seems unexpected
I've been driving Cubby's REST API server-to-server since late June, as a headless file store for an automation pipeline. A dedicated service user owns one folder; that folder has a single read-only share, so published files are readable over a public link while writes stay authenticated. There is no browser and no user session anywhere in this — just an HTTP client sending
Authorization: Bearer <token>, where the token is a row in thetokenstable, resolved bygetUserFromToken→users.getByAccessToken→tokens.get. Not the OpenID access token.Endpoints in use, all with that same Bearer credential:
Purpose Call Write / overwrite a file POST /api/v1/files?path=/home/<folder>/<name>&overwrite=true, raw binary body with the realContent-TypeCreate a folder POST /api/v1/files?path=<path>&directory=trueRead, list, delete, rename GET/DELETE/ relocate on/api/v1/filesPublish a per-file link POST /api/v1/shareswith{ path, readonly: true, ownerUsername: <service user> }Revoke a link DELETE /api/v1/shares?shareId=<id>Public read of a published file GET /api/v1/shares/<shareId>?type=raw— no auth, and this still works fineThat last row is why the app kept looking healthy: reads go through the share and never touch the token. Only the authenticated write path broke.
The failing request, from my client's execution log, 2026-08-08T08:41:15Z:
POST https://<my-cubby>/api/v1/files?path=/home/<folder>/<file>.svg&overwrite=true Content-Type: image/svg+xml Content-Length: 199 Authorization: Bearer <redacted> → 403 {"status": "Forbidden", "message": "not allowed"}Do I see errors in backend or frontend
No. There is no frontend involved — this is server-to-server. And the backend logs nothing, because a request bearing a deleted token isn't treated as an error at all; it's treated as an anonymous request and rejected in path resolution.
debugLogis off withoutDEBUGset, so nothing is written.I ran the same request with no credential at all and got a byte-identical 403. So an expired credential and an absent credential are indistinguishable to the client and invisible on the server. That's what made this hard to diagnose: my first assumption was a permissions or share-ownership problem, and my initial post here blamed the wrong credential type as a result.
What changed
Commit
00c42b6681d57b1a6428ad4e458fa2c1285bc937— "Add wopi lock support for collaboration", 2026-07-21T07:14:42Z.
https://github.com/getcubby/cubby/commit/00c42b6681d57b1a6428ad4e458fa2c1285bc937Alongside the WOPI lock work, it added to
backend/tokens.js:async function cleanup(maxAgeMs) { const cutoff = new Date(Date.now() - maxAgeMs); await database.query('DELETE FROM tokens WHERE created_at < $1', [ cutoff ]); }and wired it up in
backend/server.js:const TOKEN_MAX_AGE_MS = 24 * 60 * 60 * 1000; ... tokens.cleanup(TOKEN_MAX_AGE_MS) // at boot gTokenCleanupInterval = setInterval(() => { tokens.cleanup(TOKEN_MAX_AGE_MS) }, 60 * 60 * 1000); // then hourlycleanup()did not exist before this commit.backend/tokens.jshas nine commits in its entire history; the one before this is "Port to esm" from 2026-02-25, which was mechanical.I understand why it's there —
getHandlecallstokens.add()on every open and every collaborator join, so an office session accumulates rows quickly and something has to reap them. That's reasonable. The issue is thattokensis one undifferentiated table, andadd()writes only(id, username)— there's no column recording what a token is for, socleanup()can only sweep everything. WOPI handles and API tokens are the same row shape, so both go.Which release it shipped in
When (UTC) What 2026-07-02 08:39 package version → 2.15.1 2026-07-21 07:14 00c42b6— reaper added2026-07-21 16:17 dc23534— further WOPI work (touchestokens.js, but does not altercleanup)2026-07-21 16:38 Renovate bumps app digest to 56b14262026-07-21 17:27 package version → 2.16.0 So the reaper is in 2.16.0 and in no earlier release.
Where my token came from, since it's relevant. I minted it on 28 June by inserting the row directly, inside the container via
cloudron exec, with a small script that didcrypto.randomBytes(32).toString('hex')and thenINSERT INTO tokens (id, username) VALUES ($1, $2). That is the same statementtokens.add()runs, so the row is indistinguishable from one issued through the mobile flow — but I should be straight that I put it there myself rather than obtaining it through a supported path. I went that way because at the time the source had no token TTL anywhere in it, and there was no other non-interactive way to get a credential.I'm reporting it because two things behind it aren't specific to me at all: the 403 that should be a 401, and a reaper that can't tell one kind of token from another.
20 days, 12 hours of continuous validity on a single token, with timestamps on both ends. Whatever the intended lifetime is now, nothing was expiring tokens before 2.16.0.
My client's execution history brackets it the same way. Successful writes on:
- 1 July — 5 executions
- 7 July — 7 executions
- 18 July — 5 executions, last at 2026-07-18T18:08:59Z
Then the pipeline sat idle. The next write attempt was 2026-08-08T08:41:15Z — 21 days later, across the 2.16.0 boundary — and it 403'd, as did every attempt after it until I moved to WebDAV on 9 August. There are no executions in the gap, so this isn't a case of it degrading unnoticed; the first use after the update simply failed.
Why the response is 403 and not 401
backend/routes/files.js,add():const subject = await files.translateResourcePath(req.user?.username, filePath); if (!subject) return next(new HttpError(403, 'not allowed')); // line 33-34 if (subject.share?.readonly) return next(new HttpError(403, 'share is read-only')); if (!subject.share && !req.user) return next(new HttpError(401, 'not allowed')); // line 37and
backend/files.js:// only shares may have optional auth if (resource !== 'shares' && !username) return null;For a
/home/...path with no resolved user,translateResourcePathreturnsnull, so line 34 fires and the 401 on line 37 is unreachable. The 401 you already wrote for exactly this case can never be reached on/homepaths — it only applies to share paths.Swapping those two checks (or returning 401 when
!username && resource !== 'shares') would turn this class of failure from silent into immediately diagnosable, independently of anything about token lifetime.One thing worth checking on your side
The cutoff is measured from mint, not from last use —
tokens.getis a plainSELECTand never touchescreated_at, so there's no sliding window. A token dies 24 hours after it was issued no matter how actively it's being used.That applies to mobile tokens too.
POST /api/v1/mobile/code-to-tokenreturns:next(new HttpSuccess(200, { token: apiToken, user: { username, email, displayName, admin } }));— no expiry field, no refresh token, and no refresh path on that endpoint. So server-side, a token issued to
org.getcubby.appstops resolving within 24 hours of issue, where before 2.16.0 it didn't expire at all.I can't see the Android app's source, so I don't know what it does when its token stops working — if it silently re-runs the pairing flow against a live IdP session, users may never notice. Flagging it in case that path isn't as silent as it needs to be now.
What I'd ask for
- Record a token type at mint time and have
cleanup()filter on it, so the WOPI sweep only reaps WOPI tokens. If a blanket 24-hour lifetime on all tokens is intended, that's fine — but it needs a changelog line and a doc note. The 2.16.0 entry lists the activity log, file drop, office collaboration, recents fixes and dependencies; nothing about tokens. - Return 401 when a credential is presented and doesn't resolve. Three-line change, and it's correct HTTP regardless of the rest. WebDAV already gets this right —
webdav.jssendsWWW-Authenticate: Basic realm="Cubby"on an unauthenticated request. - A supported way to obtain a durable API credential. Right now
tokens.add()is reachable only from the mobile OIDC exchange and the WOPI flow; the browser UI has used cookie sessions since 0.7.0. So there's no non-interactive way to get a token, andPOST /api/v1/sharesisisAuthenticated, meaning share creation has no non-interactive path at all.verifyCloudronCredentialsalready exists inwebdav.jsand works — accepting App Passwords via Basic auth on/api/v1/*too would solve this cleanly, if that's less work than it looks from outside.
I've moved my file operations to WebDAV with a Cloudron App Password, which is durable and has no token involved, so I'm not blocked. Reporting it because the silent-403 behaviour will catch others the same way, and because of the mobile app question above.
Reproducing
Reaper history, no instance needed:
git clone --filter=blob:none https://github.com/getcubby/cubby.git && cd cubby git log -S "TOKEN_MAX_AGE_MS" --date=iso --pretty="%ad %H %s" git log --follow --date=iso --pretty="%ad %h %s" -- backend/tokens.jsSilent 403, any instance — no credential at all:
curl -s -w '\n%{http_code}\n' -X POST \ 'https://<cubby>/api/v1/files?path=/home/x&directory=true' -
Please do not dump AI output directly into the forum.
From what I got there is that the root cause is that you are createing the tokens via direct database access. That is bound to break eventually when we restructure internals. I think the correct solution would be to provide a way to create and manage api tokens via the UI. However first we have to see if this is an actual use-case for cubby to be honest. Maybe you can describe the usecase you have here as for example the mobile app we are working on on the side uses the OpenID login flow to obtain a token. That would be the best way for other UIs using the API, but not sure if this applies to your usage?
-
Please do not dump AI output directly into the forum.
From what I got there is that the root cause is that you are createing the tokens via direct database access. That is bound to break eventually when we restructure internals. I think the correct solution would be to provide a way to create and manage api tokens via the UI. However first we have to see if this is an actual use-case for cubby to be honest. Maybe you can describe the usecase you have here as for example the mobile app we are working on on the side uses the OpenID login flow to obtain a token. That would be the best way for other UIs using the API, but not sure if this applies to your usage?
Please do not dump AI output directly into the forum.
Fair enough.
My use-case: Headless, auth enabled filestore with link-based sharing mechanism, with a decent admin GUI. Cubby seems to fulfil most of this, except for the 'headless automation as a first-class feature' aspect. Basically, upload files/folders, and generate unique, revocable and individual share links for them, as required. Currently, i'm managing those links in another online spreadsheet tool.
Btw, i got around to Cubby after i searched for a solution in the Cloudron App Store, and couldn't identify any useful-enough option.
OIDC is going to be tricky due to the interactive nature of the auth mechanism, assuming that the access token issued there are short-lived.
So yeah, an ideal solution would be a long-lived access token/API key that allows for full programmatic access to the REST API
-
Hello @shrey
So what you need is a way for users in Cubby to create auth tokens that can be used with the API, right?
This would be a feature request.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login