amplify-category-api
amplify-category-api copied to clipboard
Gen 2 - REST API Custom Auth - Deployment failed: Error [ValidationError]: Circular dependency between resources
Amplify CLI Version
System: OS: macOS 14.3.1 CPU: (16) arm64 Apple M3 Max Memory: 446.17 MB / 48.00 GB Shell: /bin/zsh Binaries: Node: 18.18.0 - ~/.nvm/versions/node/v18.18.0/bin/node Yarn: undefined - undefined npm: 9.8.1 - ~/.nvm/versions/node/v18.18.0/bin/npm pnpm: undefined - undefined NPM Packages: @aws-amplify/backend: 0.13.0-beta.9 @aws-amplify/backend-cli: 0.12.0-beta.10 aws-amplify: 6.0.21 aws-cdk: 2.133.0 aws-cdk-lib: 2.133.0 typescript: 5.4.2 AWS environment variables: AWS_STS_REGIONAL_ENDPOINTS = regional AWS_NODEJS_CONNECTION_REUSE_ENABLED = 1 AWS_SDK_LOAD_CONFIG = 1 No CDK environment variables%
Question
I'm attempting to setup a REST API but am struggling with setting up my auth system. I'm trying to do a very simple lambda auth function that just checks a auth token header. with restAuth
I'm thinking this is likely something I'm doing wrong... not a bug with Amplify, though I'm not totally sure.
but whenever I try deploying this I get this error:
The CloudFormation deployment has failed.
Caused By: :x: Deployment failed: Error [ValidationError]: Circular dependency between resources: [data7552DF31, apigatewaystackE9277FBE, function1351588B]
I know it's definitely related to the restAuth code, because as soon as I comment that out it deploys without issue. What could I be doing wrong?
Here's what my backend ts file looks like:
export const backend = defineBackend({
auth,
data,
myRestHandler,
getEntitlements,
loginRestHandler,
addEntitlementsByCreditRest,
restAuth
});
const apiGatewayStack = backend.createStack("apigateway-stack");
const myAPI = new LambdaRestApi(apiGatewayStack, "MyApi", {
handler: backend.myRestHandler.resources.lambda,
proxy: false,
});
// THIS IS THE CODE CAUSING ISSUES
const restAuthMod = new TokenAuthorizer(apiGatewayStack, 'user-Auth', {
handler: backend.restAuth.resources.lambda,
});
const account = myAPI.root.addResource('account')
const loginRest = new LambdaIntegration(backend.loginRestHandler.resources.lambda)
account.addResource('login')
.addMethod('POST', loginRest)
const getEntitlementsRest = new LambdaIntegration(backend.getEntitlements.resources.lambda)
account.addResource('library', {
defaultMethodOptions: {authorizationType: AuthorizationType.CUSTOM, authorizer: restAuthMod}
}).addMethod('GET', getEntitlementsRest)
account.addResource('add-to-library', {
// defaultMethodOptions: {authorizationType: AuthorizationType.CUSTOM, authorizer: restAuthMod}
}).addResource('credits')
.addMethod('POST', new LambdaIntegration(backend.addEntitlementsByCreditRest.resources.lambda))
backend.addOutput({
custom: {
apiId: myAPI.restApiId,
apiEndpoint: myAPI.url,
apiName: myAPI.restApiName,
apiRegion: Stack.of(apiGatewayStack).region,
},
});
Hi @alexwhb, I assume you've previously defined the variables like restAuth you're passing to create the backend.
Instead of fetching the needed variables like backend. restAuth, can you try directly using restAuth?
Could you share the full snippet with definitions for restAuth and if possible other variables so we can better assist you?
@phani-srikar Absolutely.
restAuth is defined in functions/rest-auth the resource.ts looks like this:
import {defineFunction} from "@aws-amplify/backend";
export const restAuth = defineFunction( )
I'm just looking up a sessionID in my dynomo table to validate it. Note data client is just the Amplify graphQL client. Also note, while this function is defined, I've never actually run it because of that deployment issue when I try to connect it to my REST API. So my implementation is a best effort first attempt. Probably some modification will be needed. and the handler.ts looks like this:
import {dataClient} from "../../utils/data-client";
export type SessionInfo = {
userId: string;
createdAt: string;
sessionId: string; // this is the userId
} | undefined
// Function to look up SessionID in the Sessions table
async function lookupSessionID(sessionID: string): Promise<SessionInfo | null> {
const {data, errors} = await dataClient.models.Session.get({id: sessionID})
if (errors) {
throw new Error(errors.join(','))
}
// const session = await getSession(sessionID)
console.log(JSON.stringify(data), data?.userSessionsId, sessionID)
if (data == null || data.userSessionsId == null) {
throw new Error("Invalid session")
}
return {
userId: data.userSessionsId,
sessionId: data.id,
createdAt: data.createdAt,
};
}
export const handler = async (
event: any
) => {
console.log(event)
const sessionInfo = await lookupSessionID(event.authorizationToken)
console.log(`RESPONSE: ${JSON.stringify(sessionInfo, null, 2)}`);
return {
principalId: 'user',
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: sessionInfo == null ? 'Deny': 'Allow',
Resource: event.methodArn
}],
},
context: sessionInfo // this is our session info to pass to the rest handler
};
};
Here's a slightly abbreviated version of my schema:
const schema = a.schema({
Session: a.model({
user: a.belongsTo('User'),
appCode: a.string().required()
}).authorization([a.allow.public('iam')]),
User: a.model({
entitlements: a.hasMany('Entitlement'),
sessions: a.hasMany('Session'),
credits: a.integer(),
}).authorization([a.allow.public('iam')]),
Entitlement: a
.model({
user: a.belongsTo('User'),
productId: a.string().required(),
squareImageUrl: a.string().required(),
bookId: a.string().required(),
}).authorization([a.allow.public('iam')]),
})
.authorization([
a.allow.resource(loginRestHandler),
a.allow.resource(restAuth).to(['query']),
a.allow.resource(getEntitlements),
]);
export type Schema = ClientSchema<typeof schema>;
export const data = defineData({
schema,
name: "MyLibrary",
functions: {},
authorizationModes: {
defaultAuthorizationMode: 'iam',
},
});
Let me know if any other details would be helpful. Happy to provide them.
Instead of fetching the needed variables like backend. restAuth, can you try directly using restAuth?
Can you try this and let us know if it works?
@phani-srikar Thanks for getting back to me. Do you mean like this:
# where rest auth is just my imported defineFunction()
const addToLibrary = account.addResource('add-to-library', {
defaultMethodOptions: {authorizationType: AuthorizationType.CUSTOM, authorizer: restAuth}
})
or are you meaning like this:
const restAuthMod = new TokenAuthorizer(apiGatewayStack, 'user-Auth', {
handler: restAuth
});
then using restAuthMod here:
const addToLibrary = account.addResource('add-to-library', {
defaultMethodOptions: {authorizationType: AuthorizationType.CUSTOM, authorizer: restAuthMod}
})
Or something else?
I tried the above two and unless I'm missing something the types are not appropriate.
Hi @alexwhb apologies for the delay. Could you share more of your code or provide a minimal sample repo that reproduces the issue so that we can troubleshoot this on our end?
I tried to use the code you shared but I'm missing some things like:
myRestHandler,
getEntitlements,
loginRestHandler,
addEntitlementsByCreditRest
UPDATE: I was able to reproduce the issue on this repo and branch:
https://github.com/chrisbonifacio/amplify-gen2-app/tree/circular-dep-error
We will report back once we've found a solution
@chrisbonifacio Awesome!!! You're the best. Thanks for looking into this. My temporary solution is to just wrap all my lambdas in auth middleware, but it'll be super nice to have auth decoupled.
@chrisbonifacio were you ever able to find a solution to this one? :)
@alexwhb could you explain what you meant here by wrapping them all in auth middleware?
@LukaASoban ya I just use the middy library which allows you to wrap your handlers in different middleware. I use that for authentication as well as input validation. The downside to this is the handlers themselves are doing the authentication every single request, so it's certainly not as efficient since there's no caching.
@LukaASoban can you try this workaround to re-arrange your resources b/w stacks - https://github.com/aws-amplify/amplify-backend/issues/1552#issuecomment-2138331128
Adding the TokenAuthorizer creates a cyclical dependency between the API GW stack and the lambda function stack. You can break the dependency by supplying the auth function ARN to the TokenAuthorizer.
const func = lambda.Function.fromFunctionArn(
apiGatewayStack,
"AuthFunction",
backend.restAuth.resources.lambda.functionArn
);
const restAuthMod = new TokenAuthorizer(apiGatewayStack, "user-Auth", {
handler: func,
});