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.43k stars 2.13k forks source link

Permission denied - allow.authenticated() #13431

Closed jmarshall9120 closed 4 months ago

jmarshall9120 commented 5 months ago

Before opening, please confirm:

JavaScript Framework

Vue

Amplify APIs

Authentication, GraphQL API

Amplify Version

v6

Amplify Categories

auth, api

Backend

Amplify Gen 2 (Preview)

Environment information

``` # Put output below this line System: OS: Windows 11 10.0.22631 CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz Memory: 16.31 GB / 31.74 GB Binaries: Node: 20.12.2 - C:\Program Files\nodejs\node.EXE Yarn: 1.22.22 - ~\AppData\Roaming\npm\yarn.CMD npm: 10.5.0 - C:\Program Files\nodejs\npm.CMD Browsers: Chrome: 125.0.6422.77 Edge: Chromium (123.0.2420.97) Internet Explorer: 11.0.22621.3527 npmPackages: %name%: 0.1.0 @aws-amplify/backend: ^1.0.2 => 1.0.2 @aws-amplify/backend-cli: ^1.0.3 => 1.0.3 @aws-sdk/client-cognito-identity-provider: ^3.577.0 => 3.577.0 @aws-sdk/client-sso-oidc: 3.575.0 => 3.575.0 (3.576.0, 3.577.0, 3.338.0) @aws-sdk/types: 3.575.0 => 3.575.0 (3.387.0, 3.398.0, 3.433.0, 3.338.0, 3.577.0) @mdi/font: ^7.4.47 => 7.4.47 @pinia/nuxt: ^0.5.1 => 0.5.1 @types/aws-lambda: ^8.10.138 => 8.10.138 (8.10.137) @unocss/reset: 0.60.2 => 0.60.2 aws-amplify: ^6.3.1 => 6.3.1 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/data: undefined () aws-amplify/data/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-cdk: ^2.141.0 => 2.141.0 aws-cdk-lib: ^2.141.0 => 2.141.0 bin: 1.0.0 constructs: ^10.3.0 => 10.3.0 dist: 1.0.0 esbuild: ^0.21.2 => 0.21.2 (0.20.2) floating-vue: 5.2.2 => 5.2.2 nuxt: ^3.11.2 => 3.11.2 pinia: ^2.1.7 => 2.1.7 react: 18.3.1 => 18.3.1 react-dom: 18.3.1 => 18.3.1 terser: 5.31.0 => 5.31.0 tsx: ^4.10.2 => 4.10.2 typescript: ^5.4.5 => 5.4.5 (4.4.4, 4.9.5) unocss: 0.60.2 => 0.60.2 vite-plugin-vuetify: ^2.0.3 => 2.0.3 vue: ^3.4.27 => 3.4.27 vue-router: ^4.3.2 => 4.3.2 vuetify: ^3.6.5 => 3.6.5 npmGlobalPackages: yarn: 1.22.22 ```

Describe the bug

Disclaimer - Not 100% sure this is a bug yet, but I have enough info its time make a record.

API is not allowing authenticated users. Calls to the graphql endpoint in the sandbox, with an authenticated user result in:

{
    "errors": [
        {
            "errorType": "UnauthorizedException",
            "message": "Permission denied"
        }
    ]
}

Been following the tutorials here for Amplify Gen2: https://docs.amplify.aws/vue/build-a-backend/data/customize-authz/

Here's the setup:

//@/amplify/data/resources.ts
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';

const schema = a.schema({
  US_STATE: a
    .enum(['AL',  'AK',  'AS',  'AZ',  'AR',  'CA',  'CO',  'CT',  'DE',  'DC',  'FM',  'FL',  'GA',  'GU',  'HI',  'ID',  'IL',  'IN',  'IA',  'KS',  'KY',  'LA',  'ME',  'MH',  'MD',  'MA',  'MI',  'MN',  'MS',  'MO',  'MT',  'NE',  'NV',  'NH',  'NJ',  'NM',  'NY',  'NC',  'ND',  'MP',  'OH',  'OK',  'OR',  'PW',  'PA',  'PR',  'RI',  'SC',  'SD',  'TN',  'TX',  'UT',  'VT',  'VI',  'VA',  'WA',  'WV',  'WI',  'WY']),
  user: a
    .model({
      cognito_username: a.string().required(),
      email_1: a.email(),
      email_2: a.email(),
      first_name: a.string(),
      last_name: a.string(),
      phone_1: a.string(),
      phone_2: a.string(),
      address_1: a.string(),
      address_2: a.string(),
      city: a.string(),
      state: a.ref('US_STATE'),
      postal_code: a.string(),
      country: a.string().default('USA')
    })
    .identifier(['cognito_username']),
    // .authorization((allow) => [allow.guest()]),
  authorization: a
    .model({
      issuer_id: a.string().required(),
      user_id: a.string().required(),
      resource_type: a.enum(['COMPANY', 'LEASE', 'UNIT']),
      resource_id: a.string().required(),
      authority_type: a.enum(['COMPANY#EMPLOYEE', 'LEASE#OWNER', 'ADMIN']),
      authority_id: a.string().required()
    }),
    // .authorization((allow) => [allow.guest()]),
}).authorization((allow) => [
  // allow.guest(),
  allow.authenticated()
]);

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'iam'
  },
});
//@/amplify/auth/resources.ts
import { defineAuth } from "@aws-amplify/backend";
import { postConfirmation } from "./post-confirmation/resource";

export const auth = defineAuth({
  loginWith: {
    email: true
  },
  groups: ["admin", "employee", "tenant", "stakeholder", "allusers"],
  triggers: {
    postConfirmation
  },
  access: allow => [allow.resource(postConfirmation).to(["addUserToGroup"])]
});

In my app there's a login page, and a call to getCurrentUser() + client.models.user.list() in App.vue. I don't think that code is important.

The schema on the appsync backend query looks like this:

type Query {
    getUser(cognito_username: String!): user
        @aws_cognito_user_pools
@aws_iam
    listUsers(
        cognito_username: String,
        filter: ModelUserFilterInput,
        limit: Int,
        nextToken: String,
        sortDirection: ModelSortDirection
    ): ModelUserConnection
        @aws_cognito_user_pools
@aws_iam
    getAuthorization(id: ID!): authorization
        @aws_cognito_user_pools
@aws_iam
    listAuthorizations(filter: ModelAuthorizationFilterInput, limit: Int, nextToken: String): ModelAuthorizationConnection
        @aws_cognito_user_pools
@aws_iam
}

Here's the http request: AmplifyGraphQLQuery

You can see that the cognito calls are made correctly for the sign in and the signature/session token are calculated for the request which should suggest that everything code wise from the amplify gen 2 developers perspective (my perspective) is correct. I've also confirmed this all in python using http only.

What have I tried:

My next stop is IAM at this point. My theory is that the sandbox / amplify gen 2 have not setup user pool roles correctly.

Expected behavior

Should allow the query to pass.

Reproduction steps

  1. Create a new app.
  2. Add the resource files posted above.
  3. Create a sandbox.
  4. Sign up a user.
  5. Login.
  6. Attempt query.

Hopefully if I find the smoking gun here, more reproduction examples can be give. If this seems un-reproducible I can share the repo. I assume because the issue has persisted on rebuild of the sandbox, and in a direct http request reproduction, that the issue must be in the building of the sandbox somewhere.

Code Snippet

// Put your code below this line.

Log output

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

aws-exports.js

No response

Manual configuration

No response

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

chrisbonifacio commented 5 months ago

The default auth mode on your API is "iam". Are you specifying "userPool" as the authMode on the client?

The only client code I see in the description is:

client.models.user.list()

If you haven't already tried, can you try this?

client.models.user.list({
  authMode: "userPool"
})

https://docs.amplify.aws/react/build-a-backend/data/query-data/#:~:text=Troubleshooting,Troubleshoot%20unauthorized%20errors "Troubleshooting Troubleshoot unauthorized errors"

chrisbonifacio commented 5 months ago

Also, in case I'm misunderstanding, and you actually do mean to use iam as an auth mode for authenticated users, then the authorization rule you need on the schema is actually:

allow.authenticated("identityPool")

https://docs.amplify.aws/react/build-a-backend/data/customize-authz/signed-in-user-data-access/#use-identity-pool-for-signed-in-user-authentication

In the screenshot the request is being signed, which means IAM is being used as the authMode. The role being used is the AuthRole on your Cognito IdentityPool.

If you wanted to authorize using your Cognito UserPool, then the request would have a JWT access token in the Authorization header.

jmarshall9120 commented 5 months ago

@chrisbonifacio - You maybe on to something. The original idea when I started testing this was to allow both guests & users. So in the mixing and matching process experimenting here I may have gotten my environment a little messed up. Let me ask a couple questions, that may seem off topic, but may help me here:

1).

So is it the recommended pattern, that when authenticating for users or guests we do something like this:

authUser = await getCurrentUser();
if (authUser?.userId){
    client.models.user.list({
         authMode: 'userPools'
    })
} else {
    client.models.user.list({
        authMode: 'identityPool'
    })
}

Because even though, the docs show this: image It seems from your comments and my testing that authMode:'identityPool' does not actually allow signed in user access.

2).

Let me confirm the auth flow strategy with you so we don't confuse ourselves. Authenticated User: AWSCognitoIdentityProviderService.InitiateAuth --> JWT AWSCognitoIdentityService.GetId --> IdentityId

AWSCognitoIdentityService.GetCredentialsForIdentity --> Credentials

Unauthenticated User (guest): AWSCognitoIdentityService.GetId --> IdentityId | AWSCognitoIdentityService.GetCredentialsForIdentity --> Credentials

For a guest appsync accepts credentials but for an authenticated user appsync only accepts JWT - even though the sign in flow goes through the trouble of obtaining the credentials for an authenticated user anyway?

jmarshall9120 commented 5 months ago

Also, just retested to make sue I wasn't crazy, and noticed the authMode names in that screen shotted doc are wrong as well. There is no authMode 'userPools' - its 'userPool'.

chrisbonifacio commented 5 months ago

So is it the recommended pattern, that when authenticating for users or guests we do something like this: ... It seems from your comments and my testing that authMode:'identityPool' does not actually allow signed in user access.

The identityPool authMode allows both signed in and guest user access. The Amplify data client will use the AuthRole when signed in and UnAuthRole when signed out. With the logic you provided, the identityPool authMode would be used if the auth rule on your schema is allow.guest(), because if a user is not signed in the else statement will execute.

This would be aligned with your original idea to allow both guests & users.

Let me confirm the auth flow strategy with you so we don't confuse ourselves. Authenticated User: AWSCognitoIdentityProviderService.InitiateAuth --> JWT AWSCognitoIdentityService.GetId --> IdentityId

AWSCognitoIdentityService.GetCredentialsForIdentity --> Credentials

Unauthenticated User (guest): AWSCognitoIdentityService.GetId --> IdentityId | AWSCognitoIdentityService.GetCredentialsForIdentity --> Credentials

For a guest appsync accepts credentials but for an authenticated user appsync only accepts JWT - even though the sign in flow goes through the trouble of obtaining the credentials for an authenticated user anyway?

That breakdown seems correct to me.

AppSync accepts Cognito access tokens or an OIDC token for authenticated users. However, AppSync also accepts IAM as an auth mode, Amplify just creates an IAM role (AuthRole) for "authenticated users" but through an identityPool. This might be a bit confusing because the user still has to be logged in for Amplify to use this role.

Also, just retested to make sue I wasn't crazy, and noticed the authMode names in that screen shotted doc are wrong as well. There is no authMode 'userPools' - its 'userPool'.

Thanks for pointing this out! Just opened a PR with a correction:

https://github.com/aws-amplify/docs/pull/7673

chrisbonifacio commented 4 months ago

Hi 👋 Closing this as we have not heard back from you. If you are still experiencing this issue and in need of assistance, please feel free to comment and provide us with any information previously requested by our team members so we can re-open this issue and be better able to assist you.

Thank you!