Repository: gruntwork-io/terragrunt

Issue:
Add support for assume_role_with_web_identity to the S3 remote state block
## Describe the enhancement

Add support for assume_role_with_web_identity to the S3 remote state block. 

### Implications of the feature

Direct support for this would be favourable over generating additional files or loosing the ability to automatically deploy underlying infrastructure.  This will also maintain parity with underlying functionality of Terraform/OpenTOFU.
Several CI/CD tools (GitHub Action in our case) use OIDC to enable authentication with AWS, implementing this enhancement will allow for easier direct integrations with these tools.

### Describe alternatives you've considered

- One workaround is to not use the terragrunt remote_state block and use a generate block and generate a terraform file for the backend configuration.
- Another workaround is to generate a `~/.aws/config file.` and use the profile option.


Hints from issue comments:
@yhakbar: The backend configuration block has support for all the same configurations that the `assume_role` configuration for OpenTofu/Terraform `assume_role` configurations as of [v0.75.3](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.75.3):
https://terragrunt.gruntwork.io/docs/reference/config-blocks-and-attributes/#backend

There's also multiple ways to authenticate to AWS, including OIDC, as documented [here](https://terragrunt.gruntwork.io/docs/features/aws-authentication/).

Closing this out as a non-issue. Please feel free to keep the conversation going in GitHub Discussions and/or Discord.
@yhakbar: I misunderstood this issue. We support all `assume_role` configurations, but not all `assume_role_with_web_identity` configurations.

We can address that.@yhakbar: Released in [v0.77.2](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.77.2).

Solution patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index d607be41..15165272 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -9,7 +9,7 @@ env: &env
     GRUNTWORK_INSTALLER_VERSION: v0.0.39
     MODULE_CI_VERSION: v0.57.0
     OPENTOFU_VERSION: "1.9.0"
-    TERRAFORM_VERSION: "1.11.0"
+    TERRAFORM_VERSION: "1.11.3"
     TFLINT_VERSION: "0.47.0"
     TOFU_ENGINE_VERSION: "v0.0.16"
 
diff --git a/codegen/generate.go b/codegen/generate.go
index 947520cd..df9d7322 100644
--- a/codegen/generate.go
+++ b/codegen/generate.go
@@ -59,7 +59,8 @@ const (
 	DisabledRemoveStr           = "remove"
 	DisabledRemoveTerragruntStr = "remove_terragrunt"
 
-	assumeRoleConfigKey = "assume_role"
+	assumeRoleConfigKey                = "assume_role"
+	assumeRoleWithWebIdentityConfigKey = "assume_role_with_web_identity"
 
 	encryptionBlockName = "encryption"
 
@@ -370,6 +371,77 @@ func RemoteStateConfigToTerraformCode(backend string, config map[string]any, enc
 			continue
 		}
 
+		if key == assumeRoleWithWebIdentityConfigKey {
+			assumeRoleWithWebIdentityValue, isAssumeRoleWithWebIdentity := config[assumeRoleWithWebIdentityConfigKey].(string)
+			if !isAssumeRoleWithWebIdentity {
+				continue
+			}
+
+			// Extracting the values requires two steps.
+			// Parsing into a struct first, enabling hclsimple.Decode() to deal with complex types.
+			// Then copying values into the assumeRoleMap for rendering to HCL.
+			assumeRoleMap := make(map[string]any)
+
+			type assumeRoleWithWebIdentityConfig struct {
+				RoleArn              string   `hcl:"role_arn"`
+				Duration             string   `hcl:"duration,optional"`
+				Policy               string   `hcl:"policy,optional"`
+				SessionName          string   `hcl:"session_name,optional"`
+				WebIdentityToken     string   `hcl:"web_identity_token,optional"`
+				WebIdentityTokenFile string   `hcl:"web_identity_token_file,optional"`
+				PolicyArns           []string `hcl:"policy_arns,optional"`
+			}
+
+			var parsedConfig assumeRoleWithWebIdentityConfig
+			// split single line hcl to default multiline file
+			hclValue := strings.TrimSuffix(assumeRoleWithWebIdentityValue, "}")
+			hclValue = strings.TrimPrefix(hclValue, "{")
+			hclValue = strings.ReplaceAll(hclValue, ",", "\n")
+
+			err := hclsimple.Decode("s3_assume_role_with_web_identity.hcl", []byte(hclValue), nil, &parsedConfig)
+			if err != nil {
+				return nil, errors.New(err)
+			}
+
+			if parsedConfig.RoleArn != "" {
+				assumeRoleMap["role_arn"] = parsedConfig.RoleArn
+			}
+
+			if parsedConfig.Duration != "" {
+				assumeRoleMap["duration"] = parsedConfig.Duration
+			}
+
+			if parsedConfig.Policy != "" {
+				assumeRoleMap["policy"] = parsedConfig.Policy
+			}
+
+			if len(parsedConfig.PolicyArns) > 0 {
+				assumeRoleMap["policy_arns"] = parsedConfig.PolicyArns
+			}
+
+			if parsedConfig.SessionName != "" {
+				assumeRoleMap["session_name"] = parsedConfig.SessionName
+			}
+
+			if parsedConfig.WebIdentityToken != "" {
+				assumeRoleMap["web_identity_token"] = parsedConfig.WebIdentityToken
+			}
+
+			if parsedConfig.WebIdentityTokenFile != "" {
+				assumeRoleMap["web_identity_token_file"] = parsedConfig.WebIdentityTokenFile
+			}
+
+			// write assume role map as HCL object
+			ctyVal, err := convertValue(assumeRoleMap)
+			if err != nil {
+				return nil, errors.New(err)
+			}
+
+			backendBlockBody.SetAttributeValue(key, ctyVal.Value)
+
+			continue
+		}
+
 		ctyVal, err := convertValue(config[key])
 		if err != nil {
 			return nil, errors.New(err)
diff --git a/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx b/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx
index 18d774b2..557c69ab 100644
--- a/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx
+++ b/docs-starlight/src/content/docs/04-reference/01-hcl/02-blocks.mdx
@@ -437,6 +437,14 @@ For the `s3` backend, the following additional properties are supported in the `
   - `source_identity` - (Optional) The source identity to use when assuming the role.
   - `tags` - (Optional) A map of key value pairs used as assume role session tags.
   - `transitive_tag_keys` - (Optional) A list of tag keys that to be passed.
+- `assume_role_with_web_identity` - (Optional) A configuration `map` to use when assuming a role with a web identity token.
+  - `role_arn` - (Required) The role to be assumed.
+  - `duration` - (Optional) The duration the credentials will be valid.
+  - `policy` - (Optional) Policy JSON to further restrict the role.
+  - `policy_arns` - (Optional) A list of policy ARNs to further restrict the role.
+  - `session_name` - (Optional) The session name to use when assuming the role.
+  - `web_identity_token` - (Required) The web identity token to use when assuming the role.
+  - `web_identity_token_file` - (Optional) The path to the file containing the web identity token to use when assuming the role.
 
 For the `gcs` backend, the following additional properties are supported in the `config` attribute:
 
diff --git a/docs/_docs/04_reference/04-config-blocks-and-attributes.md b/docs/_docs/04_reference/04-config-blocks-and-attributes.md
index 08aca6f7..92b545b5 100644
--- a/docs/_docs/04_reference/04-config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/04-config-blocks-and-attributes.md
@@ -491,6 +491,14 @@ For the `s3` backend, the following additional properties are supported in the `
   - `source_identity` - (Optional) The source identity to use when assuming the role.
   - `tags` - (Optional) A map of key value pairs used as assume role session tags.
   - `transitive_tag_keys` - (Optional) A list of tag keys that to be passed.
+- `assume_role_with_web_identity` - (Optional) A configuration `map` to use when assuming a role with a web identity token.
+  - `role_arn` - (Required) The role to be assumed.
+  - `duration` - (Optional) The duration the credentials will be valid.
+  - `policy` - (Optional) Policy JSON to further restrict the role.
+  - `policy_arns` - (Optional) A list of policy ARNs to further restrict the role.
+  - `session_name` - (Optional) The session name to use when assuming the role.
+  - `web_identity_token` - (Required) The web identity token to use when assuming the role.
+  - `web_identity_token_file` - (Optional) The path to the file containing the web identity token to use when assuming the role.
 
 For the `gcs` backend, the following additional properties are supported in the `config` attribute:
 
@@ -1746,7 +1754,7 @@ rds
 The `vpc` unit is placed inside `.terragrunt-stack`, as expected.
 The `rds` unit is generated in the **same directory as `terragrunt.stack.hcl`**, rather than inside `.terragrunt-stack`, due to `no_dot_terragrunt_stack = true`.
 
-**Note:**  
+**Note:**
 The `source` value can be updated dynamically using the `--source-map` flag, just like `terraform.source`.
 
 ### stack
@@ -1797,7 +1805,7 @@ In this example, the `services` stack is defined with path `services`, which wil
 The stack is also provided with custom values for `project` and `cidr`, which can be used within the stack's configuration files.
 Terragrunt will recursively generate a stack using the contents of the `.terragrunt-stack/services/terragrunt.stack.hcl` file until the entire stack is fully generated.
 
-**Note:**  
+**Note:**
 The `source` value can be updated dynamically using the `--source-map` flag, just like `terraform.source`.
 
 ## Attributes
diff --git a/go.mod b/go.mod
index 38d3bd9e..08a4e6d8 100644
--- a/go.mod
+++ b/go.mod
@@ -14,7 +14,7 @@ require (
 	github.com/charmbracelet/bubbles v0.20.0
 	github.com/charmbracelet/bubbletea v1.3.4
 	github.com/charmbracelet/glamour v0.8.0
-	github.com/charmbracelet/lipgloss v1.0.0
+	github.com/charmbracelet/lipgloss v1.1.0
 	github.com/creack/pty v1.1.24
 	github.com/fatih/structs v1.1.0
 	github.com/getsops/sops/v3 v3.9.4
@@ -25,7 +25,7 @@ require (
 	github.com/google/uuid v1.6.0
 	github.com/gruntwork-io/boilerplate v0.6.0
 	github.com/gruntwork-io/go-commons v0.17.2
-	github.com/gruntwork-io/terragrunt-engine-go v0.0.11
+	github.com/gruntwork-io/terragrunt-engine-go v0.0.13
 	github.com/gruntwork-io/terratest v0.48.2
 	github.com/hashicorp/go-cleanhttp v0.5.2
 	github.com/hashicorp/go-getter v1.7.8
@@ -58,7 +58,7 @@ require (
 	github.com/sirupsen/logrus v1.9.3
 	github.com/stretchr/testify v1.10.0
 	github.com/terraform-linters/tflint v0.55.0
-	github.com/urfave/cli/v2 v2.27.5
+	github.com/urfave/cli/v2 v2.27.6
 	github.com/zclconf/go-cty v1.16.2
 	go.opentelemetry.io/otel v1.35.0
 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
@@ -79,7 +79,7 @@ require (
 	golang.org/x/text v0.23.0
 	google.golang.org/api v0.227.0
 	google.golang.org/grpc v1.71.0
-	google.golang.org/protobuf v1.36.5
+	google.golang.org/protobuf v1.36.6
 	gopkg.in/ini.v1 v1.67.0
 )
 
@@ -166,13 +166,15 @@ require (
 	github.com/boombuler/barcode v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/charmbracelet/colorprofile v0.3.0 // indirect
 	github.com/charmbracelet/x/ansi v0.8.0 // indirect
+	github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
 	github.com/cloudflare/circl v1.6.0 // indirect
 	github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 // indirect
 	github.com/containerd/console v1.0.4 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/dlclark/regexp2 v1.11.4 // indirect
+	github.com/dlclark/regexp2 v1.11.5 // indirect
 	github.com/docker/docker v27.5.1+incompatible // indirect
 	github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
 	github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
@@ -238,7 +240,7 @@ require (
 	github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
 	github.com/muesli/cancelreader v0.2.2 // indirect
 	github.com/muesli/reflow v0.3.0 // indirect
-	github.com/muesli/termenv v0.15.3-0.20241212154518-8c990cd6cf4b // indirect
+	github.com/muesli/termenv v0.16.0 // indirect
 	github.com/oklog/run v1.1.0 // indirect
 	github.com/owenrumney/go-sarif v1.1.1 // indirect
 	github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect
@@ -266,7 +268,7 @@ require (
 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 	github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
 	github.com/yuin/goldmark v1.7.8 // indirect
-	github.com/yuin/goldmark-emoji v1.0.4 // indirect
+	github.com/yuin/goldmark-emoji v1.0.5 // indirect
 	github.com/zclconf/go-cty-yaml v1.1.0 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
 	go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
@@ -275,7 +277,7 @@ require (
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.5.0 // indirect
 	golang.org/x/crypto v0.36.0 // indirect
-	golang.org/x/net v0.37.0 // indirect
+	golang.org/x/net v0.38.0 // indirect
 	golang.org/x/time v0.11.0 // indirect
 	golang.org/x/tools v0.29.0 // indirect
 	google.golang.org/genproto v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
diff --git a/go.sum b/go.sum
index 1cb47132..7afebea6 100644
--- a/go.sum
+++ b/go.sum
@@ -892,12 +892,16 @@ github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQW
 github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
 github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
 github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
+github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ=
+github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0=
 github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=
 github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=
-github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
-github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
 github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
 github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
 github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
 github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
 github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
@@ -950,8 +954,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
-github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
-github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
 github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/docker v27.5.1+incompatible h1:4PYU5dnBYqRQi0294d1FBECqT9ECWeQAIfE8q4YnPY8=
@@ -1229,8 +1233,8 @@ github.com/gruntwork-io/boilerplate v0.6.0 h1:77NJLgryLJXdvpCdBbHmVyPDviKBHJezmt
 github.com/gruntwork-io/boilerplate v0.6.0/go.mod h1:lEt8kd3r7TJa61CGiHcDRYANbAoSvPCeGwZQ+RqwBPc=
 github.com/gruntwork-io/go-commons v0.17.2 h1:14dsCJ7M5Vv2X3BIPKeG9Kdy6vTMGhM8L4WZazxfTuY=
 github.com/gruntwork-io/go-commons v0.17.2/go.mod h1:zs7Q2AbUKuTarBPy19CIxJVUX/rBamfW8IwuWKniWkE=
-github.com/gruntwork-io/terragrunt-engine-go v0.0.11 h1:XSeaJFLWA2HwaWc0KDaby/mU/iIRzvPAuZBzHpnkQU8=
-github.com/gruntwork-io/terragrunt-engine-go v0.0.11/go.mod h1:VjEKP2FgtUSNKKOEaY4Rw3svxQZZPaw1tKz7FUvhsWc=
+github.com/gruntwork-io/terragrunt-engine-go v0.0.13 h1:IoIBGD5La7hyyowqRhBCHF6gDtptms/PvOyPcHbWtKQ=
+github.com/gruntwork-io/terragrunt-engine-go v0.0.13/go.mod h1:kKxO2j7D3DTzzMqkQFSEhcZPYMW+g7rBaEbjMsa6/lo=
 github.com/gruntwork-io/terratest v0.48.2 h1:+VwfODchq8jxZZWD+s8gBlhD1z6/C4bFLNrhpm9ONrs=
 github.com/gruntwork-io/terratest v0.48.2/go.mod h1:Y5ETyD4ZQ2MZhasPno272fWuCpKwvTPYDi8Y0tIMqTE=
 github.com/hashicorp/aws-sdk-go-base v0.6.0/go.mod h1:2fRjWDv3jJBeN6mVWFHV6hFTNeFBx2gpDLQaZNxUVAY=
@@ -1511,8 +1515,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
 github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.15.3-0.20241212154518-8c990cd6cf4b h1:gmVbquSG+bANneniKyO7R2DkOlCat7XDaSfXxHKKQBY=
-github.com/muesli/termenv v0.15.3-0.20241212154518-8c990cd6cf4b/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
@@ -1678,8 +1682,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
 github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
 github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
-github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
-github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
+github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
+github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -1715,8 +1719,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
 github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
 github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
 github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxAEF90=
-github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
+github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
+github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
 github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
 github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
 github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
@@ -1945,8 +1949,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2510,8 +2514,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=



Test patch:
diff --git a/codegen/generate_test.go b/codegen/generate_test.go
index 5653d7e9..6c5970c8 100644
--- a/codegen/generate_test.go
+++ b/codegen/generate_test.go
@@ -81,6 +81,21 @@ func TestRemoteStateConfigToTerraformCode(t *testing.T) {
     bucket = "mybucket"
   }
 }
+`)
+	expectedS3WithAssumeRoleWithWebIdentity := []byte(`terraform {
+  backend "s3" {
+    assume_role_with_web_identity = {
+      duration                = "1h30m"
+      policy                  = "{}"
+      policy_arns             = ["arn:aws:iam::123456789012:policy/MyPolicy"]
+      role_arn                = "arn:aws:iam::123456789012:role/MyRole"
+      session_name            = "MySession"
+      web_identity_token      = "123456789012"
+      web_identity_token_file = "/path/to/web_identity_token_file"
+    }
+    bucket = "mybucket"
+  }
+}
 `)
 
 	testCases := []struct {
@@ -147,6 +162,17 @@ func TestRemoteStateConfigToTerraformCode(t *testing.T) {
 			expectedS3WithAssumeRole,
 			false,
 		},
+		{
+			"s3-backend-with-assume-role-with-web-identity",
+			"s3",
+			map[string]any{
+				"bucket":                        "mybucket",
+				"assume_role_with_web_identity": "{role_arn=\"arn:aws:iam::123456789012:role/MyRole\",duration=\"1h30m\", policy=\"{}\", policy_arns=[\"arn:aws:iam::123456789012:policy/MyPolicy\"], session_name=\"MySession\", web_identity_token=\"123456789012\", web_identity_token_file=\"/path/to/web_identity_token_file\"}",
+			},
+			map[string]any{},
+			expectedS3WithAssumeRoleWithWebIdentity,
+			false,
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/internal/remotestate/backend/gcs/config.go b/internal/remotestate/backend/gcs/config.go
index a47eebc6..b557c860 100644
--- a/internal/remotestate/backend/gcs/config.go
+++ b/internal/remotestate/backend/gcs/config.go
@@ -10,6 +10,8 @@ import (
 	"github.com/mitchellh/mapstructure"
 	"golang.org/x/exp/slices"
 
+	"maps"
+
 	"github.com/gruntwork-io/terragrunt/util"
 )
 
@@ -42,9 +44,7 @@ func (cfg Config) IsEqual(targetCfg Config, logger log.Logger) bool {
 	// Construct a new map excluding custom GCS labels that are only used in Terragrunt config and not in Terraform's backend
 	newConfig := backend.Config{}
 
-	for key, val := range cfg.FilterOutTerragruntKeys() {
-		newConfig[key] = val
-	}
+	maps.Copy(newConfig, cfg.FilterOutTerragruntKeys())
 
 	return newConfig.IsEqual(backend.Config(targetCfg), BackendName, logger)
 }
diff --git a/internal/remotestate/backend/s3/config.go b/internal/remotestate/backend/s3/config.go
index d47d3fc0..b2966c5b 100644
--- a/internal/remotestate/backend/s3/config.go
+++ b/internal/remotestate/backend/s3/config.go
@@ -11,6 +11,8 @@ import (
 	"github.com/gruntwork-io/terragrunt/pkg/log"
 	"github.com/mitchellh/mapstructure"
 
+	"maps"
+
 	"github.com/gruntwork-io/terragrunt/util"
 )
 
@@ -20,6 +22,7 @@ const (
 	configEncryptKey                   = "encrypt"
 	configKeyKey                       = "key"
 	configAssumeRoleKey                = "assume_role"
+	configAssumeRoleWithWebIdentityKey = "assume_role_with_web_identity"
 	configAccessloggingTargetPrefixKey = "accesslogging_target_prefix"
 
 	DefaultS3BucketAccessLoggingTargetPrefix = "TFStateLogs/"
@@ -63,6 +66,14 @@ func (cfg Config) GetTFInitArgs() Config {
 			}
 		}
 
+		if key == configAssumeRoleWithWebIdentityKey {
+			if mapVal, ok := val.(map[string]any); ok {
+				filtered[key] = hclhelper.WrapMapToSingleLineHcl(mapVal)
+
+				continue
+			}
+		}
+
 		filtered[key] = val
 	}
 
@@ -72,9 +83,7 @@ func (cfg Config) GetTFInitArgs() Config {
 func (cfg Config) Normalize(logger log.Logger) Config {
 	var normalized = make(Config)
 
-	for key, val := range cfg {
-		normalized[key] = val
-	}
+	maps.Copy(normalized, cfg)
 
 	// Nowadays it only makes sense to set the "dynamodb_table" attribute as it has
 	// been supported in Terraform since the release of version 0.10. The deprecated
@@ -117,9 +126,7 @@ func (cfg Config) IsEqual(targetCfg Config, logger log.Logger) bool {
 	// related configs.
 	newConfig := backend.Config{}
 
-	for key, val := range cfg.FilterOutTerragruntKeys() {
-		newConfig[key] = val
-	}
+	maps.Copy(newConfig, cfg.FilterOutTerragruntKeys())
 
 	return newConfig.IsEqual(backend.Config(targetCfg), BackendName, logger)
 }
diff --git a/test/fixtures/assume-role-web-identity/file-path/terragrunt.hcl b/test/fixtures/assume-role-web-identity/file-path/terragrunt.hcl
index ac9db833..e15f7932 100644
--- a/test/fixtures/assume-role-web-identity/file-path/terragrunt.hcl
+++ b/test/fixtures/assume-role-web-identity/file-path/terragrunt.hcl
@@ -1,4 +1,4 @@
-iam_role = "__FILL_IN_ASSUME_ROLE__"
+iam_role               = "__FILL_IN_ASSUME_ROLE__"
 iam_web_identity_token = "__FILL_IN_IDENTITY_TOKEN_FILE_PATH__"
 
 remote_state {
@@ -8,9 +8,9 @@ remote_state {
     if_exists = "overwrite_terragrunt"
   }
   config = {
-    bucket         = "__FILL_IN_BUCKET_NAME__"
-    key            = "${path_relative_to_include()}/terraform.tfstate"
-    region         = "__FILL_IN_REGION__"
-    encrypt        = true
+    bucket  = "__FILL_IN_BUCKET_NAME__"
+    key     = "${path_relative_to_include()}/terraform.tfstate"
+    region  = "__FILL_IN_REGION__"
+    encrypt = true
   }
 }
diff --git a/test/integration_aws_test.go b/test/integration_aws_test.go
index 7dc7f224..f552c853 100644
--- a/test/integration_aws_test.go
+++ b/test/integration_aws_test.go
@@ -43,7 +43,6 @@ const (
 	testFixtureS3Errors                  = "fixtures/s3-errors/"
 	testFixtureAssumeRole                = "fixtures/assume-role/external-id"
 	testFixtureAssumeRoleDuration        = "fixtures/assume-role/duration"
-	testFixtureAssumeRoleWebIdentityEnv  = "fixtures/assume-role-web-identity/env-var"
 	testFixtureAssumeRoleWebIdentityFile = "fixtures/assume-role-web-identity/file-path"
 	testFixtureReadIamRole               = "fixtures/read-config/iam_role_in_file"
 	testFixtureOutputFromRemoteState     = "fixtures/output-from-remote-state"