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

aws_apigatewayv2: Can't define HTTP API Step Functions integration with JWT authorizer #28904

Closed
mrichman opened this issue Jan 28, 2024 · 4 comments · Fixed by #28982
Closed
Labels
@aws-cdk/aws-apigatewayv2 Related to Amazon API Gateway v2 bug This issue is a bug. effort/medium Medium work item – several days of effort p2

Comments

@mrichman
Copy link

Describe the bug

This should behave similarly to the REST API construct

Expected Behavior

Either of these approaches should work:

httpApi.addRoutes({
      path: '/execute',
      methods: [apigwv2.HttpMethod.POST],
      integration: StepFunctionsIntegration.startExecution(props.stateMachine)
      authorizer: jwtAuthorizer,
      authorizationType: AuthorizationType.JWT,
      credentialsRole: credentialsRole,
    };

    new CfnRoute(this, 'StepFunctionRoute', {
      apiId: httpApi.apiId,
      routeKey: 'POST /execute',
      target: `integrations/${integration.ref}`,
      authorizationType: HttpAuthorizerType.JWT,
      authorizerId: jwtAuthorizer,
    });

Current Behavior

Can't get this to compile because of error:

Type 'AwsIntegration' is missing the following properties from type 'HttpRouteIntegration': id, _bindToRoute, completeBind

Reproduction Steps

See code example

Possible Solution

No response

Additional Information/Context

No response

CDK CLI Version

2.124.0 (build 4b6724c)

Framework Version

No response

Node.js Version

v20.11.0

OS

Ubuntu 20.04

Language

TypeScript

Language Version

TypeScript (5.3.3)

Other information

No response

@mrichman mrichman added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Jan 28, 2024
@github-actions github-actions bot added the @aws-cdk/aws-apigatewayv2 Related to Amazon API Gateway v2 label Jan 28, 2024
@pahud
Copy link
Contributor

pahud commented Jan 29, 2024

Thank you for the report. Are you able to provide a full minimal working code snippets that we can just copy/paste into our IDE for reproduction?

@pahud pahud added response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. effort/medium Medium work item – several days of effort and removed needs-triage This issue or PR still needs to be triaged. labels Jan 29, 2024
@mrichman
Copy link
Author

mrichman commented Jan 29, 2024

Try this:

import { aws_apigatewayv2 as apigwv2, Stack } from 'aws-cdk-lib';
import { ConnectionType, IntegrationType } from 'aws-cdk-lib/aws-apigateway';
import { CfnIntegration, CfnRoute, CorsHttpMethod, HttpIntegrationSubtype } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import { UserPool } from 'aws-cdk-lib/aws-cognito';
import { TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { Effect, Policy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { StateMachine } from 'aws-cdk-lib/aws-stepfunctions';
import { Construct } from 'constructs';
import { createListJobsFunction } from '../lambda';

export interface HttpApiConstructProps {
  readonly userPool: UserPool;
  readonly appClientId: string;
  readonly dynamoDbTable: TableV2;
  readonly stateMachine: StateMachine;
}

export class HttpApiConstruct extends Construct {
  constructor(scope: Construct, id: string, props: HttpApiConstructProps) {
    super(scope, id);

    const region = Stack.of(this).region;
    const issuer = `https://cognito-idp.${region}.amazonaws.com/${props.userPool.userPoolId}`;

    const jwtAuthorizer = new HttpJwtAuthorizer('JwtAuthorizer', issuer, {
      authorizerName: 'MyJwtAuthorizer',
      jwtAudience: [props.appClientId],
    });

    const httpApi = new apigwv2.HttpApi(this, 'HttpApi', {
      apiName: 'My HTTP API',
      defaultAuthorizer: jwtAuthorizer,
      corsPreflight: {
        allowHeaders: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key'],
        allowMethods: [CorsHttpMethod.GET, CorsHttpMethod.DELETE, CorsHttpMethod.PUT, CorsHttpMethod.POST],
        allowOrigins: ['*'], //TODO restrict to specific domain
      },
    });

    const listJobsFunction = createListJobsFunction(this, props.dynamoDbTable); // this can be anything that returns a lambda.Function
    const listJobsIntegration = new HttpLambdaIntegration('ListJobsIntegration', listJobsFunction);

    httpApi.addRoutes({
      path: '/listJobs',
      methods: [apigwv2.HttpMethod.GET],
      integration: listJobsIntegration,
      authorizer: jwtAuthorizer,
    });

    const credentialsRole = new Role(this, 'StartExecution', {
      assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
    });

    credentialsRole.attachInlinePolicy(
      new Policy(this, 'StartExecutionPolicy', {
        statements: [
          new PolicyStatement({
            actions: ['states:StartExecution'],
            effect: Effect.ALLOW,
            resources: [props.stateMachine.stateMachineArn],
          }),
        ],
      }),
    );

    var apiGatewayRole = new Role(this, 'ApiGatewayRole', {
      assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
    });

    apiGatewayRole.addToPolicy(
      new PolicyStatement({
        effect: Effect.ALLOW,
        sid: 'AllowStepFunctionExecution',
        actions: ['states:StartExecution'],
        resources: [props.stateMachine.stateMachineArn],
      }),
    );

    const integration = new CfnIntegration(this, 'StepFunctionIntegration', {
      apiId: httpApi.apiId,
      integrationType: IntegrationType.AWS_PROXY,
      integrationSubtype: HttpIntegrationSubtype.STEPFUNCTIONS_START_EXECUTION,
      credentialsArn: apiGatewayRole.roleArn,
      requestParameters: {
        Input: '$request.body',
        StateMachineArn: props.stateMachine.stateMachineArn,
      },
      payloadFormatVersion: '1.0',
      connectionType: ConnectionType.INTERNET,
    });

    httpApi.addRoutes({
      path: '/jobs',
      methods: [apigwv2.HttpMethod.POST],
      integration: integration,   // <=== ERROR HERE
      authorizer: jwtAuthorizer,
    });

    new CfnRoute(this, 'StepFunctionRoute', {
      apiId: httpApi.apiId,
      routeKey: 'POST /jobs',
      target: `integrations/${integration.ref}`,
      authorizerId: jwtAuthorizer,  // <===== ERROR HERE
    });
  }
}

@github-actions github-actions bot removed the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Jan 29, 2024
@pahud pahud added the p2 label Jan 30, 2024
@pahud
Copy link
Contributor

pahud commented Jan 30, 2024

Hi @mrichman

Per our discussion offline, as aws-apigatewayv2-integrations is missing step function integration class, we need to declare one in our code like this and pass it all the way to the addRoute().

Consider the code below(not fully tested):

import { aws_apigatewayv2 as apigwv2, Stack } from 'aws-cdk-lib';
import { ConnectionType, IntegrationType } from 'aws-cdk-lib/aws-apigateway';
import { CfnIntegration, CfnRoute, CorsHttpMethod, HttpIntegrationSubtype, 
	HttpRouteIntegration, HttpMethod, ParameterMapping, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig,
	PayloadFormatVersion, HttpIntegrationType, IntegrationCredentials, HttpConnectionType } from 'aws-cdk-lib/aws-apigatewayv2';
import { HttpJwtAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
import { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import { UserPool } from 'aws-cdk-lib/aws-cognito';
import { TableV2 } from 'aws-cdk-lib/aws-dynamodb';
import { Effect, Policy, PolicyStatement, Role, ServicePrincipal, IRole, } from 'aws-cdk-lib/aws-iam';
import { StateMachine } from 'aws-cdk-lib/aws-stepfunctions';
import { Construct } from 'constructs';
// import { createListJobsFunction } from '../lambda';

export interface HttpApiConstructProps {
  readonly userPool: UserPool;
  readonly appClientId: string;
  readonly dynamoDbTable: TableV2;
  readonly stateMachine: StateMachine;
}

interface requestParameters {
	input: string,
	stateMachineArn: string,
}

interface HttpStepFunctionIntegrationProps {
  /**
   * The HTTP method that must be used to invoke the underlying HTTP proxy.
   * @default HttpMethod.ANY
   */
  readonly method?: HttpMethod;

  /**
   * Specifies how to transform HTTP requests before sending them to the backend
   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-parameter-mapping.html
   * @default undefined requests are sent to the backend unmodified
   */
  readonly parameterMapping?: ParameterMapping;

	readonly apiGatewayRole: IRole,
}


export class HttpStepFunctionIntegration extends HttpRouteIntegration {
  /**
   * @param id id of the underlying integration construct
   * @param props properties to configure the integration
   */
  constructor(id: string, private readonly props: HttpStepFunctionIntegrationProps) {
    super(id);
  }

  public bind(_options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig {
    return {
      method: this.props.method ?? HttpMethod.ANY,
      payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, // 1.0 is required and is the only supported format
      type: HttpIntegrationType.AWS_PROXY,
			subtype: HttpIntegrationSubtype.STEPFUNCTIONS_START_EXECUTION,
			credentials: IntegrationCredentials.fromRole(this.props.apiGatewayRole),
			connectionType: HttpConnectionType.INTERNET,
      parameterMapping: this.props.parameterMapping,
    };
  }
}

export class HttpApiConstruct extends Construct {
  constructor(scope: Construct, id: string, props: HttpApiConstructProps) {
    super(scope, id);

    const region = Stack.of(this).region;
    const issuer = `https://cognito-idp.${region}.amazonaws.com/${props.userPool.userPoolId}`;

    const jwtAuthorizer = new HttpJwtAuthorizer('JwtAuthorizer', issuer, {
      authorizerName: 'MyJwtAuthorizer',
      jwtAudience: [props.appClientId],
    });

    const httpApi = new apigwv2.HttpApi(this, 'HttpApi', {
      apiName: 'My HTTP API',
      defaultAuthorizer: jwtAuthorizer,
      corsPreflight: {
        allowHeaders: ['Content-Type', 'X-Amz-Date', 'Authorization', 'X-Api-Key'],
        allowMethods: [CorsHttpMethod.GET, CorsHttpMethod.DELETE, CorsHttpMethod.PUT, CorsHttpMethod.POST],
        allowOrigins: ['*'], //TODO restrict to specific domain
      },
    });

    const listJobsFunction = createListJobsFunction(this, props.dynamoDbTable); // this can be anything that returns a lambda.Function
    const listJobsIntegration = new HttpLambdaIntegration('ListJobsIntegration', listJobsFunction);

    httpApi.addRoutes({
      path: '/listJobs',
      methods: [apigwv2.HttpMethod.GET],
      integration: listJobsIntegration,
      authorizer: jwtAuthorizer,
    });

    const credentialsRole = new Role(this, 'StartExecution', {
      assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
    });

    credentialsRole.attachInlinePolicy(
      new Policy(this, 'StartExecutionPolicy', {
        statements: [
          new PolicyStatement({
            actions: ['states:StartExecution'],
            effect: Effect.ALLOW,
            resources: [props.stateMachine.stateMachineArn],
          }),
        ],
      }),
    );

    var apiGatewayRole = new Role(this, 'ApiGatewayRole', {
      assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
    });

    apiGatewayRole.addToPolicy(
      new PolicyStatement({
        effect: Effect.ALLOW,
        sid: 'AllowStepFunctionExecution',
        actions: ['states:StartExecution'],
        resources: [props.stateMachine.stateMachineArn],
      }),
    );

    // const integration = new CfnIntegration(this, 'StepFunctionIntegration', {
    //   apiId: httpApi.apiId,
    //   integrationType: IntegrationType.AWS_PROXY,
    //   integrationSubtype: HttpIntegrationSubtype.STEPFUNCTIONS_START_EXECUTION,
    //   credentialsArn: apiGatewayRole.roleArn,
    //   requestParameters: {
    //     Input: '$request.body',
    //     StateMachineArn: props.stateMachine.stateMachineArn,
    //   },
    //   payloadFormatVersion: '1.0',
    //   connectionType: ConnectionType.INTERNET,
    // });

  const integration = new HttpStepFunctionIntegration('StepFunctionIntegration', {
	apiGatewayRole,
	parameterMapping: new apigwv2.ParameterMapping()
		.custom('Input', '$request.body')
		.custom('StateMachineArn', props.stateMachine.stateMachineArn),
  })

    httpApi.addRoutes({
      path: '/jobs',
      methods: [apigwv2.HttpMethod.POST],
      integration,   // <=== ERROR HERE
      authorizer: jwtAuthorizer,
    });

    // new CfnRoute(this, 'StepFunctionRoute', {
    //   apiId: httpApi.apiId,
    //   routeKey: 'POST /jobs',
    //   target: `integrations/${integration.ref}`,
    //   authorizerId: jwtAuthorizer,  // <===== ERROR HERE
    // });
  }
}

It should be a general workaround before we have a separate PR that contributes the HttpStepFunctionIntegration to the aws-apigatewayv2-integrations.

@mergify mergify bot closed this as completed in #28982 Feb 28, 2024
mergify bot pushed a commit that referenced this issue Feb 28, 2024
### Issue

Closes #28904.

### Reason for this change

It is not possible to create an integration between Step Functions and HTTP API.

### Description of changes

You can create integration by `HttpStepFunctionsIntegration` class:

```ts
declare const httpApi: apigwv2.HttpApi;
declare const stateMachine: sfn.StateMachine;

const integration = new HttpStepFunctionIntegration('StepFunctionIntegration', {
	stateMachine,
  })

    httpApi.addRoutes({
      path: '/jobs',
      methods: [apigwv2.HttpMethod.POST],
      integration,
    });
```

### Description of how you validated changes

Added unit tests and integ test.

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-apigatewayv2 Related to Amazon API Gateway v2 bug This issue is a bug. effort/medium Medium work item – several days of effort p2
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants