Skip to content

Conversation

@guillett
Copy link
Contributor

@guillett guillett commented May 14, 2025

Context and related issues

I built a custom widget to upload documents.

It relies on access tokens based requests.

Access token generated unauthenticated requests (logged in as anonymous) which leads to impossible attachment manipulation. (Anonymous user "made" the upload, but I want to add it to a row as user X, user X is not authorized, because of a check that limits who can access a yet-unreferenced attachment.)


To replicate the issue

  1. have a table with an attachment column
  2. have a row in that table
  3. Use an access token (with write access) to add an attachment (and get an attachment id back from grist)
  4. Use that same access token to link the new attachment to the row (try to add the id in the correct column)

1, 2 and 3 are ok but 4 fails. Well actually it fails when you are not the owner of the doc (because as a owner you have all the rights).

That's the reason why I would like the access token to authenticate as the user of the widget and therefore attach user information to the request in such a case.

(Many thanks to @fflorent)


To give more context, here is a working exemple. It highlights a slightly different issue.
https://grist.numerique.gouv.fr/o/docs/daZkkLK5fpoZ/Demo-limit-widget

https://docs.getgrist.com/c3nzPGK4VHbZ/Demo-limit-widget1

Here is the custom widget code.

I created this custom widget to show the difference between record addition via the widget and via the access token.

As an unlogged user both buttons generate record related to [email protected].
As a logged-in user records added with the widget are correcly linked but via the access token, records are linked to [email protected].

Proposed solution

Attach the user after verification that the access is legitimate at the document level.

Has this been tested?

  • 👍 yes, I added tests to the test suite

const tokenResult = (await cliEditor.send("getAccessToken", 0, {readOnly: false})).data;
const i7 = await postAttachment(editor, docId, tokenResult.token, "data7", "7.txt");
await assert.isFulfilled(getAttachment(owner, docId, i7));
//await assert.isFulfilled(getAttachment(editor, docId, i7));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be discussed, it fails because it is impossible to set maybeNew from the web api. To me, in should be made possible. I can open another PR for that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure of what you mean here, especially about the maybeNew. Can you provide more context please?

Copy link
Contributor Author

@guillett guillett Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was, to me, counterintuitive that as the editor uploader of an attachment I can't access it after upload.
For the attachment to be accessible to me, I need to add it to a row accessible to me (cf lines below this comment).

I refer to the maybeNew because it is linked to a specific permissive use case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this seems fairly straightforward to add support for, although I wonder what use-cases there would be apart from testing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the commented out line, it would be good to add a comment explaining why it is commented out.

@hexaltation hexaltation requested a review from paulfitz June 11, 2025 14:18
@hexaltation hexaltation moved this from In Progress to Needs feedback in French administration Board Jun 11, 2025
@hexaltation hexaltation moved this from Needs feedback to Needs Internal Feedback in French administration Board Jun 18, 2025
@guillett guillett force-pushed the access-token-document-level-auth branch from 4a3f00b to ffe3068 Compare July 24, 2025 15:33
}
const userId = tokenObj.userId;
const user = await dbManager.getUser(userId);
setRequestUser(mreq, dbManager, user!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If, given an access token, you fully grant the request the identity of the user, it is important to check that this does not accidentally permit more than you intend.

For example, can you add a test showing that a request accessing /api/profile/apiKey using a token will be denied?

#1512 (comment)

Copy link
Contributor Author

@guillett guillett Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this is already tested with https://github.com/gristlabs/grist-core/blob/main/test/server/lib/AccessTokens.ts#L112-L118 (with a slightly different context/check).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please indulge me? The family of endpoints having to do with documents and the endpoints that don't deal with documents operate quite differently. I see a check in

if (tokenObj) {
// Sanity check: does the current document match the document the token is
// for? If not, fail.
if (!mreq.docAuth.docId || mreq.docAuth.docId !== tokenObj.docId) {
throw new ApiError('token misuse', 401);
}
that looks good in the context of documents. But what happens otherwise? What stops /api/profile/apiKey from returning a value once the user has been vouched for as your code does? Maybe there is something. It is worth a test. If it is already blocked, great, if not, you'd just need to reject use of an access token outside of the context of documents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't mean not to do what is important here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to add a test to ensure resquests outside of the document endpoint would fail but they actually do not generate responses with error code because of anonymous access. cf. https://github.com/gristlabs/grist-core/pull/1614/files#diff-1aeb508e4b5874641fe6a997ac27fa7315a9c05513aa0695bb889b9c5de4ca49R120

I added a test that ensure that the request is considered from a anonymous user and not the use linked to the provided token.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it is be enough for you? Let me know what I can add to this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @guillett, it all looks ok. I hadn't carefully checked where your intervention was placed.

@guillett guillett force-pushed the access-token-document-level-auth branch from 5a287e0 to 6db6701 Compare October 1, 2025 11:21
Copy link
Collaborator

@fflorent fflorent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my prospective, LGTM

@hexaltation hexaltation moved this from In Progress to Needs feedback in French administration Board Oct 1, 2025
Copy link
Member

@paulfitz paulfitz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good, thanks @guillett. One small request.

const tokenResult = (await cliEditor.send("getAccessToken", 0, {readOnly: false})).data;
const i7 = await postAttachment(editor, docId, tokenResult.token, "data7", "7.txt");
await assert.isFulfilled(getAttachment(owner, docId, i7));
//await assert.isFulfilled(getAttachment(editor, docId, i7));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this seems fairly straightforward to add support for, although I wonder what use-cases there would be apart from testing.

const tokenResult = (await cliEditor.send("getAccessToken", 0, {readOnly: false})).data;
const i7 = await postAttachment(editor, docId, tokenResult.token, "data7", "7.txt");
await assert.isFulfilled(getAttachment(owner, docId, i7));
//await assert.isFulfilled(getAttachment(editor, docId, i7));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the commented out line, it would be good to add a comment explaining why it is commented out.

}
const userId = tokenObj.userId;
const user = await dbManager.getUser(userId);
setRequestUser(mreq, dbManager, user!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am uncomfortable with this setRequestUser call. It changes the request object, and makes it hard to know elsewhere which user the request object will contain. An accessToken should allow a limited scope to act as the given user, but so much code relies on the user in the request that it's hard to know what might unintentionally get authorized.

I need more time to look into this.

(One idea to look into is adding info for the effective user (user/userId/fullUser) as an extra property to mreq or to mreq.docAuth, and then update just the code that should be affected by accessToken to pay attention to that.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried the last idea I mentioned (adding a property to mreq.docAuth) and it seems to work well, at least for the test cases in this PR: https://gist.github.com/dsagal/ebd7a0b5e8bce37caae4a335cf744caa.

What do you think? @paulfitz, interested in your feedback too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, using mreq.docAuth feels a clear way to do it.

@fflorent fflorent moved this from Needs feedback to In Progress in French administration Board Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants