Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug] Amplify Gen2: AuthMode Type Mismatch ('userPool' vs 'userPools') Causes Authentication Failures #3083

Closed
georgechristman opened this issue Dec 18, 2024 · 6 comments

Comments

@georgechristman
Copy link

georgechristman commented Dec 18, 2024

Environment information

## Environment
System:
  - OS: macOS 15.1.1
  - CPU: (10) arm64 Apple M1 Pro
  - Memory: 133.06 MB / 32.00 GB
  - Shell: /bin/zsh

Binaries:
  - Node: 20.9.0
  - npm: 10.1.0
  - pnpm: 8.10.5

NPM Packages:
  - aws-amplify: 6.10.0
  - @aws-amplify/backend: 1.8.0
  - @aws-amplify/backend-cli: 1.4.2
  - @aws-amplify/backend-data: 1.2.1
  - @aws-amplify/backend-auth: 1.4.1
  - aws-cdk-lib: 2.171.1
  - typescript: 5.7.2

Describe the bug

AuthMode Type Definition Mismatch in Amplify Gen2

Description

There is a type definition mismatch between the runtime implementation and TypeScript types for AuthMode in Amplify Generation 2. The runtime expects 'userPools' (plural) as a valid auth mode, but the TypeScript type definition only includes 'userPool' (singular). This leads to TypeScript errors and potential runtime authentication issues.

Current Behavior

  • TypeScript type AuthMode includes 'userPool' (singular)
  • Runtime implementation expects 'userPools' (plural)
  • This causes type errors when trying to use the correct runtime value
  • When using 'userPool' (as suggested by types), results in authentication errors

Error Details

When attempting to create an order with userPool (as suggested by types):

Error: NoValidAuthTokens: No federated jwt
    at headerBasedAuth (webpack-internal:///(action-browser)/./node_modules/@aws-amplify/api-graphql/dist/esm/internals/graphqlAuth.mjs:49:23)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async GraphQLAPIClass._graphql (webpack-internal:///(action-browser)/./node_modules/@aws-amplify/api-graphql/dist/esm/internals/InternalGraphQLAPI.mjs:150:29)
    at async eval (webpack-internal:///(action-browser)/./node_modules/@aws-amplify/data-schema/dist/esm/runtime/internals/operations/get.mjs:47:42)

Recovery Suggestion: "If you intended to make an authenticated API request, review if the current user is signed in."

Expected Behavior

The TypeScript type definition should match the runtime implementation by including 'userPools' as a valid value for AuthMode, preventing both type errors and runtime authentication issues.

Reproduction Steps

  1. Create a new Amplify Gen2 project
  2. Try to use userPools as the auth mode:
const order = await client.models.Order.create(
  orderData,
  { 
    authMode: 'userPools', // Type error here
    authToken: token 
  }
);
  1. Observe the TypeScript error:
Type '"userPools"' is not assignable to type 'AuthMode | undefined'.
  1. If using 'userPool' as suggested by types, observe the runtime error as well as the [docs]:(https://docs.amplify.aws/react/build-a-backend/data/customize-authz/per-user-per-owner-data-access/)
NoValidAuthTokens: No federated jwt

Current Workaround

Currently, developers need to use type assertions to work around this issue:

authMode: 'userPools' as any

Additional Context

The AuthMode type is currently defined as:

export type AuthMode = 'apiKey' | 'iam' | 'identityPool' | 'oidc' | 'userPool' | 'lambda' | 'none';

This type definition enforces using 'userPool' (singular), but the runtime implementation requires using 'userPools' (plural). When we use the working runtime value 'userPools', TypeScript shows a compilation error because it's not included in the AuthMode type.

This leads to a situation where using the correct runtime value ('userPools') works at runtime but fails TypeScript compilation, forcing developers to use type assertions as a workaround.

Impact

This type mismatch:

  • Forces developers to use type assertions
  • Creates confusion about the correct value to use
  • Reduces type safety by requiring workarounds
  • Can lead to runtime authentication failures when following the type definitions
  • Wastes development time debugging what appears to be an authentication issue but is actually a type definition mismatch

Reproduction steps

Reproduction Steps

  1. Create a new Amplify Gen2 project
  2. Try to use userPool as the auth mode within an NextJS action
//amplifyServerUtils.ts
import { createServerRunner } from '@aws-amplify/adapter-nextjs';
import config from '@/amplify_outputs.json';
import { cookies } from 'next/headers';
import { getCurrentUser, fetchUserAttributes, fetchAuthSession } from 'aws-amplify/auth/server';
import { AuthError, AuthUser } from 'aws-amplify/auth';

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

export interface AuthResult {
  user: AuthUser | null;
  isAuthenticated: boolean;
  token?: string;
}

export async function getServerSideUser(): Promise<AuthResult> {
  try {
    const result = await runWithAmplifyServerContext({
      nextServerContext: { cookies },
      operation: async (contextSpec) => {
        const user = await getCurrentUser(contextSpec);
        const session = await fetchAuthSession(contextSpec);
        const token = session.tokens?.idToken?.toString();
        return { user, token };
      },
    });

    return {
      user: result?.user ?? null,
      isAuthenticated: true,
      token: result?.token,
    };
  } catch (error) {
    if (error instanceof AuthError) {
      return {
        user: null,
        isAuthenticated: false,
        token: undefined,
      };
    }

    console.error('Unexpected error in getServerSideUser:', error);
    return {
      user: null,
      isAuthenticated: false,
      token: undefined,
    };
  }
}

// actions.ts
'use server';
export async function submitOrder(orderData: OrderFormData) {
  try {
    const { user, token } = await getServerSideUser();
    
    const order = await client.models.Order.create(
      orderData,
      { 
        authMode: 'userPools', // Type error: not assignable to type 'AuthMode'
        authToken: token 
      }
    );
  } catch (error) {
    console.error('Error:', error);
  }
}
@ykethan
Copy link
Member

ykethan commented Dec 18, 2024

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

@ykethan ykethan transferred this issue from aws-amplify/amplify-backend Dec 18, 2024
@Siqi-Shan
Copy link
Member

Hey @ykethan, thanks for raising the issue! We'll investigate and see what's going on there, and get to back to you for next steps

@chrisbonifacio chrisbonifacio self-assigned this Dec 18, 2024
@chrisbonifacio chrisbonifacio added to-be-reproduced Pending reproduction and removed pending-triage labels Dec 18, 2024
@iartemiev
Copy link
Member

@georgechristman - userPool is the expected runtime value as well. Here's the relevant line in the source. The library is likely defaulting the auth mode to one specified in the outputs file. My guess is if you pass in any-other-value as the authMode you'll see the same behavior.

Could you please share the contents of ./amplify/data/resource.ts with us here so we can reproduce?

@georgechristman
Copy link
Author

Hi @iartemiev, please see attached. Yes I saw the code and it's very strange. It does not function with userPool but does function with userPools.

import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
import { postConfirmation } from '../functions/auth/post-confirmation/resource';

const schema = a
  .schema({

    Order: a
      .model({
        userID: a.string().required(), // Either individual userId or dealerId
        orderNumber: a.string().required(),
        totalAmount: a.float().required(),
        listingIDs: a.string().array().required(),
        paymentStatus: a.enum(['PENDING', 'PAID', 'FAILED', 'REFUNDED']),
        paymentMethod: a.string(),
        paidAt: a.datetime(),
        createdAt: a.datetime().required(),
        updatedAt: a.datetime().required(),
      })
      .secondaryIndexes((orderIndex) => [
        orderIndex('orderNumber').name('orderNumberIndex'),
        orderIndex('paymentStatus').name('paymentStatusIndex').sortKeys(['createdAt']),
        orderIndex('userID').name('userIDIndex').sortKeys(['createdAt']),
      ])
      .identifier(['userID', 'orderNumber'])
      .authorization((allow) => [allow.ownerDefinedIn('userID').to(['create', 'read'])]),
  })
  .authorization((allow) => [allow.resource(postConfirmation)]);

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'apiKey',
    apiKeyAuthorizationMode: {
      expiresInDays: 30,
    },
  },
});

@georgechristman
Copy link
Author

After investigating, we've found that the authMode type is correct. However, the problem lies in the method used to generate the client for server components.

For server components in Next.js, you should use generateServerClientUsingCookies instead of the client-side generateClient. This is documented in the AWS Amplify documentation for Next.js server runtime integration: https://docs.amplify.aws/react/build-a-backend/data/connect-from-server-runtime/nextjs-server-runtime/

Here's an example of how to set up your configuration files:

  1. amplifyConfig.ts (shared config for client-side and server-side with custom auth for link verification and guest access)
import type { ResourcesConfig } from 'aws-amplify';
import outputs from '@/amplify_outputs.json';

export const getAmplifyConfig = (): ResourcesConfig => {
  return {
    Auth: {
      Cognito: {
        userPoolId: outputs.auth.user_pool_id,
        userPoolClientId: outputs.auth.user_pool_client_id,
        identityPoolId: outputs.auth.identity_pool_id,
        signUpVerificationMethod: 'link',
        allowGuestAccess: true,
        loginWith: {
          oauth: {
            domain: outputs.auth.oauth.domain,
            scopes: outputs.auth.oauth.scopes,
            redirectSignIn: outputs.auth.oauth.redirect_sign_in_uri,
            redirectSignOut: outputs.auth.oauth.redirect_sign_out_uri,
            responseType: outputs.auth.oauth.response_type as 'code',
          },
        },
      },
    },
  };
};
  1. amplifyClientConfig.ts (for client-side):
'use client';

import type { Schema } from '@/amplify/data/resource';
import { Amplify } from 'aws-amplify';
import { generateClient } from 'aws-amplify/api';
import outputs from '@/amplify_outputs.json';
import { getAmplifyConfig } from './amplifyConfig';

Amplify.configure(outputs);

const currentConfig = Amplify.getConfig();
const customConfig = {
  ...currentConfig,
  ...getAmplifyConfig(),
};

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

export default function AmplifyClientConfig() {
  return null;
}

export const client = generateClient<Schema>();
  1. amplifyServerConfig.ts (for server-side):
import { Amplify } from 'aws-amplify';
import outputs from '@/amplify_outputs.json';
import { getAmplifyConfig } from './amplifyConfig';

Amplify.configure(outputs);

const currentConfig = Amplify.getConfig();
const customConfig = {
  ...currentConfig,
  ...getAmplifyConfig(),
};

Amplify.configure(customConfig);

export const updatedConfig = Amplify.getConfig();
  1. amplifyServerUtils.ts (for server-side utilities):
import type { Schema } from '@/amplify/data/resource';
import { createServerRunner } from '@aws-amplify/adapter-nextjs';
import { generateServerClientUsingCookies } from '@aws-amplify/adapter-nextjs/data';
import { cookies } from 'next/headers';
import { updatedConfig } from './amplifyServerConfig';

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

export const cookieBasedClient = generateServerClientUsingCookies<Schema>({
  config: updatedConfig,
  cookies,
});

With this setup, you can use the cookieBasedClient in your server components like this:

  1. server component
import {cookieBasedClient} from '../lib/amplifyServerUtils'

export async function createOrder(order: Order) {
  const order = await cookieBasedClient.models.Order.create(
    orderData,
    { authMode: 'userPool' }
  );
}

This approach ensures that you're using the correct client for server-side operations while maintaining the user's authentication state through cookies.


Feel free to adjust or expand on this response as needed for your GitHub issue.

Copy link

This issue is now closed. Comments on closed issues are hard for our team to see.
If you need more assistance, please open a new issue that references this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants