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:
- Frontend cannot resolve the API endpoint
- CORS preflight requests fail
- Lambda crashes because Cognito authorizer claims are missing
- 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:
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:
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.jsexposed REST APIs underaws_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:
Gen2 behavior
amplify_outputs.jsondoes not haveaws_cloud_logic_custom. Custom REST APIs defined via CDK and exported throughbackend.addOutputare placed undercustom.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:
Issue 2: CORS preflight fails on proxy resource
Gen1 behavior
The gen1 CloudFormation template included a Swagger/OpenAPI definition with an explicit
OPTIONSmethod on/{proxy+}using a mock integration:Gen2 behavior
The CDK
addProxy()call does not create anOPTIONShandler by default. Gateway responses for 4XX/5XX with CORS headers only apply to error responses, not to the preflightOPTIONSrequest. The browser sends a preflight request, API Gateway has no handler for it, and the request is blocked with:Fix
Add
defaultCorsPreflightOptionsat theRestApilevel so all resources (root and proxy) get anOPTIONShandler:Issue 3: Missing Cognito User Pool authorizer on API Gateway
Gen1 behavior
The gen1 Swagger definition included a Cognito User Pool authorizer in
securityDefinitions:Applied to routes with:
API Gateway validated the JWT, decoded it, and populated
requestContext.authorizer.claimswith the user's Cognito attributes (includingcognito:groups) before the Lambda ran.Gen2 behavior
The gen2
backend.tsused IAM authorization (viaexecute-api:Invokepolicies) with no Cognito authorizer on the Gateway. Requests reached the Lambda without JWT validation at the Gateway level, sorequestContext.authorizer.claimswasundefined. ThecheckGroupmiddleware in the Lambda crashed with:Fix
Add a
CognitoUserPoolsAuthorizerand attach it to all methods:Note:
anyMethodmust befalseto avoid creating duplicate methods. TheANYmethod 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
defineFunctionin gen2 does not automatically grant Cognito permissions. The Lambda's execution role has nocognito-idp:*actions, so any call to the Cognito SDK fails with: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):
Summary of all changes required
aws_cloud_logic_custominaws-exports.jscustom.APIinamplify_outputs.jsonconfig.custom.APIOPTIONSmock in Swagger definitionOPTIONShandler on resourcesdefaultCorsPreflightOptionsonRestApisecurityDefinitionsCognitoUserPoolsAuthorizerattached to all methodsaws.cognito.signin.user.adminscope)cognito-idp:*granted via CloudFormationcognito-idp:Admin*andcognito-idp:List*actions granted viaaddToRolePolicyReproduction steps
NA
Project Identifier
No response
Log output
Details
Additional information
backend.ts
Before submitting, please confirm: