aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.42k stars 2.12k forks source link

identityPool guest data access fails for authenticated users when amplify ssr is true #13655

Open leonanderson88 opened 1 month ago

leonanderson88 commented 1 month ago

Environment information

System:
  OS: macOS 14.5
  CPU: (16) arm64 Apple M3 Max
  Memory: 2.39 GB / 64.00 GB
  Shell: /bin/zsh
Binaries:
  Node: 22.5.1 - ~/.nvm/versions/node/v22.5.1/bin/node
  Yarn: undefined - undefined
  npm: 10.8.2 - ~/.nvm/versions/node/v22.5.1/bin/npm
  pnpm: undefined - undefined
NPM Packages:
  @aws-amplify/auth-construct: 1.2.0
  @aws-amplify/backend: 1.0.4
  @aws-amplify/backend-auth: 1.1.0
  @aws-amplify/backend-cli: 1.2.1
  @aws-amplify/backend-data: 1.1.0
  @aws-amplify/backend-deployer: 1.0.2
  @aws-amplify/backend-function: 1.3.0
  @aws-amplify/backend-output-schemas: 1.1.0
  @aws-amplify/backend-output-storage: 1.0.2
  @aws-amplify/backend-secret: 1.0.0
  @aws-amplify/backend-storage: 1.0.4
  @aws-amplify/cli-core: 1.1.1
  @aws-amplify/client-config: 1.1.1
  @aws-amplify/deployed-backend-client: 1.1.0
  @aws-amplify/form-generator: 1.0.0
  @aws-amplify/model-generator: 1.0.2
  @aws-amplify/platform-core: 1.0.3
  @aws-amplify/plugin-types: 1.1.0
  @aws-amplify/sandbox: 1.1.1
  @aws-amplify/schema-generator: 1.2.0
  aws-amplify: 6.4.3
  aws-cdk: 2.150.0
  aws-cdk-lib: 2.150.0
  typescript: 5.5.4
AWS environment variables:
  AWS_STS_REGIONAL_ENDPOINTS = regional
  AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1
  AWS_SDK_LOAD_CONFIG = 1
No CDK environment variables

Data packages

onlihoney.com.au@0.1.0 /Users/leonanderson/Sites/onlihoney.com.au
├─┬ @aws-amplify/backend-cli@1.2.1
│ └─┬ @aws-amplify/schema-generator@1.2.0
│   └── @aws-amplify/graphql-schema-generator@0.9.3
└─┬ @aws-amplify/backend@1.0.4
  └─┬ @aws-amplify/backend-data@1.1.0
    └── @aws-amplify/data-construct@1.9.3

Description

Current calls and issues

Data set up with access pattern.

allow.guest().to("read"),
allow.authenticated("identityPool").to("read"),
allow.groups(["ADMINS"])

And call to data from front end home page

Amplify.configure(outputs, { ssr: true });

const client = generateClient({ authMode: "identityPool" });

Unauthenticated (guest) user can access data. Authenticated user (Signed in) cannot access data. Receive error:

"UnauthorizedException: Unknown error\n    at parseJsonError (webpack-internal:///(app-pages-browser)/./node_modules/@aws-amplify/core/dist/esm/clients/serde/json.mjs:34:19)\n    at async parseRestApiServiceError (webpack-internal:///(app-pages-browser)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/serviceError.mjs:26:28)\n    at async job (webpack-internal:///(app-pages-browser)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/createCancellableOperation.mjs:35:23)\n    at async GraphQLAPIClass._graphql (webpack-internal:///(app-pages-browser)/./node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs:265:44)\n    at async _list (webpack-internal:///(app-pages-browser)/./node_modules/@aws-amplify/data-schema/dist/esm/runtime/internals/operations/list.mjs:32:16)\n    at async fetchProducts (webpack-internal:///(app-pages-browser)/./src/components/shoppingCart/ProductCards.js:27:48)"

With recovery suggestion:

"If you're calling an Amplify-generated API, make sure to set the "authMode" in generateClient({ authMode: '...' }) to the backend authorization rule's auth provider ('apiKey', 'userPool', 'iam', 'oidc', 'lambda')"

Note: No option listed for identityPool

This issue does not present if SSR True flag is not used:

Amplify.configure(outputs);

However SSR is needed for my use case as I have API routes with SSR function calls to mutations etc.

Expected behaviour.

ADMINS can read,write,delete data when calls to Data via userPool are made: Works Authenticated users can read data when calls to Data via identityPool are made: Error Guest users can read data when calls to Data via identityPool are made: Works

leonanderson88 commented 1 month ago

Access to data by both guest and authenticated users works via:

allow.publicApiKey().to(["read"])

However this is not recommended for production environments based on documentation: https://docs.amplify.aws/nextjs/build-a-backend/data/customize-authz/

AnilMaktala commented 1 month ago

Hey,👋 thanks for raising this! I'm going to transfer this over to our JS repository for better assistance 🙂

chrisbonifacio commented 1 month ago

Hi @leonanderson88 👋 thanks for raising this issue. Before I reproduce this issue, can you please check the request details of the requests being made with SSR enabled vs disabled? I'm curious if the Authorization headers are any different between the two requests.

For a bit more context, the difference between SSR true/false is that, when disabled (default), credentials are stored in localStorage. When enabled, credentials are stored in cookies so they can be consumed on the server side and used to authorize requests.

Also, can you confirm whether those requests are being made on the client or server side?

Lastly, if you haven't already come across this page in the docs please refer to it for server side data. Connecting to data in a server context requires a particular type of client depending on whether you are using middleware or not:

https://docs.amplify.aws/react/build-a-backend/data/connect-from-server-runtime/nextjs-server-runtime/

leonanderson88 commented 1 month ago

Hi @chrisbonifacio Thank you for clarifying how SSR credentials are stored and transported. I was aware of the cookies but wasn't aware that local storage was used for client only.

There are only slight differences in the authorisation headers, namely the signature is different. But the structure is the same. X-Amz-Security-Token is set Authorisation header is set the same structure with only signature differences. I assume this different on each request.

The request that is failing is on the client side. In my use case, users land on a public front page of the website and the client makes a call to data to show products.

The SSR calls I am making are from my /api routes for mutations and queries that may or may not come from authenticated users. They make calls using a cookies cookieBasedClient.

chrisbonifacio commented 1 month ago

Hi @leonanderson88 , I'm trying to reproduce this issue but so far I am not able to.

I'm configuring Amplify with ssr:true in the root layout (client component). I have two pages, one that renders only a client component and one that renders a server component. Both pages are able to read from the model which should be protected by identityPool auth, both guest and authenticated.

Here's my repro app: https://github.com/chrisbonifacio/amplify-next-isr

Is there anything you notice I might be doing differently from your app?

Or, if you can share a sample app that reproduces the issue please do.

In any case, it seems if Amplify is configured with ssr: true on the client side, server side requests should not be affected. It's not currently clear to me why the behavior would change, especially if both requests are being signed with IAM roles as the headers you mentioned would suggest.

My best guess, judging from the error stack trace and it mentioning parseJsonError from serde (serialize/deserialize), that there was an error response from AppSync that was not able to be de-serialized for some reason. Would you be able to share the exact response payload if you can see it in the network activity?

leonanderson88 commented 1 month ago

@robokozo To access data using the allow guest pattern:

allow.guest()

You need to either change the default auth mode to 'identityPool'

export const data = defineData({
  schema,
  authorizationModes: {
    //defaultAuthorizationMode: 'userPool',
    defaultAuthorizationMode: 'identityPool',
  },
})

Or specify this as the auth method on the client when you create it:

const client = generateClient({authMode:'identityPool'});

Which would be in your 01.amplify-apis.client.ts file if you are following the guides on this page for a Nuxt.js setup. https://docs.amplify.aws/nextjs/build-a-backend/data/connect-from-server-runtime/nuxtjs-server-runtime/

The authorisations strategy is set out here: https://docs.amplify.aws/nextjs/build-a-backend/data/customize-authz/

IdentityPool covers Guest and Authenticated checks UserPool covers Owner and Group checks

leonanderson88 commented 1 month ago

@chrisbonifacio

I have downloaded and run your repo to check and uncovered more to clarify this issue.

I will clarify as well what is not working based on your Todos app repo. Using identityPool the following users can list todos:

However, I added some extra resources to more closely model my app. I added the storage definition with a few items that reference groups for access:

import { defineStorage } from "@aws-amplify/backend";

export const storage = defineStorage({
  name: "amplify-next-isr",
  access: (allow) => ({
    "profile-pictures/{entity_id}/*": [
      allow.guest.to(["read"]),
      allow.authenticated.to(["read"]),
      allow.entity("identity").to(["read", "write", "delete"]),
    ],
    "product-pictures/*": [
      allow.guest.to(["read"]),
      allow.authenticated.to(["read"]),
      allow.groups(["ADMINS", "EDITORS"]).to(["read", "write", "delete"]),
    ],
    "media/*": [
      allow.guest.to(["read"]),
      allow.authenticated.to(["read"]),
      allow.groups(["ADMINS", "EDITORS"]).to(["read", "write", "delete"]),
    ]
  }),
});

Amplify Sand Box would not deploy because

No auth IAM role found for "ADMINS".
Resolution: If you are trying to configure UserPool group access, ensure that the group name is specified correctly.

So I added the following to the auth resource.ts

export const auth = defineAuth({
  loginWith: {
    email: true,
  },
  groups: ["ADMINS", "EDITORS"],
});

Now the deploy worked.

This is where I think the problem lies. When I add the user to one of the groups created in auth resource.ts the user cannot view the Todos with the same error unauthenticated I get in my app:

I have tested this behavoir in my app by removing my user from the ADMINS group in Cognito dashboard and then adding them again. I get the same access pattern as above:

Interestingly, I tested creating the groups from Cognito dashboard and access worked:

But I cannot use this method in my app as storage resource won't allow deploy unless groups are defined in the auth resource.ts

In your repo can you try adding the storage resource and updating the auth resource.ts as above. Then add a user to the ADMINS group to recreate the issue.

robokozo commented 1 month ago

Thanks anyway @leonanderson88. I deleted my posted because I tried the allow.authenticated("identityPool").to("read"), and it started working as expected

chrisbonifacio commented 1 month ago

Hi @leonanderson88, according to your latest comment, it seems that users that belong to a group are not being granted access to a model.

Guest - Works Authenticated - Works Authenticated (in ADMINS Group) - ERROR authentication

There are some things that might help diagnose the root cause of the issue. If you could share

  1. your full schema
  2. the way you are generating the server-side cookieBasedClient
  3. any relevant network logs so that we can see how the requests are being authorized or signed
  4. the code that is performing the request so that we can see what authMode is being passed at run time, if any other than the default

My best guess is that because the default authMode is identityPool, and the user is logged in AND belongs to a group, maybe there is some mismatch or mistake in the way the request is being generated.

One thing you can try is to set the authMode to userPool if the authenticated user's access token has a groups claim so that they can have the full permissions of that group rather than trying to authorize the request with the identityPool authMode, which has less permissions.

leonanderson88 commented 1 month ago

Hi @chrisbonifacio

No worries. Here is the code:

  1. https://github.com/leonanderson88/onlihoney.com.au/blob/dev/amplify/data/resource.ts - Schema
  2. https://github.com/leonanderson88/onlihoney.com.au/blob/dev/src/lib/amplify-utils.js - Server Cookie Client
  3. https://github.com/leonanderson88/onlihoney.com.au/blob/dev/localhost.har - HAR network file
  4. https://github.com/leonanderson88/onlihoney.com.au/blob/dev/src/components/shoppingCart/ProductCards.js - Call to data client on client side

You should have complete access to the Repo now.

The behaviour that is odd is that using default identityPool works for users attached to groups as long as the groups were not defined inside the auth/resource.ts file.

I have tried calling the data client with the default set to identityPool and explicitly setting the data client to identity pool in the ProductCards.js component. Neither resolves this issue.

I suppose I could set the call to:

const authSession = await fetchAuthSession();
const userGroups = authSession.tokens.accessToken.payload["cognito:groups"] || [];
const isAdmin = includesAny(userGroups, ["ADMINS", "EDITORS"]); // includesAny = (arr, values) => values.some(v => arr.includes(v));
const client = (isAdmin)?generateClient({authMode:'userPool'}):generateClient({authMode:'identityPool'});

But this seems like a clunky work around to something that is almost working perfectly except for the groups issue. I'd be more inclined to use publicApiKey instead as the above solution has to wait for a response before checking and will add latency to the client call.

chrisbonifacio commented 1 month ago

Thank you for sharing the info, I will take a look and report back with any findings.

const client = (isAdmin)?generateClient({authMode:'userPool'}):generateClient({authMode:'identityPool'});

You don't have to generate another client, you can have a single client with a default authMode of identityPool and import it where you need it. You just have to change the authMode on a request like this:

const { errors, data: newSalary } = await client.models.Salary.create(
  {
    wage: 50.25,
    currency: 'USD'
  },
  {
    authMode: isAdmin ? 'userPool' : 'identityPool',
  }
);
chrisbonifacio commented 3 weeks ago

Hi @leonanderson88 thanks again for sharing the repo. Unfortunately, I ran into issues trying to deploy the same backend. I ran into errors like the following when simply running npx ampx sandbox

CleanShot 2024-08-13 at 12 21 57@2x

It's still not clear to me why you're experiencing this issue and I'm unable to reproduce it on the latest version of aws-amplify.

Have you tried upgrading as well?

If that doesn't help, and if you can, please create a branch that simplifies the backend just enough to reproduce the issue consistently as that would help us identify the problem.

leonanderson88 commented 3 weeks ago

Hi @chrisbonifacio

How strange. I don't get this issue. I will upgrade my lambda functions to NodeJS 20. I'm not sure why the default is 18 for this. It really should be v20 and only v18 if requested.

I have forked your example from before and made the necessary adjustments to reproduce the issue. https://github.com/leonanderson88/amplify-next-isr

Running this you should see that an authenticated user can see the todo item. But if you then go to Cognito console and add that user to the ADMINS group; log them out and log back in; the authenticated user will no longer see the TODO.