Skip to content

(gen2-migration): generate incorrectly handles Admin queries in auth definition #14757

@dgandhi62

Description

@dgandhi62

How did you install the Amplify CLI?

No response

If applicable, what version of Node.js are you using?

No response

Amplify CLI Version

NA

What operating system are you using?

NA

Did you make any manual changes to the cloud resources managed by Amplify? Please describe the changes made.

NA

Describe the bug

When migrating an Amplify app with an AdminQueries REST API from gen1 to gen2, four issues prevent the API from functioning correctly:

  1. Frontend cannot resolve the API endpoint
  2. CORS preflight requests fail
  3. Lambda crashes because Cognito authorizer claims are missing
  4. Lambda lacks IAM permissions to call Cognito admin APIs

All four stem from differences in how gen1 and gen2 configure and expose the REST API and its backing Lambda.


Issue 1: API endpoint not discoverable by frontend

Gen1 behavior

aws-exports.js exposed REST APIs under aws_cloud_logic_custom:

{
  "aws_cloud_logic_custom": [
    {
      "name": "AdminQueries",
      "endpoint": "https://xxxxx.execute-api.us-east-1.amazonaws.com/dev",
      "region": "us-east-1"
    }
  ]
}

Frontend code resolved the endpoint with:

config.aws_cloud_logic_custom.find(api => api.name === 'AdminQueries')?.endpoint

Gen2 behavior

amplify_outputs.json does not have aws_cloud_logic_custom. Custom REST APIs defined via CDK and exported through backend.addOutput are placed under custom.API:

{
  "custom": {
    "API": {
      "AdminQueries-branch": {
        "endpoint": "https://xxxxx.execute-api.us-east-1.amazonaws.com/prod",
        "region": "us-east-1",
        "apiName": "AdminQueries-branch"
      }
    }
  }
}

Fix

Update frontend API resolution to read from the gen2 config structure:

const customApis = config.custom?.API;
const apiEntry = customApis ? Object.values(customApis)[0] : null;
const apiEndpoint = apiEntry?.endpoint;

Issue 2: CORS preflight fails on proxy resource

Gen1 behavior

The gen1 CloudFormation template included a Swagger/OpenAPI definition with an explicit OPTIONS method on /{proxy+} using a mock integration:

"options": {
  "x-amazon-apigateway-integration": {
    "type": "mock",
    "requestTemplates": {
      "application/json": "{\"statusCode\": 200}"
    },
    "responses": {
      "default": {
        "statusCode": "200",
        "responseParameters": {
          "method.response.header.Access-Control-Allow-Methods": "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'",
          "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
          "method.response.header.Access-Control-Allow-Origin": "'*'"
        }
      }
    }
  }
}

Gen2 behavior

The CDK addProxy() call does not create an OPTIONS handler by default. Gateway responses for 4XX/5XX with CORS headers only apply to error responses, not to the preflight OPTIONS request. The browser sends a preflight request, API Gateway has no handler for it, and the request is blocked with:

Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

Fix

Add defaultCorsPreflightOptions at the RestApi level so all resources (root and proxy) get an OPTIONS handler:

const api = new RestApi(stack, 'RestApi', {
  restApiName: 'AdminQueries',
  defaultCorsPreflightOptions: {
    allowOrigins: Cors.ALL_ORIGINS,
    allowMethods: Cors.ALL_METHODS,
    allowHeaders: [
      'Content-Type',
      'X-Amz-Date',
      'Authorization',
      'X-Api-Key',
      'X-Amz-Security-Token',
    ],
  },
});

Issue 3: Missing Cognito User Pool authorizer on API Gateway

Gen1 behavior

The gen1 Swagger definition included a Cognito User Pool authorizer in securityDefinitions:

"securityDefinitions": {
  "Cognito": {
    "type": "apiKey",
    "name": "Authorization",
    "in": "header",
    "x-amazon-apigateway-authtype": "cognito_user_pools",
    "x-amazon-apigateway-authorizer": {
      "providerARNs": ["arn:aws:cognito-idp:...:userpool/..."],
      "type": "cognito_user_pools"
    }
  }
}

Applied to routes with:

"security": [
  { "Cognito": ["aws.cognito.signin.user.admin"] }
]

API Gateway validated the JWT, decoded it, and populated requestContext.authorizer.claims with the user's Cognito attributes (including cognito:groups) before the Lambda ran.

Gen2 behavior

The gen2 backend.ts used IAM authorization (via execute-api:Invoke policies) with no Cognito authorizer on the Gateway. Requests reached the Lambda without JWT validation at the Gateway level, so requestContext.authorizer.claims was undefined. The checkGroup middleware in the Lambda crashed with:

Cannot read properties of undefined (reading 'claims')

Fix

Add a CognitoUserPoolsAuthorizer and attach it to all methods:

const cognitoAuthorizer = new CognitoUserPoolsAuthorizer(stack, 'CognitoAuthorizer', {
  cognitoUserPools: [backend.auth.resources.userPool],
  identitySource: 'method.request.header.Authorization',
});

const methodOptions = {
  authorizationType: AuthorizationType.COGNITO,
  authorizer: cognitoAuthorizer,
  authorizationScopes: ['aws.cognito.signin.user.admin'],
};

root.addMethod('ANY', lambdaIntegration, methodOptions);
const proxy = root.addProxy({
  anyMethod: false,
  defaultIntegration: lambdaIntegration,
});
proxy.addMethod('ANY', lambdaIntegration, methodOptions);

Note: anyMethod must be false to avoid creating duplicate methods. The ANY method is added explicitly with the authorizer.


Issue 4: Lambda missing IAM permissions for Cognito admin APIs

Gen1 behavior

The gen1 CloudFormation template for the Lambda function included an IAM policy granting cognito-idp:* (or specific admin actions) on the User Pool. This was automatically provisioned as part of the AdminQueries function setup.

Gen2 behavior

defineFunction in gen2 does not automatically grant Cognito permissions. The Lambda's execution role has no cognito-idp:* actions, so any call to the Cognito SDK fails with:

User: arn:aws:sts::...:assumed-role/.../AdminQueriesd29134db-... is not authorized to perform:
cognito-idp:ListUsers on resource: arn:aws:cognito-idp:...:userpool/... because no identity-based
policy allows the cognito-idp:ListUsers action

Additionally, during migration the Lambda may need access to both the gen1 and gen2 User Pools if the gen1 pool is still in use.

Fix

Grant the Lambda explicit Cognito permissions on the relevant User Pool(s):

backend.AdminQueriesd29134db.resources.lambda.addToRolePolicy(
  new PolicyStatement({
    actions: [
      'cognito-idp:AdminAddUserToGroup',
      'cognito-idp:AdminConfirmSignUp',
      'cognito-idp:AdminDisableUser',
      'cognito-idp:AdminEnableUser',
      'cognito-idp:AdminGetUser',
      'cognito-idp:AdminListGroupsForUser',
      'cognito-idp:AdminRemoveUserFromGroup',
      'cognito-idp:AdminUserGlobalSignOut',
      'cognito-idp:ListGroups',
      'cognito-idp:ListUsers',
      'cognito-idp:ListUsersInGroup',
    ],
    resources: [
      backend.auth.resources.userPool.userPoolArn,
      // Include gen1 User Pool ARN if still in use during migration
      'arn:aws:cognito-idp:<region>:<account>:userpool/<gen1-pool-id>',
    ],
  })
);

Summary of all changes required

Area Gen1 Gen2 (before fix) Gen2 (after fix)
API endpoint config aws_cloud_logic_custom in aws-exports.js custom.API in amplify_outputs.json Frontend reads from config.custom.API
CORS preflight Explicit OPTIONS mock in Swagger definition No OPTIONS handler on resources defaultCorsPreflightOptions on RestApi
Authorization Cognito User Pool authorizer in Swagger securityDefinitions IAM only, no Cognito authorizer CognitoUserPoolsAuthorizer attached to all methods
Token type Access token (carries aws.cognito.signin.user.admin scope) N/A (no authorizer) Access token (same as gen1)
Lambda Cognito IAM cognito-idp:* granted via CloudFormation No Cognito permissions on Lambda role Explicit cognito-idp:Admin* and cognito-idp:List* actions granted via addToRolePolicy

Reproduction steps

NA

Project Identifier

No response

Log output

Details
# Put your logs below this line


Additional information

backend.ts

import { auth } from './auth/resource';
import { AdminQueriesd29134db } from './function/AdminQueriesd29134db/resource';
import {
  RestApi,
  LambdaIntegration,
  AuthorizationType,
  CognitoUserPoolsAuthorizer,
  Cors,
  ResponseType,
} from 'aws-cdk-lib/aws-apigateway';
import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { defineBackend } from '@aws-amplify/backend';
import { Stack, Duration } from 'aws-cdk-lib';

const backend = defineBackend({
  auth,
  AdminQueriesd29134db,
});
const branchName = process.env.AWS_BRANCH ?? 'sandbox';
const AdminQueriesStack = backend.createStack('rest-api-stack-AdminQueries');
const AdminQueriesApi = new RestApi(AdminQueriesStack, 'RestApi', {
  restApiName: `AdminQueries-${branchName}`,
  defaultCorsPreflightOptions: {
    allowOrigins: Cors.ALL_ORIGINS,
    allowMethods: Cors.ALL_METHODS,
    allowHeaders: [
      'Content-Type',
      'X-Amz-Date',
      'Authorization',
      'X-Api-Key',
      'X-Amz-Security-Token',
    ],
  },
});
AdminQueriesApi.addGatewayResponse('Default4XX', {
  type: ResponseType.DEFAULT_4XX,
  responseHeaders: {
    'Access-Control-Allow-Origin': "'*'",
    'Access-Control-Allow-Headers':
      "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
    'Access-Control-Allow-Methods': "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'",
    'Access-Control-Expose-Headers': "'Date,X-Amzn-ErrorType'",
  },
});
AdminQueriesApi.addGatewayResponse('Default5XX', {
  type: ResponseType.DEFAULT_5XX,
  responseHeaders: {
    'Access-Control-Allow-Origin': "'*'",
    'Access-Control-Allow-Headers':
      "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
    'Access-Control-Allow-Methods': "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'",
    'Access-Control-Expose-Headers': "'Date,X-Amzn-ErrorType'",
  },
});
const AdminQueriesd29134dbIntegration = new LambdaIntegration(
  backend.AdminQueriesd29134db.resources.lambda
);

const cognitoAuthorizer = new CognitoUserPoolsAuthorizer(
  AdminQueriesStack,
  'CognitoAuthorizer',
  {
    cognitoUserPools: [backend.auth.resources.userPool],
    identitySource: 'method.request.header.Authorization',
  }
);

const methodOptions = {
  authorizationType: AuthorizationType.COGNITO,
  authorizer: cognitoAuthorizer,
  authorizationScopes: ['aws.cognito.signin.user.admin'],
};
const gen1AdminQueriesApi = RestApi.fromRestApiAttributes(
  AdminQueriesStack,
  'Gen1AdminQueriesApi',
  {
    restApiId: '4q4ef0gdj0',
    rootResourceId: 'fgk8sdjzck',
  }
);
const gen1AdminQueriesPolicy = new Policy(
  AdminQueriesStack,
  'Gen1AdminQueriesPolicy',
  {
    statements: [
      new PolicyStatement({
        actions: ['execute-api:Invoke'],
        resources: [
          `${gen1AdminQueriesApi.arnForExecuteApi('POST', '/*')}`,
          `${gen1AdminQueriesApi.arnForExecuteApi('GET', '/*')}`,
          `${gen1AdminQueriesApi.arnForExecuteApi('PUT', '/*')}`,
          `${gen1AdminQueriesApi.arnForExecuteApi('DELETE', '/*')}`,
        ],
      }),
    ],
  }
);
backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(
  gen1AdminQueriesPolicy
);
const root = AdminQueriesApi.root;
root.addMethod('ANY', AdminQueriesd29134dbIntegration, methodOptions);
const proxy = root.addProxy({
  anyMethod: false,
  defaultIntegration: AdminQueriesd29134dbIntegration,
});
proxy.addMethod('ANY', AdminQueriesd29134dbIntegration, methodOptions);
// /{proxy+} - all authenticated users
backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(
  new Policy(AdminQueriesStack, 'proxyAuthPolicy', {
    statements: [
      new PolicyStatement({
        actions: ['execute-api:Invoke'],
        resources: [
          AdminQueriesApi.arnForExecuteApi('POST', '/{proxy+}'),
          AdminQueriesApi.arnForExecuteApi('POST', '/{proxy+}/*'),
          AdminQueriesApi.arnForExecuteApi('GET', '/{proxy+}'),
          AdminQueriesApi.arnForExecuteApi('GET', '/{proxy+}/*'),
          AdminQueriesApi.arnForExecuteApi('PUT', '/{proxy+}'),
          AdminQueriesApi.arnForExecuteApi('PUT', '/{proxy+}/*'),
          AdminQueriesApi.arnForExecuteApi('DELETE', '/{proxy+}'),
          AdminQueriesApi.arnForExecuteApi('DELETE', '/{proxy+}/*'),
        ],
      }),
    ],
  })
);
backend.addOutput({
  custom: {
    API: {
      [AdminQueriesApi.restApiName]: {
        endpoint: AdminQueriesApi.url.slice(0, -1),
        region: Stack.of(AdminQueriesApi).region,
        apiName: AdminQueriesApi.restApiName,
      },
    },
  },
});
const cfnUserPool = backend.auth.resources.cfnResources.cfnUserPool;
cfnUserPool.usernameAttributes = undefined;
cfnUserPool.policies = {
  passwordPolicy: {
    minimumLength: 8,
    requireUppercase: false,
    requireLowercase: false,
    requireNumbers: false,
    requireSymbols: false,
    temporaryPasswordValidityDays: 7,
  },
};
const cfnIdentityPool = backend.auth.resources.cfnResources.cfnIdentityPool;
cfnIdentityPool.allowUnauthenticatedIdentities = false;
const userPool = backend.auth.resources.userPool;
userPool.addClient('NativeAppClient', {
  refreshTokenValidity: Duration.days(30),
  enableTokenRevocation: true,
  enablePropagateAdditionalUserContextData: false,
  authSessionValidity: Duration.minutes(3),
  disableOAuth: true,
  generateSecret: false,
});
backend.AdminQueriesd29134db.resources.cfnResources.cfnFunction.functionName = `AdminQueriesd29134db-${branchName}`;

// Grant Lambda permission to perform Cognito admin actions
backend.AdminQueriesd29134db.resources.lambda.addToRolePolicy(
  new PolicyStatement({
    actions: [
      'cognito-idp:AdminAddUserToGroup',
      'cognito-idp:AdminConfirmSignUp',
      'cognito-idp:AdminDisableUser',
      'cognito-idp:AdminEnableUser',
      'cognito-idp:AdminGetUser',
      'cognito-idp:AdminListGroupsForUser',
      'cognito-idp:AdminRemoveUserFromGroup',
      'cognito-idp:AdminUserGlobalSignOut',
      'cognito-idp:ListGroups',
      'cognito-idp:ListUsers',
      'cognito-idp:ListUsersInGroup',
    ],
    resources: [
      backend.auth.resources.userPool.userPoolArn,
      'arn:aws:cognito-idp:us-east-1:615368094448:userpool/us-east-1_krl1dUgIo',
    ],
  })
);

Before submitting, please confirm:

  • I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
  • I have removed any sensitive information from my code snippets and submission.

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions