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

GraphQL API: API user group authorization causes identity pool authorization to not work for users in the user group #13484

Closed CooperW824 closed 2 months ago

CooperW824 commented 3 months ago

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

Authentication, GraphQL API

Amplify Version

v6

Amplify Categories

auth, api

Backend

Amplify Gen 2 (Preview)

Environment information

``` System: OS: Windows 11 10.0.22631 CPU: (12) x64 AMD Ryzen 5 5600X 6-Core Processor Memory: 2.45 GB / 15.92 GB Binaries: Node: 20.11.0 - C:\Program Files\nodejs\node.EXE npm: 10.5.1 - C:\Program Files\nodejs\npm.CMD Browsers: Edge: Chromium (125.0.2535.92) Internet Explorer: 11.0.22621.1 npmPackages: %name%: 0.1.0 @ampproject/toolbox-optimizer: undefined () @aws-amplify/backend: ^1.0.3 => 1.0.3 @aws-amplify/backend-cli: ^1.0.4 => 1.0.4 @aws-amplify/ui-react: ^6.1.12 => 6.1.12 @aws-amplify/ui-react-internal: undefined () @babel/core: undefined () @babel/runtime: 7.22.5 @edge-runtime/cookies: 4.1.1 @edge-runtime/ponyfill: 2.4.2 @edge-runtime/primitives: 4.1.0 @hapi/accept: undefined () @mswjs/interceptors: undefined () @napi-rs/triples: undefined () @next/font: undefined () @opentelemetry/api: undefined () @types/node: ^20 => 20.14.2 @types/react: ^18 => 18.3.3 @types/react-dom: ^18 => 18.3.0 @vercel/nft: undefined () @vercel/og: 0.6.2 acorn: undefined () amphtml-validator: undefined () anser: undefined () arg: undefined () assert: undefined () async-retry: undefined () async-sema: undefined () aws-amplify: ^6.3.6 => 6.3.6 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.145.0 => 2.145.0 aws-cdk-lib: ^2.145.0 => 2.145.0 babel-packages: undefined () browserify-zlib: undefined () browserslist: undefined () buffer: undefined () bytes: undefined () ci-info: undefined () cli-select: undefined () client-only: 0.0.1 commander: undefined () comment-json: undefined () compression: undefined () conf: undefined () constants-browserify: undefined () constructs: ^10.3.0 => 10.3.0 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 () esbuild: ^0.21.4 => 0.21.4 (0.20.2) events: undefined () find-cache-dir: undefined () find-up: undefined () 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 () mini-css-extract-plugin: undefined () nanoid: undefined () native-url: undefined () neo-async: undefined () next: 14.2.3 => 14.2.3 node-fetch: undefined () node-html-parser: undefined () ora: undefined () os-browserify: undefined () p-limit: undefined () path-browserify: undefined () picomatch: undefined () platform: undefined () postcss: ^8 => 8.4.38 (8.4.31) 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.3.1 react-builtin: undefined () react-dom: ^18 => 18.3.1 react-dom-builtin: undefined () react-dom-experimental-builtin: undefined () react-experimental-builtin: undefined () 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 () source-map08: undefined () stacktrace-parser: undefined () stream-browserify: undefined () stream-http: undefined () string-hash: undefined () string_decoder: undefined () strip-ansi: undefined () superstruct: undefined () tailwindcss: ^3.4.1 => 3.4.4 tar: undefined () terser: undefined () text-table: undefined () timers-browserify: undefined () tsx: ^4.14.0 => 4.14.0 tty-browserify: undefined () typescript: ^5.4.5 => 5.4.5 (4.4.4, 4.9.5) 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: @angular/cli: 14.2.1 @aws-amplify/cli: 12.1.1 angular: 1.8.3 npm: 10.5.1 nuxi: 3.5.3 prettier: 2.8.8 typescript: 4.7.2 ```

Describe the bug

So I define my API like so with the goal of only admins being able to modify content, and with authenticated and unauthenticated users being able to read the content using the Identity Pool. However, if an Admin user tries to use the identity pool to read the data, they get a "GraphQL: Unauthorized" error. All other users (authenticated or not) can read, as expected, but users with this 'admin' user group can't read using the Identity Pool.

API definition:

const schema = a.schema({
    Todo: a
        .model({
            content: a.string(),
        })
        .authorization((allow) => [
            allow.group("admin").to(["create", "update", "read", "delete"]),
            // These two lines should allow everyone to read the data
            // regardless of whether they are authenticated or not
            // however, if the user is authenticated, and is a part of the admin user group I get a GraphQLError
            // saying that the user is not authorized to perform this action
            // I would expect this admin user to be able to read the data using the identityPool auth mode
            // because the user is authenticated, but it seems like the user group is causing an issue
            // if a user is not apart of a user group, the data is returned as expected
            allow.authenticated("identityPool").to(["read"]),
            allow.guest().to(["read"]),
        ]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
    schema,
    authorizationModes: {
        defaultAuthorizationMode: "iam",
    },
});

Expected behavior

I would expect, that since the admin user is authenticated, they can use the Identity Pool to read the data from the API without any errors.

Reproduction steps

  1. Configure an API to use user groups and the identity pool for authorization like the one shown above.
  2. Create a user that has the 'admin" user group
  3. Sign in as that user
  4. Try to read from the API using the Identity Pool auth mode while signed in as the admin user
  5. Observe the GraphQL: Unauthorized error occur

Code Snippet

I was able to reproduce the bug in this example repository: https://github.com/CooperW824/amplify-user-group-auth-issue-example

amplify/data/resource.ts

// Put your code below this line.
const schema = a.schema({
    Todo: a
        .model({
            content: a.string(),
        })
        .authorization((allow) => [
            allow.group("admin").to(["create", "update", "read", "delete"]),
            // These two lines should allow everyone to read the data
            // regardless of whether they are authenticated or not
            // however, if the user is authenticated, and is a part of the admin user group I get a GraphQLError
            // saying that the user is not authorized to perform this action
            // I would expect this admin user to be able to read the data using the identityPool auth mode
            // because the user is authenticated, but it seems like the user group is causing an issue
            // if a user is not apart of a user group, the data is returned as expected
            allow.authenticated("identityPool").to(["read"]),
            allow.guest().to(["read"]),
        ]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
    schema,
    authorizationModes: {
        defaultAuthorizationMode: "iam",
    },
});

app/page.tsx

"use client";

import { useEffect, useState } from "react";

import { Amplify } from "aws-amplify";
import { generateClient } from "aws-amplify/api";
import awsexports from "@/amplify_outputs.json";
import { Schema } from "@/amplify/data/resource";
import * as Auth from "@aws-amplify/auth";

Amplify.configure(awsexports);

const client = generateClient<Schema>();

type Todo = {
    content: string;
};

export default function Page() {
    const [todos, setTodos] = useState<Todo[]>([]);

    async function listTodos() {
        const { data, errors } = await client.models.Todo.list({
            authMode: "identityPool",
        });

        if (errors || !data) {
            console.error(errors);
            return;
        }

        setTodos(data.map((todo) => ({ content: todo.content } as Todo)));
    }

    useEffect(() => {
        listTodos();
    }, []);

    return (
        <div>
            <h1>Todos</h1>
            <ul>
                {todos.map((todo, index) => (
                    <li key={index}>{todo.content}</li>
                ))}
            </ul>
        </div>
    );
}

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 3 months ago

Hi @CooperW824 can you try setting the auth mode to userPool in your request? You can change between userPool and identityPool depending on whether your user is in a group or not:

  const listTodos = async () => {
    const session = await fetchAuthSession();

    let groups = session.tokens?.accessToken.payload["cognito:groups"];

    console.log({ groups });

    const { data, errors } = await client.models.Todo.list({
      authMode: groups ? "userPool" : "identityPool",
    });

    if (errors) {
      console.error({ errors });
    }

    console.log({ data });
  };

We're working on a feature that will solve this automatically but for now this would be the suggested workaround. Marking this as a feature-request for tracking along with the work.

CooperW824 commented 3 months ago

Thanks for reaching out! Yeah using the userPool auth mode works fine. This work around will work for what I need to do here. Thanks to the Amplify team for all the great work!