Ahhh I think I see what's going on. The s3 buckets can be restricted so that only a particular user can do things if the object path starts with private/{user cognito ID}. The docs note that this is the sub here https://docs.amplify.aws/lib/storage/configureaccess/q/platform/js/. The access for this is determined by what's passed in the Authorization header (screenshot attached #1), which by default is the Access JWT (not the ID token).
You could replace it with the ID JWT by putting this where relevant:
Amplify.configure({
...config,
graphql_headers: async () => ({
Authorization: (await Auth.currentSession()).getIdToken().getJwtToken(),
}),
});
and it's as secure as the normal access JWT, containing a verified signature as well. Its actually provided in the same Cognito auth request that provides the access token (screenshot attached #2).
However, I don't think you need to do this.
The ID token is a JWT provided by Cognito when the user first authenticates (or refreshes their token, or a few other events). The token is just signed (i.e. it's got a bit at the end saying it's legit and from Cognito), encoded data about the user and their current session. You can actually go here https://jwt.io/ to see what's included.
Part of that token is the Cognito user ID (the "sub"), and this is how all the Amplify backend services automatically determine authorization and ownership of stuff. But both the Access token and the ID token have that particular bit of info.
To prove I couldn't just swap the payload with a modified one, I tried it using the legit payload of another user, leaving the token's header and signature untouched, and included that in the header of a request that should have been sent to a custom Lambda GraphQL resolver (meaning it had to go through AppSync like any other GraphQL request before it could hit my Lambda function). However, it never made it to my Lambda function, because I couldn't fake the signature and was rejected with a 401 response (screenshot attached #3).
If you're curious, I use the ID token, because I wanted to include extra information that I use for permissions relating to GraphQL queries, since you can add custom stuff in the JWT payload if you use the PreTokenGeneration Cognito trigger. And this is the recommended approach in their docs here https://docs.amplify.aws/cli-legacy/graphql-transformer/auth/#custom-claims. But that's for my use case. Since it sounds like you just need the Cognito ID, I don't think you need to do that.