fix(cloudformation): provision AWS::ApiGatewayV2::Authorizer and wire Route.AuthorizerId - #1760
Conversation
… Route.AuthorizerId CloudFormationResourceProvisioner had no case for AWS::ApiGatewayV2::Authorizer, so the resource fell through the default stub branch and never reached ApiGatewayV2Service — the same failure mode floci-io#788/floci-io#796 fixed for the v1 AWS::ApiGateway::Authorizer type. Stacks reported CREATE_COMPLETE, but apigatewayv2 get-authorizers came back empty and routes referencing the authorizer carried no authorizerId, so a JWT-authorized route silently served every request unauthenticated. Adds provisionApiGatewayV2Authorizer, modeled on the neighboring v2 Route/Integration/Stage provisioners, mapping the CFN PascalCase properties (Name, AuthorizerType, IdentitySource, JwtConfiguration, AuthorizerUri, AuthorizerPayloadFormatVersion, AuthorizerResultTtlInSeconds, EnableSimpleResponses) onto the existing ApiGatewayV2Service.createAuthorizer camelCase request shape. Also fixes provisionApiGatewayV2Route, which built its request map without reading AuthorizerId at all even though ApiGatewayV2Service.createRoute/updateRoute already handle it. Fixes floci-io#1758.
|
| Filename | Overview |
|---|---|
| src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java | Core fix: adds provision/delete cases for AWS::ApiGatewayV2::Authorizer and wires AuthorizerId into the Route provisioner; the new resolveIdentitySource helper correctly handles both scalar and array CFN forms. |
| src/main/java/io/github/hectorvent/floci/services/apigateway/ApiGatewayExecuteController.java | Extends JWT enforcement to support $request.querystring.* identity sources by threading UriInfo through enforceJwtAuthorizer and extractToken; change is minimal and consistent with the existing header path. |
| src/test/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationIntegrationTest.java | Adds four integration tests covering authorizer provisioning, Ref and Fn::GetAtt wiring, scalar IdentitySource, and scoped delete against an out-of-band API; IdentitySource is now asserted in GetAuthorizers responses. |
| src/test/java/io/github/hectorvent/floci/services/apigatewayv2/HttpApiJwtAuthorizerQuerystringTest.java | New regression test for querystring-based JWT token extraction; validates that a valid token in the query string is accepted (502, not 401), a missing token is rejected (401), and a header-only token does not satisfy a querystring identity source. |
Reviews (8): Last reviewed commit: "chore: retrigger CI" | Re-trigger Greptile
…horizer resolveStringListOrEmpty silently returned an empty list when a template supplied IdentitySource as a bare scalar string rather than the documented array form, dropping the identity source instead of persisting it. ApiGatewayV2Service.createAuthorizer/updateAuthorizer already accept a scalar string for identitySource (identitySourceRaw instanceof String), so the CFN provisioner was stricter than the service it calls. Adds resolveIdentitySource, a local resolver that accepts both a scalar string and an array. Adds a regression test asserting the scalar form is persisted, alongside strengthening the existing test to assert IdentitySource itself (previously unchecked). Addresses review feedback on floci-io#1760.
…horizer resolveStringListOrEmpty silently returned an empty list when a template supplied IdentitySource as a bare scalar string rather than the documented array form, dropping the identity source instead of persisting it. ApiGatewayV2Service.createAuthorizer/updateAuthorizer already accept a scalar string for identitySource (identitySourceRaw instanceof String), so the CFN provisioner was stricter than the service it calls. Adds resolveIdentitySource, a local resolver that accepts both a scalar string and an array. Adds a regression test asserting the scalar form is persisted, alongside strengthening the existing test to assert IdentitySource itself (previously unchecked). Addresses review feedback on floci-io#1760.
|
Good catch — fixed in f252ba5.
One correction on the framing: the AWS docs for |
|
@hectorvent can you approve this one for CI? I have a follow up to this one once it merges to fix the SAM side of this. |
|
@hectorvent ready for you to review/merge as you have time. Thanks! |
| } else { | ||
| authorizer = apiGatewayV2Service.updateAuthorizer(region, apiId, r.getPhysicalId(), req); | ||
| } | ||
| r.setPhysicalId(authorizer.getAuthorizerId()); |
There was a problem hiding this comment.
@rogueserenity thanks for building out the CloudFormation support here. Could we put AuthorizerId in the resource attributes as well as the physical ID? Ref works, but Fn::GetAtt Authorizer.AuthorizerId does not, so route templates receive the literal string and fail later. A small GetAtt route case would cover it.
There was a problem hiding this comment.
Fixed in def4f3b — provisionApiGatewayV2Authorizer now also stores r.getAttributes().put("AuthorizerId", authorizer.getAuthorizerId()) alongside the physical id, following the same pattern every other resource type already uses (S3, SQS, VPC, etc.) to make GetAtt resolvable. Fn::GetAtt Authorizer.AuthorizerId reads that attributes map, which was never populated before — only Ref (which reads the physical id) worked.
Added createStack_apiGatewayV2RouteResolvesAuthorizerIdViaGetAtt, a variant of the existing wired-route test that uses Fn::GetAtt instead of Ref for the route's AuthorizerId, asserting it resolves to the real id rather than the literal placeholder string. Verified the test actually catches the regression by temporarily removing the attribute and confirming it fails.
There was a problem hiding this comment.
Thanks for adding that and for the regression coverage. Fn::GetAtt should work properly now.
| Map<String, Object> req = new HashMap<>(); | ||
| req.put("name", resolveOptional(props, "Name", engine)); | ||
| req.put("authorizerType", resolveOptional(props, "AuthorizerType", engine)); | ||
| req.put("identitySource", resolveIdentitySource(props, "IdentitySource", engine)); |
There was a problem hiding this comment.
One runtime follow-up: HTTP API JWT authorizers support $request.querystring.* sources too, but the runtime only passes header information into token extraction. A template using a query-string token will 401 even with a valid JWT. Could we pass query parameters through and add a quick execution-level test?
There was a problem hiding this comment.
Fixed in 34d238b — extractToken only ever received HttpHeaders, with no query-parameter access at all, so any IdentitySource like $request.querystring.token was silently unusable and a request using it always 401'd. Threaded UriInfo through enforceJwtAuthorizer/extractToken (it was already available one call site up, in dispatchV2) and added a $request.querystring. branch mirroring the one enforceRequestAuthorizerV2 already has for Lambda REQUEST authorizers.
Added HttpApiJwtAuthorizerQuerystringTest — an execution-level test hitting a real JWT-protected route with the token only in the query string (asserting it gets past authorization), plus checks that a request with no token anywhere 401s, and that a token in the Authorization header alone doesn't satisfy an authorizer configured for a querystring-only identity source. Verified the primary case fails without the fix.
There was a problem hiding this comment.
Nice, thanks for wiring that through the execution path and adding the test. This handles the query-string JWT case I was concerned about.
| case "AWS::ApiGateway::Deployment" -> provisionApiGatewayDeployment(resource, properties, engine, region); | ||
| case "AWS::ApiGateway::Stage" -> provisionApiGatewayStage(resource, properties, engine, region); | ||
| case "AWS::ApiGatewayV2::Api" -> provisionApiGatewayV2Api(resource, properties, engine, region, accountId, stackName); | ||
| case "AWS::ApiGatewayV2::Authorizer" -> provisionApiGatewayV2Authorizer(resource, properties, engine, region); |
There was a problem hiding this comment.
One cleanup detail: the generic delete path does not know about this resource. If ApiId comes from a parameter rather than a stack-owned API, deleting the stack reports success but leaves the authorizer behind. Could we retain the API ID on the resource and add a scoped delete path for it?
There was a problem hiding this comment.
Fixed in def4f3b — added a scoped case for AWS::ApiGatewayV2::Authorizer in delete(StackResource, region), following the same shape already used for AWS::EKS::Nodegroup (a resource needing more context than type+physicalId to delete). ApiId is now stored as a resource attribute during provisioning and read back here to call apiGatewayV2Service.deleteAuthorizer(region, apiId, physicalId) directly — previously the generic type/physicalId switch had no case for this type at all and silently no-op'd.
Added deleteStack_apiGatewayV2AuthorizerOnNonStackOwnedApiIsRemoved, which deliberately creates the API out-of-band (not as a stack resource) so AWS::ApiGatewayV2::Api's own cascading delete can't be the thing removing the authorizer — that's exactly the scenario you flagged, and it isolates the new scoped delete path as the only thing that could make the test pass. Verified it fails without the fix.
There was a problem hiding this comment.
Thanks for tracking the API ID and covering the external API case. This looks right now.
…te for AWS::ApiGatewayV2::Authorizer - Fn::GetAtt Authorizer.AuthorizerId now resolves: provisionApiGatewayV2Authorizer only set the physical id, never the resource's attributes map, so GetAtt (which reads a separate map from Ref) fell back to a literal placeholder string. - Stack deletion now actually removes the authorizer: the generic type/physicalId delete switch had no case for AWS::ApiGatewayV2::Authorizer at all and silently no-op'd. Added a scoped case in delete(StackResource, region) following the same shape already used for AWS::EKS::Nodegroup (a resource needing extra context beyond type+physicalId, stored as an attribute during provisioning). Both fixes store ApiId as a resource attribute during provisioning, since it's needed by the new delete path and wasn't previously retained anywhere.
…JWT authorizers extractToken only checked HttpHeaders — it had no query-parameter access at all, so any IdentitySource entry like $request.querystring.token was silently unusable, and a request using a valid token there always 401'd. Threads UriInfo through enforceJwtAuthorizer/extractToken and adds a $request.querystring. branch mirroring the one enforceRequestAuthorizerV2 already has for Lambda REQUEST authorizers.
…thorizer-cfn # Conflicts: # src/test/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationIntegrationTest.java
…thorizer-cfn # Conflicts: # src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java
|
Hello @rogueserenity, Thanks for the PR! Could you please update the commit history to remove the Co-authored-by: Claude <...> line from the commit message? We prefer to keep the attribution focused on human contributors for this project. Once that's cleaned up, I'm happy to take another look! |
08106bb to
e9b3553
Compare
|
Done — history rewritten in e9b3553 to drop the Only the two commits that carried it were touched ( Audit for the record: comes back empty across all 7 commits. CI is re-running now. |
|
@hectorvent this is ready for you to merge |
| // the authorizer id) — same shape as the Nodegroup case above. Without this, the generic | ||
| // type/physicalId delete path has no case for this type at all and silently no-ops, | ||
| // leaving the authorizer behind in AWS after the stack reports deleted. | ||
| if ("AWS::ApiGatewayV2::Authorizer".equals(resourceType)) { |
There was a problem hiding this comment.
Heads up before this gets merged — GitHub reports the branch as conflicting with main (mergeable: CONFLICTING, mergeStateStatus: DIRTY), so it can't go in as-is despite the green checks.
Exactly one file collides, and it's right here. main has since gained an AWS::Events::Rule case in delete(StackResource, region) in the same spot this PR adds its AWS::ApiGatewayV2::Authorizer case — both immediately after the AWS::EKS::Nodegroup block, and both ending on the same shared return; } lines, which is what makes git treat it as a conflict rather than two independent insertions.
The resolution is to keep both blocks, each closing with its own return;:
if ("AWS::Events::Rule".equals(resourceType)) {
deleteEventBridgeRuleSafe(resource.getPhysicalId(),
resource.getAttributes().get("EventBusName"), region);
return;
}
if ("AWS::ApiGatewayV2::Authorizer".equals(resourceType)) {
String apiId = resource.getAttributes().get("ApiId");
...
return;
}Worth resolving deliberately rather than by reflex: because that trailing return; } is shared context, taking either side wholesale still compiles and CI still goes green — it just silently drops the other side's delete path, either main's custom-bus rule cleanup or the authorizer cleanup this PR exists to add. That failure is invisible until a stack delete leaves something behind.
I checked the rest of the merge and nothing else collides: resolveOptional, resolveStringListOrEmpty, and the createAuthorizer / updateAuthorizer / deleteAuthorizer signatures are all unchanged on main, so no other fixups should be needed after resolving this hunk.
The three things I raised earlier — the AuthorizerId attribute, the $request.querystring.* identity source, and this scoped delete — all look right at e9b35533.
There was a problem hiding this comment.
Conflict resolved. Ready for merge.
…thorizer-cfn # Conflicts: # src/main/java/io/github/hectorvent/floci/services/cloudformation/CloudFormationResourceProvisioner.java
The Build and Test job failed on a single flaky error in RuntimeApiServerTest.extensionRegister_racedAgainstStop_eventuallyDeliversShutdown (EOFException on an HTTP/2 read while racing extension registration against server stop) — 1 of 8325 tests. This branch changes no Lambda code; that test is from floci-io#1773 and lives on main independently. Passes locally on repeat runs.
hectorvent
left a comment
There was a problem hiding this comment.
Thanks @rogueserenity @abanna
|
🎉 This PR is included in version 1.6.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
# [1.6.0](floci-io/floci@1.5.34...1.6.0) (2026-08-06) ### Bug Fixes * **1790:** better populate describe alarm responses ([floci-io#1792](floci-io#1792)) ([fcb22be](floci-io@fcb22be)) * **apigateway:** fall through to less specific resources on method mi… ([floci-io#1630](floci-io#1630)) ([96c7aa6](floci-io@96c7aa6)), closes [floci-io#1547](floci-io#1547) * **apigateway:** import authorizers and security requirements from OpenAPI ([floci-io#1798](floci-io#1798)) ([91e06eb](floci-io@91e06eb)) * **apigateway:** use last duplicate header value ([floci-io#1806](floci-io#1806)) ([6a214e8](floci-io@6a214e8)) * **athena:** report the result CSV object key as OutputLocation ([floci-io#1895](floci-io#1895)) ([418ec1b](floci-io@418ec1b)), closes [floci-io#1752](floci-io#1752) * **cloudformation:** AWS::IAM::ManagedPolicy exposes PolicyArn for Fn::GetAtt ([floci-io#2056](floci-io#2056)) ([f719033](floci-io@f719033)) * **cloudformation:** keep uniqueness suffix when truncating generated resource names ([floci-io#1802](floci-io#1802)) ([48b2139](floci-io@48b2139)), closes [floci-io#1825](floci-io#1825) * **cloudformation:** preserve subnet public IP setting ([floci-io#2031](floci-io#2031)) ([b62d1e7](floci-io@b62d1e7)) * **cloudformation:** provision AWS::ApiGatewayV2::Authorizer and wire Route.AuthorizerId ([floci-io#1760](floci-io#1760)) ([7f51c74](floci-io@7f51c74)), closes [floci-io#1773](floci-io#1773) * **cloudformation:** resolve Fn::GetAtt [Vpc, DefaultSecurityGroup] ([floci-io#1977](floci-io#1977)) ([bdad066](floci-io@bdad066)), closes [floci-io#1976](floci-io#1976) * **cloudformation:** treat delete of an already-removed DynamoDB table or Lambda function as idempotent ([floci-io#1803](floci-io#1803)) ([5cb26ff](floci-io@5cb26ff)) * **codebuild:** stabilize start and retry responses ([floci-io#2052](floci-io#2052)) ([fa96b79](floci-io@fa96b79)) * **cognito:** reject unconfirmed users in SRP auth ([floci-io#2027](floci-io#2027)) ([733c72d](floci-io@733c72d)) * **dockerfile:** ensure docker-java compatibility ([floci-io#2096](floci-io#2096)) ([7ade290](floci-io@7ade290)) * **dynamodb:** resolve nested document paths in ADD and DELETE update actions ([floci-io#1908](floci-io#1908)) ([c052259](floci-io@c052259)) * **ec2:** DescribeKeyPairs returns InvalidKeyPair.NotFound for missing names/ids ([floci-io#1932](floci-io#1932)) ([7d96106](floci-io@7d96106)), closes [floci-io#1911](floci-io#1911) * **ec2:** return AWS-parity MissingParameter for CreateSubnet without VpcId ([floci-io#2094](floci-io#2094)) ([88408d8](floci-io@88408d8)), closes [floci-io#2089](floci-io#2089) * **ecs:** resolve Secrets Manager JSON-key selectors in task definition secrets ([floci-io#2133](floci-io#2133)) ([e3c180b](floci-io@e3c180b)), closes [floci-io#1912](floci-io#1912) * **lambda:** carry the owning account into published version snapshots ([floci-io#2041](floci-io#2041)) ([f4e8bd8](floci-io@f4e8bd8)), closes [floci-io#2040](floci-io#2040) * **lambda:** report resolved executed version ([floci-io#2026](floci-io#2026)) ([2889fbd](floci-io@2889fbd)) * **lambda:** reset volatile DynamoDB Streams ESM checkpoints on restart ([floci-io#2077](floci-io#2077)) ([847c30a](floci-io@847c30a)) * **pipes:** register kafka-clients reflection metadata for native image ([floci-io#2068](floci-io#2068)) ([ab73379](floci-io@ab73379)) * **s3:** accept a percent-encoded bucket/key separator in copy sources ([floci-io#2060](floci-io#2060)) ([a8ed218](floci-io@a8ed218)), closes [floci-io#2038](floci-io#2038) * **s3:** apply CreateBucketConfiguration tags on bucket creation ([floci-io#2115](floci-io#2115)) ([79031a2](floci-io@79031a2)) * **s3:** include EventBridgeConfiguration in GetBucketNotification response ([floci-io#2072](floci-io#2072)) ([b8edd37](floci-io@b8edd37)) * **s3:** recognize virtual-hosted-style requests over HTTP/2 ([floci-io#1954](floci-io#1954)) ([5f7d4d2](floci-io@5f7d4d2)), closes [floci-io#1866](floci-io#1866) * **ssm:** report invalid batch parameter names ([floci-io#2075](floci-io#2075)) ([04873bf](floci-io@04873bf)) * **tls:** reuse the persisted self-signed certificate across restarts ([floci-io#1799](floci-io#1799)) ([a4356f2](floci-io@a4356f2)) ### Features * Add AWSCloudFormationReadOnlyAccess as a managed policy. ([floci-io#2057](floci-io#2057)) ([0142dc4](floci-io@0142dc4)) * **apigateway:** support floci:override-id for v1 and v2 ([floci-io#2045](floci-io#2045)) ([02385da](floci-io@02385da)), closes [floci-io#1593](floci-io#1593) * **bedrock:** add proxy backend for real LLM responses via OpenAI-compatible API ([floci-io#1789](floci-io#1789)) ([875e0f2](floci-io@875e0f2)) * **cloudcontrol:** include EC2 resource tags ([floci-io#1933](floci-io#1933)) ([205a8f1](floci-io@205a8f1)) * **cloudformation:** provision AWS::EC2::LaunchTemplate ([floci-io#1973](floci-io#1973)) ([4e0a815](floci-io@4e0a815)), closes [floci-io#1971](floci-io#1971) * **cloudformation:** provision AWS::EC2::VPCGatewayAttachment ([floci-io#1972](floci-io#1972)) ([1a99721](floci-io@1a99721)), closes [floci-io#1970](floci-io#1970) * **cloudformation:** provision AWS::SecretsManager::SecretTargetAttachment ([floci-io#1804](floci-io#1804)) ([62a576c](floci-io@62a576c)) * **cloudformation:** support AWS::Events::EventBus and EventBusPolicy ([floci-io#1794](floci-io#1794)) ([76f602f](floci-io@76f602f)) * **cognito:** implement ResendConfirmationCode ([floci-io#2071](floci-io#2071)) ([bb7c45c](floci-io@bb7c45c)), closes [floci-io#2067](floci-io#2067) * **ec2:** added attach and detach volume support ([floci-io#1787](floci-io#1787)) ([2c74329](floci-io@2c74329)) * **ec2:** return an empty set for DescribeVpnGateways ([floci-io#1975](floci-io#1975)) ([3baf4f4](floci-io@3baf4f4)), closes [floci-io#1974](floci-io#1974) * **iam:** list entities attached to managed policies ([floci-io#1808](floci-io#1808)) ([78ba5b3](floci-io@78ba5b3)) * **iam:** seed Amazon Bedrock managed policies ([floci-io#2034](floci-io#2034)) ([5874348](floci-io@5874348)), closes [floci-io#1559](floci-io#1559) [floci-io#1216](floci-io#1216) * **iam:** seed the managed policies CDK bootstrap and the scenario stacks attach ([floci-io#2064](floci-io#2064)) ([43aea09](floci-io@43aea09)), closes [floci-io#2059](floci-io#2059) [floci-io#2057](floci-io#2057) [floci-io#2057](floci-io#2057) * **kms:** implement ListKeyPolicies ([floci-io#2046](floci-io#2046)) ([760115d](floci-io@760115d)), closes [floci-io#1528](floci-io#1528) * **lambda:** implement the Lambda Extensions API ([floci-io#1773](floci-io#1773)) ([9f0dec9](floci-io@9f0dec9)) * **lambda:** support extra /etc/hosts entries on Lambda launch ([floci-io#2073](floci-io#2073)) ([b543aa4](floci-io@b543aa4)) * **mwaa:** add Amazon MWAA emulation backed by real Airflow (LocalExecutor) ([floci-io#2086](floci-io#2086)) ([7e4e3e8](floci-io@7e4e3e8)) * **release:** one-button release cut from main and ECR Public versioned publishing ([floci-io#2127](floci-io#2127)) ([61011c0](floci-io@61011c0)) * **rum:** add CloudWatch RUM app-monitor service ([floci-io#1797](floci-io#1797)) ([aadc93e](floci-io@aadc93e)) * **sqs:** retain MessageGroupId on standard queue messages ([floci-io#1891](floci-io#1891)) ([a01b707](floci-io@a01b707)), closes [floci-io#1496](floci-io#1496) ### Performance Improvements * **docker:** stop duplicating the native binary into a second image layer ([floci-io#2069](floci-io#2069)) ([9e90e5c](floci-io@9e90e5c))
Summary
Fixes #1758.
CloudFormationResourceProvisionerhad nocaseforAWS::ApiGatewayV2::Authorizer(onlyAWS::ApiGateway::Authorizer, the v1/REST type, added in #796 for #788). The v2 resource fell through the default branch and was never provisioned —apiGatewayV2Service.createAuthorizerwas never called, soapigatewayv2 get-authorizerscame back empty regardless of what the template declared.provisionApiGatewayV2Routealso never readAuthorizerIdoff the template at all, even thoughApiGatewayV2Service.createRoute/updateRoutealready handle it. Net effect: a CloudFormation-managed HTTP API v2 JWT authorizer silently degraded toAuthorizationType: NONEin practice — a route configured withAuthorizationType: JWTand a realAuthorizerIdstill let every request through unauthenticated, because neither the authorizer nor the route's link to it were ever persisted.What changed
case "AWS::ApiGatewayV2::Authorizer"inCloudFormationResourceProvisioner, plusprovisionApiGatewayV2Authorizer, modeled on the neighboringprovisionApiGatewayV2Route/Integration/Stagemethods. Maps the CFN PascalCase properties (Name,AuthorizerType,IdentitySource,JwtConfiguration.Audience/Issuer,AuthorizerUri,AuthorizerPayloadFormatVersion,AuthorizerResultTtlInSeconds,EnableSimpleResponses) onto the existingApiGatewayV2Service.createAuthorizer/updateAuthorizercamelCase request shape.provisionApiGatewayV2Routenow readsAuthorizerIdfrom the template and passes it through, so a route'sRefto an authorizer resource actually resolves to something and gets persisted.AWS::ApiGatewayV2::Authorizer— consistent with the existing v2Route/Integration/Stagetypes, none of which have a case indelete(...)either, since that method's signature (resourceType,physicalId,region) has noapiIdto scope the lookup.Test plan
createStack_apiGatewayV2AuthorizerIsProvisionedAndWiredToRouteinCloudFormationIntegrationTest: deploys a template with anAWS::ApiGatewayV2::Api+Authorizer+Integration+Route(AuthorizerId: !Ref Authorizer), then assertsGetAuthorizersreturns the authorizer with the rightJwtConfiguration, andGetRoutesshows the route'sAuthorizerIdresolved to that authorizer's real id.CloudFormationIntegrationTestsuite (90 tests) plusRouteAuthorizerIdResponseTestandIntegrationConnectionTypeAndAuthorizerSimpleResponsesTest(the existing v2 authorizer/route regression coverage) — 100/100 pass, no regressions.floci/floci:latest(1.5.30) viadocker run+ the AWS CLI before writing the fix, confirmingget-authorizersreturned{"Items":[]}and an unauthenticated request to the route returned200instead of401.