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.4k stars 2.11k forks source link

Error: "No Credentials" on GraphQL Request Via Amplify in NextJs 14 (with App Router) SSR for Stripe Webhook #12867

Closed CooperCodeComposer closed 5 months ago

CooperCodeComposer commented 6 months ago

Before opening, please confirm:

JavaScript Framework

Next.js

Amplify APIs

Authentication, GraphQL API

Amplify Version

v6

Amplify Categories

api

Backend

Amplify CLI

Environment information

``` # Put output below this line System: OS: macOS 13.6 CPU: (10) arm64 Apple M1 Pro Memory: 839.03 MB / 32.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 16.14.2 - ~/.nvm/versions/node/v16.14.2/bin/node Yarn: 1.22.18 - ~/.nvm/versions/node/v16.14.2/bin/yarn npm: 9.6.7 - ~/.nvm/versions/node/v16.14.2/bin/npm Browsers: Chrome: 120.0.6099.234 Safari: 17.0 npmPackages: @ampproject/toolbox-optimizer: undefined () @apollo/client: ^3.8.8 => 3.8.8 @apollo/client/cache: undefined () @apollo/client/core: undefined () @apollo/client/dev: undefined () @apollo/client/errors: undefined () @apollo/client/link/batch: undefined () @apollo/client/link/batch-http: undefined () @apollo/client/link/context: undefined () @apollo/client/link/core: undefined () @apollo/client/link/error: undefined () @apollo/client/link/http: undefined () @apollo/client/link/persisted-queries: undefined () @apollo/client/link/remove-typename: undefined () @apollo/client/link/retry: undefined () @apollo/client/link/schema: undefined () @apollo/client/link/subscriptions: undefined () @apollo/client/link/utils: undefined () @apollo/client/link/ws: undefined () @apollo/client/react: undefined () @apollo/client/react/components: undefined () @apollo/client/react/context: undefined () @apollo/client/react/hoc: undefined () @apollo/client/react/hooks: undefined () @apollo/client/react/parser: undefined () @apollo/client/react/ssr: undefined () @apollo/client/testing: undefined () @apollo/client/testing/core: undefined () @apollo/client/utilities: undefined () @apollo/client/utilities/globals: undefined () @aws-amplify/adapter-nextjs: ^1.0.10 => 1.0.10 @aws-amplify/adapter-nextjs/api: undefined () @aws-amplify/adapter-nextjs/data: undefined () @babel/core: undefined () @babel/runtime: 7.22.5 @chakra-ui/icons: ^2.1.1 => 2.1.1 @chakra-ui/react: ^2.8.2 => 2.8.2 @edge-runtime/cookies: 4.0.2 @edge-runtime/ponyfill: 2.4.1 @edge-runtime/primitives: 4.0.2 @emotion/react: ^11.11.3 => 11.11.3 @emotion/styled: ^11.11.0 => 11.11.0 @hapi/accept: undefined () @mswjs/interceptors: undefined () @napi-rs/triples: undefined () @next/font: undefined () @next/react-dev-overlay: undefined () @opentelemetry/api: undefined () @segment/ajv-human-errors: undefined () @stripe/react-stripe-js: ^2.4.0 => 2.4.0 @stripe/stripe-js: ^2.3.0 => 2.3.0 @types/node: ^20 => 20.10.6 @types/react: ^18 => 18.2.46 @types/react-dom: ^18 => 18.2.18 @vercel/nft: undefined () @vercel/og: 0.5.15 acorn: undefined () amphtml-validator: undefined () anser: undefined () apollo-link: ^1.2.14 => 1.2.14 (1.2.5) apollo-link-http: ^1.5.17 => 1.5.17 (1.5.8) arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () aws-amplify: ^6.0.10 => 6.0.10 aws-amplify/adapter-core: undefined () aws-amplify/analytics: undefined () aws-amplify/analytics/kinesis: undefined () aws-amplify/analytics/kinesis-firehose: undefined () aws-amplify/analytics/personalize: undefined () aws-amplify/analytics/pinpoint: undefined () aws-amplify/api: undefined () aws-amplify/api/server: undefined () aws-amplify/auth: undefined () aws-amplify/auth/cognito: undefined () aws-amplify/auth/cognito/server: undefined () aws-amplify/auth/enable-oauth-listener: undefined () aws-amplify/auth/server: undefined () aws-amplify/datastore: undefined () aws-amplify/in-app-messaging: undefined () aws-amplify/in-app-messaging/pinpoint: undefined () aws-amplify/push-notifications: undefined () aws-amplify/push-notifications/pinpoint: undefined () aws-amplify/storage: undefined () aws-amplify/storage/s3: undefined () aws-amplify/storage/s3/server: undefined () aws-amplify/storage/server: undefined () aws-amplify/utils: undefined () aws-appsync: ^4.1.9 => 4.1.9 babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 comment-json: undefined () compression: undefined () conf: undefined () constants-browserify: undefined () content-disposition: undefined () content-type: undefined () cookie: undefined () cross-spawn: undefined () crypto-browserify: undefined () css.escape: undefined () data-uri-to-buffer: undefined () debug: undefined () devalue: undefined () domain-browser: undefined () edge-runtime: undefined () eslint: ^8 => 8.56.0 eslint-config-next: 14.0.4 => 14.0.4 events: undefined () find-cache-dir: undefined () find-up: undefined () framer-motion: ^10.17.9 => 10.17.9 fresh: undefined () get-orientation: undefined () glob: undefined () gzip-size: undefined () http-proxy: undefined () http-proxy-agent: undefined () https-browserify: undefined () https-proxy-agent: undefined () icss-utils: undefined () ignore-loader: undefined () image-size: undefined () is-animated: undefined () is-docker: undefined () is-wsl: undefined () jest-worker: undefined () json5: undefined () jsonwebtoken: undefined () loader-runner: undefined () loader-utils: undefined () lodash.curry: undefined () lru-cache: undefined () micromatch: undefined () mini-css-extract-plugin: undefined () nanoid: undefined () native-url: undefined () neo-async: undefined () next: 14.0.4 => 14.0.4 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () platform: undefined () postcss-flexbugs-fixes: undefined () postcss-modules-extract-imports: undefined () postcss-modules-local-by-default: undefined () postcss-modules-scope: undefined () postcss-modules-values: undefined () postcss-preset-env: undefined () postcss-safe-parser: undefined () postcss-scss: undefined () postcss-value-parser: undefined () process: undefined () punycode: undefined () querystring-es3: undefined () raw-body: undefined () react: ^18 => 18.2.0 react-builtin: undefined () react-dom: ^18 => 18.2.0 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-experimental-builtin: undefined () react-icons: ^4.12.0 => 4.12.0 react-is: 18.2.0 react-refresh: 0.12.0 react-server-dom-turbopack-builtin: undefined () react-server-dom-turbopack-experimental-builtin: undefined () react-server-dom-webpack-builtin: undefined () react-server-dom-webpack-experimental-builtin: undefined () regenerator-runtime: 0.13.4 sass-loader: undefined () scheduler-builtin: undefined () scheduler-experimental-builtin: undefined () schema-utils: undefined () semver: undefined () send: undefined () server-only: 0.0.1 setimmediate: undefined () shell-quote: undefined () source-map: undefined () stacktrace-parser: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () stripe: ^14.12.0 => 14.12.0 superstruct: undefined () tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tty-browserify: undefined () typescript: ^5 => 5.3.3 ua-parser-js: undefined () unistore: undefined () util: undefined () vm-browserify: undefined () watchpack: undefined () web-vitals: undefined () webpack: undefined () webpack-sources: undefined () ws: undefined () zod: undefined () npmGlobalPackages: @aws-amplify/cli: 12.10.0 apollo: 2.34.0 avo: 3.2.1 corepack: 0.10.0 graphql: 16.6.0 n: 9.2.0 npm: 9.6.7 yarn: 1.22.18 ```

Describe the bug

I'm trying to setup a route.ts to listen for Stripe webhooks then make a graphql request to log details of the transaction in dynamo db. I've tried many different approaches following fragments of the Amplify documentation, although I can't find any docs that seem to follow this exact use case? Some of the docs refer to the old NextApiResponse as opposed to the NextResponse in the App Router approach.

It sounds like "cookies" from next/header is suppose to include the auth information, but how should this be passed in the graphql query? My cookies are always nil in this ssr route. I'm setting the "ssr" to true when I configure amplify in my layout.tsx.

Help greatly appreciated! Should it be possible to use Amplify with Next 14?

Expected behavior

The most basic functionality of a graphql query to work and be documented.

Reproduction steps

I can use the Stripe CLI tool to trigger the payment_intent.succeeded. My code executes but the graphql query fails with the error "No Credentials".

Code Snippet

// Put your code below this line.
import { NextRequest, NextResponse } from "next/server";
import Stripe from 'stripe';
// import { generateClient } from 'aws-amplify/api';
import { createTransaction } from '../../../graphql/mutations';
import { TransactionInput } from '../../../graphql/API';

// Amplify ssr
import { runWithAmplifyServerContext } from '../../../utils/amplifyServerUtils';
import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/api';
import { cookies } from 'next/headers';
import amplifyConfig from '../../../deployment/amplify-config';

// TODO: put in utils 
export const cookieBasedClient = generateServerClientUsingCookies({
  config: amplifyConfig,
  cookies
});

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  // @ts-ignore
  apiVersion: '2022-11-15',
});

export async function POST(req: NextRequest) {
  if (req.method !== 'POST') {
    return new NextResponse(JSON.stringify({ error: 'Method not allowed' }), {
      status: 405,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }

  // Cookie debug
  const rawCookies = req.headers.get('cookie');
  console.log('Raw Cookie String:', rawCookies);

  const sig = req.headers.get('stripe-signature');
  let event;

  try {
    const body = await readRawBody(req);
    event = stripe.webhooks.constructEvent(body, sig!, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err: any) {
    return new NextResponse(JSON.stringify({ error: `Webhook Error: ${err.message}` }), {
      status: 400,
      headers: {
        'Content-Type': 'application/json',
      },
    });
  }

  switch (event.type) {
    case 'charge.succeeded':
      console.log('Handling charge.succeeded');
      break;
    case 'payment_intent.created':
      console.log('Handling payment_intent.created');
      break;
    case 'payment_intent.canceled':
      console.log('Handling payment_intent.canceled');
      break;
    case 'payment_intent.succeeded':
      console.log('Handling payment_intent.succeeded');

      const paymentIntentSucceeded = event.data.object as Stripe.PaymentIntent;

      console.log('paymentIntentSucceeded = ', paymentIntentSucceeded);

      const transactionInput: TransactionInput = {
        transactionId: paymentIntentSucceeded.id,
        userId: '1234', // TODO: get this
        amount: paymentIntentSucceeded.amount, 
        currency: paymentIntentSucceeded.currency,
        timestamp: new Date().toISOString(),
        status: paymentIntentSucceeded.status, 
        paymentIntentId: paymentIntentSucceeded.id, 
        priceId: 'priceId', // TODO: set this 
      };

      console.log('cookies = ', cookies().getAll());

      try {
        const result = await runWithAmplifyServerContext({
            nextServerContext: { cookies },
            operation: () =>
            cookieBasedClient.graphql({
                    query: createTransaction,
                    variables: {
                      transaction: transactionInput
                    },
                }),
        });
        console.log("createTransaction result transactionId = ", result.data.createTransaction?.transactionId);
    } catch (error) {
        console.log("createTransaction failed with error: ", error);
    }
      break;
    default:
      console.log('Unhandled Stripe webhook event type:', event.type);
      break;
  }

  return new NextResponse(JSON.stringify({ received: true }), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
  });
}

// Helper function to read raw request body
async function readRawBody(req: NextRequest): Promise<string> {
  if (!req.body) {
    throw new Error("Request body is null");
  }

  const reader = req.body.getReader();
  let receivedValue = '';
  let done = false;

  while (!done) {
    const { value, done: readerDone } = await reader.read();
    done = readerDone;
    if (value) {
      receivedValue += new TextDecoder().decode(value, { stream: true });
    }
  }

  return receivedValue;
}

Log output

``` // Put your logs below this line ```

aws-exports.js

No response

Manual configuration

const amplifyConfig: ResourcesConfig = {
  Auth: {
    Cognito: {
      userPoolClientId: config.USER_POOL_CLIENT_ID,
      userPoolId: config.USER_POOL_ID,
      loginWith: { // Optional
        oauth: {
          domain: 'https://my_domain.auth.us-east-1.amazoncognito.com',  
          scopes: ['email', 'openid', 'aws.cognito.signin.user.admin'],
          redirectSignIn: [config.REDIRECT_SIGN_IN],
          redirectSignOut: [config.REDIRECT_SIGN_OUT],
          responseType: 'code',
        },
        username: true, // note: username is their email 
        email: false, // Optional
        phone: false, // Optional
      }
    }
  },
  API: {
    GraphQL: {
      endpoint: config.GRAPHQL_ENDPOINT,
      defaultAuthMode: 'iam',  
      region: config.REGION, // Optional
    }
  },
};

export default amplifyConfig;

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

Tomekmularczyk commented 6 months ago

I have a very related problem. It happens with Nextjs v 14.0.2 and above, it doesn't happen with 14.0.1. This happens when navigating as a signed-in user for the first time after deploying the app.

"@aws-amplify/api": "^5.4.9", "aws-amplify": "^5.3.15",

Error: No api-key configured
    at e.<anonymous> (/var/task/.next/server/chunks/6453.js:112:27361)
    at /var/task/.next/server/chunks/6453.js:75:10081
    at Object.next (/var/task/.next/server/chunks/6453.js:75:10186)
    at /var/task/.next/server/chunks/6453.js:75:9128
    at new Promise (<anonymous>)
    at D (/var/task/.next/server/chunks/6453.js:75:8877)
    at e._headerBasedAuth (/var/task/.next/server/chunks/6453.js:112:26967)
    at e.<anonymous> (/var/task/.next/server/chunks/6453.js:112:29581)
    at /var/task/.next/server/chunks/6453.js:75:10081
    at Object.next (/var/task/.next/server/chunks/6453.js:75:10186)
chrisbonifacio commented 6 months ago

Hi @CooperCodeComposer 👋 thanks for raising this issue.

A few things:

  1. Have you tried defining the client right after where you are configuring Amplify in layout.tsx or in a utility file and importing it into your route handler according to the documentation?

https://docs.amplify.aws/react/build-a-backend/graphqlapi/connect-from-server-runtime/#step-3---call-graphql-api-using-generated-server-api-clients

  1. Since it looks like you're using a route handler you don't need to use the runWithAmplifyServerContext, that's for the req/res based client.

  2. Have you tried logging what credentials are in the cookies? Do they contain the credentials you expect to authorize the graphql operation with?

  3. It looks like your api's default auth mode is iam. Is that the correct auth mode createTransaction is expecting?

CooperCodeComposer commented 6 months ago

Thank you very much for the quick reply @chrisbonifacio ! I appreciate it!

  1. Yes. I’ve tried having the cookieBasedClient in a utils files that’s imported into the route.ts

  2. Thank you for clarifying that I don’t need this runWithAmplifyServerContext

I’ve set it up using this in the route.ts

try {
        // This doesn't work. Fails with "No Credentials" error 
        const result = await cookieBasedClient.graphql({
          query: createTransaction,
                    variables: {
                      transaction: transactionInput
                    },
        })

…however it’s still failing with the “No Credentials” error.

  1. In route.ts I’m logging the cookies. The cookies are always “null” or []
// Cookie debug
  const rawCookies = req.headers.get('cookie');
  console.log('Raw Cookie String:', rawCookies);

  const otherRawCookies = req.cookies.getAll();
  console.log('Other Raw Cookies:', otherRawCookies);

  console.log('cookies from next header = ', cookies().getAll());

This seems like a problem for something called “cookieBasedClient” lol.

Should the cookies in the next/header be automagically set somehow? Maybe I’m missing a step to manually put the necessary credentials in there?

If I’m using “iam” as “defaultAuthMode” does that mean that it’s expecting an idToken and an accessToken from a user session? I thought it would be looking for IAM credentials, like for an IAM role not a specific user?

I think that AppSync is expecting a temporary security credential (access key, secret key, and the security token). Any idea how this could get into the cookies?

I read somewhere (…totally not GPT4…) that:

“With AWS Amplify set up, you don't usually need to worry about manually adding these credentials to your GraphQL requests. Amplify handles the fetching and refreshing of these temporary credentials and includes them in the API calls.”

Is that true?

  1. Yes, I believe that “iam” is the correct role. In AppSync for that project that is included in the code snippet for Amplify setup under the “integrate with your app” section.

Thank you!

CooperCodeComposer commented 6 months ago

I also tried following these doc and using just the const client = generateClient(); and not the cookieBasedClient, but it was the same "No Credentials" error.

https://docs.amplify.aws/nextjs/build-a-backend/troubleshooting/migrate-from-javascript-v5-to-v6/#api-graphql

Should you just be call this once on the client side? I've tried also calling it in the route.ts , but it still didn't solve:

Amplify.configure(amplifyConfig, {
  ssr: true // required when using Amplify with Next.js - it make Amplify store the auth tokens in cookies
});
CooperCodeComposer commented 5 months ago

On day 5 still there were no credentials.

If I call the query from the route.ts file that is called from the Stripe webhook all the cookies are empty and graphql errors with “No Credentials”.

However!

If I call the query from a route.ts called directly from the client I DO have all kinds of Cognito credentials in the cookies from next/header. … however the graphql call still errors saying “No Credentials”.

For example the cookies include things like:

{ name: 'CognitoIdentityServiceProvider.[very-long-string].accessToken', value: ‘very’-long-string, path: '/' },

(also there's the idToken, refreshToken etc).

If there’s a minor problem with the credentials would the cookieBasedClient still error with “No Credentials”?

The IAM role for authenticated users does have permissions to access both appsync graphql and the dynamo db table.

Thanks!

HuiSF commented 5 months ago

Hi @CooperCodeComposer Could you verify a few things for us to investigate?

  1. inspect the http request sent to your API route on the client side, to verify if the cookie header contain the auth tokens

If the cookie header is correct, then it must be that something went wrong on the server side.

  1. It sounds like you have update your implementation based on Chris's suggestion. Could you request your implementation here?

If I call the query from the route.ts file that is called from the Stripe webhook all the cookies are empty and graphql errors with “No Credentials”.

The stripe webhook may create closure that disturbs Next.js's cookies() function to run in a correct context.

CooperCodeComposer commented 5 months ago

Hi @HuiSF

I’ve tried to isolate the problem by making the request inside a normal api route.ts file that the client call. Instead of the web hook route.ts.

With the webhook the whole route.ts is running on the server so none of the debug shows up in Chrome (I’m triggering the hook with the Stripe CLI). I have no idea how you’d get auth creds inside that black box in order to log the transaction with graphql?

Here is a full list of the cookies sent when I trigger it inside a normal api route.ts request. It still fails with “No Credentials”. (I’ve shortened some of the long strings)

CognitoIdentityServiceProvider.6f3uavvmetc.LastAuthUser=1cf94651-dc69-495b-9734-8d09d1146a41; CognitoIdentityServiceProvider.6f3uavvmetc.accessToken=dGltZSI6MT etc… ; CognitoIdentityServiceProvider.6f3uavvmetc.idToken=eyJraW etc…; CognitoIdentityServiceProvider.6f3uavvmetc.refreshToken=eyJjd etc…; CognitoIdentityServiceProvider.6f3uavvmetc.deviceKey=us-east etc…; CognitoIdentityServiceProvider.6f3uavvmetc.deviceGroupKey=-Bw etc…; CognitoIdentityServiceProvider.6f3uavvmetc.randomPasswordKey=Sa etc…; CognitoIdentityServiceProvider.6f3uavvmetc.signInDetails={%2 etc…; CognitoIdentityServiceProvider.6f3uavvmetc.clockDrift=-1275; __stripe_sid=235 etc…

Alas this is not enough to satisfy the beast’s insatiable lust for credentials!!! Isn’t it normally just idToken and accessToken required? AppSync is setup with AWS_IAM as the primary auth mode.

It’s like following a shoe string through a swimming pool of spaghetti. But the AWS console is showing that the IAM role (I think it’s using) has these permissions:

{ "Version": "2012-10-17", "Statement": [ { "Action": [ "mobileanalytics:PutEvents", "cognito-sync:", "cognito-identity:", "appsync:GraphQL" ], "Effect": "Allow", "Resource": [ "" ] }, { "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan" ], "Resource": [ "" ] } ] }

Sorry I’m not sure exactly what you mean by “Could you request your implementation here?” Here’s how I’m calling graphql now:

try { const result = await cookieBasedClient.graphql({ query: createTransaction, variables: { transaction: transactionInput }, }) console.log("createTransaction result transactionId = ", result.data.createTransaction?.transactionId);

} catch (error) { console.log("createTransaction failed with error: ", error);

}

Thank you for you help!

CooperCodeComposer commented 5 months ago

It's day 8. I have realized that I wasn't passing in the identityPoolId to the Cognito Auth amplifyConfig. It's not erroring with "No Credentials", but it's still not working. I get this "Unknown error" now:

get transaction failed with error:  {
  data: {},
  errors: [
    UnauthorizedException: Unknown error
        at buildRestApiServiceError (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/serviceError.mjs:17:26)
        at parseRestApiServiceError (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/serviceError.mjs:30:26)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at async job (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/createCancellableOperation.mjs:31:23)
        at async GraphQLAPIClass._graphql (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs:234:44)
        at async runWithAmplifyServerContext (webpack-internal:///(rsc)/./node_modules/aws-amplify/dist/esm/adapterCore/runWithAmplifyServerContext.mjs:23:24)
        at async GET (webpack-internal:///(rsc)/./app/api/price/route.ts:57:24)
        at async /Users/acooper/Documents/DOCS Work/DEV WebProjects/projectname/front-end/node_modules/next/dist/compiled/next-server/app-route.runtime.dev.js:6:63251 {
      recoverySuggestion: `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')`
    }
  ]
}

So I modified the cookie client to specifically tell it to use "iam" (even though that's meant to be the default):

try {
  const result = await cookieBasedClient.graphql({
    query: getTransaction,
    variables: { transactionId: "1234" },
    authMode: 'iam'
  });
  console.log("getTransaction result transactionId = ", result.data.getTransaction?.transactionId);

} catch (error) {
  console.log("get transaction failed with error: ", error);
}

I also explicitly tell getTransaction to use "iam" (although I'm not sure if this is needed?):

type Query {
  getCognitoUser: CognitoUser
  getTransaction(transactionId: String!): Transaction @aws_iam
  listTransactions: [Transaction]
}

I've tested my queries in Lambda + Appsync and they're working ok. Cognito just won't let me sleep.

Thx

HuiSF commented 5 months ago

Hi @CooperCodeComposer Apologies I'm a bit lost with your set up. So you are using IAM role to authentication, does that mean this GraphQL API call is not tight to a specific user session (i.e. doesn't depend on the auth tokens for a specific user)?

cwomack commented 5 months ago

@CooperCodeComposer, can you also share your schema please? Curious how your authorization rules are set up.

CooperCodeComposer commented 5 months ago

Thank you! @cwomack @HuiSF

I think I’m getting a lot closer. I’ve made a successful graphql query by switching to using “userPool” as the auth method everywhere.

I think that IAM auth is maybe just for authorizing access from AWS service to AWS service?
This was how my user sign up/in calls were successfully working. But I think that’s because I never directly called the graphql API within the app to do this. I think that’s AWS -> AWS access. ??

Goals

(As mentioned) within that stripe web hook the cookies are always null. I’m not sure that it’s possible for that server side webhook to ever get user pool credential?

Do you think I should change the queries within the web hook to be accessed via “API_KEY” and pass that in as an environment variable?

There’s this documentation that talks about modifying the schema to specify different access levels: https://aws.amazon.com/blogs/mobile/graphql-security-appsync-amplify/

So I tried this:

type Query {
  getCognitoUser: CognitoUser
  getTransaction(transactionId: String!): Transaction @aws_iam
  listTransactions: [Transaction]
}

type Transaction {
  transactionId: String!
  userId: String!
  amount: Float!
  currency: String!
  timestamp: String!
  status: String!
  paymentIntentId: String!
  priceId: String!
}
etc...

I’m using $pulumi up It seems like it did make changes when I made that change to the schema.

But it seems like the client really responds to the authMode passed to it. Like this:

try { const result = await cookieBasedClient.graphql({ query: getTransaction, variables: { transactionId: "1234" }, authMode: 'userPool' // 'iam' }); console.log("getTransaction result transactionId = ", result.data.getTransaction?.transactionId);

} catch (error) { console.log("get transaction failed with error: ", error); }

…at no point in the setup have I run $amplify add auth …it’s confusing as to what pulumi has setup and whether I should run $amplify add auth ? to setup an unauth IAM role? ? Although In IAM there are amplify auth and unauth roles for the app... but the only have access to Cognito? Pulumi has setup a auth role for the app with all the bells and whistles access.

I’ve read this doc, but I can’t get any of the syntax to work in my schema. Is this the way? https://docs.amplify.aws/nextjs/build-a-backend/graphqlapi/customize-authorization-rules/

Let’s say I try this in my schema:

type Transaction @model @auth(rules: [{ allow: public }]) { transactionId: String! userId: String! amount: Float! currency: String! timestamp: String! status: String! paymentIntentId: String! priceId: String! }

I get this on $pulumi up

error: aws:appsync:GraphQLApi (appsync): error: 1 error occurred:

...it's dev-ops rocket science trying to debug why and where any of this is going wrong based on these errors.

“When you run amplify add auth, the Amplify CLI generates scoped down IAM policies for the "Unauthenticated role" in Cognito identity pool automatically.”

This makes me think you could use IAM for unauthenticated users? Although that web hook route.ts still wouldn’t have any cookies so I don’t see the graphql client working using iam or the userPools.

I’m going to try setting authMode to api keys for that call in the web hook to see if that can work.

For ref here’s how my config looks right now:

const amplifyConfig: ResourcesConfig = {
  Auth: {
    Cognito: {
      userPoolClientId: config.USER_POOL_CLIENT_ID,
      userPoolId: config.USER_POOL_ID,
      identityPoolId: config.IDENTITY_POOL_ID,
      loginWith: { // Optional
        oauth: {
          domain: 'https://myapp.auth.us-east-1.amazoncognito.com',  
          scopes: ['email', 'openid', 'aws.cognito.signin.user.admin'],
          redirectSignIn: [config.REDIRECT_SIGN_IN],
          redirectSignOut: [config.REDIRECT_SIGN_OUT],
          responseType: 'code',
        },
        username: true, // note: username is their email 
        email: false, // Optional
        phone: false, // Optional
      }
    }
  },
  API: {
    GraphQL: {
      endpoint: config.GRAPHQL_ENDPOINT,
      defaultAuthMode: 'userPool', 
      region: config.REGION, // Optional
    }
  },
};

export default amplifyConfig;

Thank you for you help!

HuiSF commented 5 months ago

Hey @CooperCodeComposer, thanks for providing more details. From your description, your use case for making the query doesn't rely on a user session that's created on the client side. In this case, could you try the following?

  1. use generateServerClientUsingReqRes to create the GQL client, for example in src/amplifyUtils.ts
import { createServerRunner } from '@aws-amplify/adapter-nextjs';
import { generateServerClientUsingReqRes } from '@aws-amplify/adapter-nextjs/api';
import amplifyConfig from '@/amplifyconfiguration.json';

export const { runWithAmplifyServerContext } = createServerRunner({
  config: amplifyConfig
});

export const reqResBasedClient = generateServerClientUsingReqRes({
  config: amplifyConfig
});
  1. use the runWithAmplifyServerContext and reqResBasedClient to make the query call
try {
  const result = await runWithAmplifyServerContext({
    nextServerContext: null, // <-- NOTE here, see the explantation below
    operation: (contextSpec) => {
      return reqResBasedClient.graphql(
        contextSpec,
        {
          query: getTransaction,
          variables: { transactionId: "1234" },
          authMode: 'iam',
        }
      );
    }
  });

  console.log("getTransaction result transactionId = ", 
  result.data.getTransaction?.transactionId);
} catch (error) {
  console.log("get transaction failed with error: ", error);
}

Note that in the above code example, we are setting nextServerContext as null. This specifies that this server context is not meant to correlate with a user session that's sent from the client. So this context will attempt to get authentication details from the IAM provider. Similar to ApiKey, if you are using it, you should do the same.

CooperCodeComposer commented 5 months ago

Thanks @HuiSF

With that code I'm getting the "No Credentials" error.

If my AWS AppSync API says it's using IAM for auth what else do I need to verify to debug if there's an issue with the IAM setup? It feels like something is wrong in IAM because all my calls on the graphql client have always failed when trying to use that.

I would have thought that if the graphql client is has an endpoint set isn't it internally checking within AWS that AppSync has IAM attached and then saying "cool you have credentials". But ney. It's saying "No Credentials".

There aren't any settings with in the AppSync console (that I can see) to even tweak the IAM settings.

I've also tried for a while using and apiKey auth. It doesn't seem like a great approach. + that too was failing.

Not sure why I can't seem to specify access levels individually in my schema file as the AWS docs seem to suggest? (as mentioned it errors when I $pulumi up).

Thanks!

HuiSF commented 5 months ago

I checked the resources created by Amplify CLI, and if I specify using IAM as an authentication provider for my AppSync endpoint, it generates corresponding IAM roles and policies for unauthenticated and authenticated use cases (details see this document). Since you are using a third-party tool to set up the resource, it may be worth checking if everything is set up correctly.

There aren't any settings with in the AppSync console (that I can see) to even tweak the IAM settings.

If you go to AppSync console, choose settings from the left nav bar, you can view authorization provider settings on the page. By looking the resource configuration you provided, I'd expect to see something similar to this:

ghost commented 5 months ago

Hi there, i have the same problem.

I solved by the moment using apiKey, but if you find any solution, please share.

I test from client-side and server-side, and i get errors like: "No Credentials: Credentials should not be empty", "'Not Authorized to access....".

HuiSF commented 5 months ago

Hi @jojemapa how did you set up the Amplify resource? Using a 3rd-party tool, too, or Amplify CLI?

ghost commented 5 months ago

Hi, i use Amplkify CLI.

amplify add auth amplidy add api

in my schema i just put allow: private and provider: iam.

The last part amplify add the iam permission to appsync in the auth role.

mmm, i finally i get that error, i still searching for a fix.

HuiSF commented 5 months ago

Thanks @jojemapa, were you able to make a mutation/query successfully using IAM on the client side at all?

CooperCodeComposer commented 5 months ago

@jojemapa Thanks for the info. I've also only been able to get it to work using the api key. It's tough! I'm setting up a support group for developers with IAM issues. The only problem....you need IAM credentials to enter. Nooooooooooooooo! jk ;^)

If I use the api key method, I can't individually set queries to use user pools. + IAM hasn't worked for anything. I think I'm stuck rotating api keys like it's 1692 for a while.

ghost commented 5 months ago

Hi there guys, I've test again in a "new simple test app" using Nextjs.

I've configure the Auth and API using CLI.

Here some screenshots of my commands :

amplify add auth Screenshot 2024-02-07 222231

amplify add api Screenshot 2024-02-07 222257

This is my schema:

type Todo @model @auth(rules: [{ allow: private, provider: iam }]) {
  id: ID!
  content: String!
}

This is my code from client-side:

try {
  const client = generateClient(); // By default is authMode is IAM

  const result = await client.graphql({
    query: mutations.createTodo,
    variables: {
      input: {
        content: "Content",
      },
    },
  });

  console.log(result.data);
} catch (error) {
  console.log("Error :(", error);
}

In client-side its works OK. The problem is server-side.

And this is my code from server-side:

Attempt 1: Usuing generateServerClientUsingCookies

const client = generateServerClientUsingCookies({
  config: config,
  cookies,
  authMode: "iam",
});

const result = await client.graphql({
  query: mutations.createTodo,
  variables: {
    input: {
      content: "Content",
    },
  },
  authMode: "iam",
});

The error is:

{
  data: {},
  errors: [
    UnauthorizedException: Unknown error
        at buildRestApiError (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/serviceError.mjs:87:26)
        at parseRestApiServiceError (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/serviceError.mjs:30:16)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at async job (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-rest/dist/esm/utils/createCancellableOperation.mjs:31:23)
        at async GraphQLAPIClass._graphql (webpack-internal:///(rsc)/./node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs:234:44)
        at async runWithAmplifyServerContext (webpack-internal:///(rsc)/./node_modules/aws-amplify/dist/esm/adapterCore/runWithAmplifyServerContext.mjs:23:24)
        at async test (webpack-internal:///(rsc)/./app/data.ts:19:20)
        at async Page (webpack-internal:///(rsc)/./app/cosa/page.tsx:15:18) {
      recoverySuggestion: `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')`
    }
  ]
}

Attempt 2: Using generateServerClientUsingReqRes

const reqResBasedClient = generateServerClientUsingReqRes({
  config: config,
  authMode: "iam",
});

const { runWithAmplifyServerContext } = createServerRunner({
  config: config,
});

const result = await runWithAmplifyServerContext({
  nextServerContext: null,
  operation: async (contextSpec) => {
    const request = await reqResBasedClient.graphql(contextSpec, {
      query: mutations.createTodo,
      variables: {
        input: {
          content: "Content",
        },
      },
      authMode: "iam",
    });

    return request.data.createTodo;
  },
});

The error is:

NotAuthorizedException: Unauthenticated access is not supported for this identity pool.

Attempt 3: Using generateServerClientUsingCookies + runWithAmplifyServerContext

const result = await runWithAmplifyServerContext({
  nextServerContext: { cookies },
  operation: async (contextSpec) => {
    const request = await cookiesBasedClient.graphql({
      query: mutations.createTodo,
      variables: {
        input: {
          content: "Content",
        },
      },
      authMode: "iam",
    });

    return request.data.createTodo;
  },
});

Same error as the Attempt 1.

Another test using just userPool like the following works OK in client and server side:

type PrivateTodo @model @auth(rules: [{ allow: private }]) {} type AdminsTodo @model @auth(rules: [{ allow: groups, groups: ["Admin"] }]) {}

I think that the problem is using IAM provider in server-side

HuiSF commented 5 months ago

Thanks @jojemapa for providing the set up. I will run some tests and dig, will get back to you.

HuiSF commented 5 months ago

Based on the testing with the stack that @jojemapa provided, this is a bug that using iam auth mode is not working as expected on the server side. We are currently working on a fix.

chrisbonifacio commented 5 months ago

We have a tagged release that you can use to test whether it resolves your issue. Please note these are not meant for production, only for verification.

Install these packages and versions in your project and let us know if it fixes the issue for you.

npm i aws-amplify@6.0.16-iam-auth-server.ebba1a4.0 @aws-amplify/adapter-nextjs@1.0.16-
iam-auth-server.ebba1a4.0
ghost commented 5 months ago

Yes, it works nice @chrisbonifacio . Thanks.

I've test that in server-side and it works OK.

const client = generateServerClientUsingCookies({
  config: config,
  cookies,
  // authMode: "iam", Default is configured to IAM
});

const mutationTest = await client.graphql({
  query: mutations.createTodo,
  variables: {
    input: {
      content: "Content",
    },
  },
  // authMode: "iam", Default is configured to IAM
});

const queryTest = await client.graphql({
    query: queries.listTodo,
    variables: {},
    // authMode: "iam", Default is configured to IAM
  });
chrisbonifacio commented 5 months ago

Thank you everyone for reporting this issue, your patience is appreciated! Closing this issue as the fix was just released. Please upgrade to v6.0.17.