Repository: gruntwork-io/terragrunt

Issue:
External IDs with a comma , fails
## Describe the bug

When using the `remote_state` block in a terragrunt HCL file, trying to assume a role while specifying a `.config.external_id` string with a comma in it causes any Terragrunt action (plan/apply/etc) to fail with the following error:

```
s3_assume_role.hcl:1,38-2,1: Invalid multi-line string; Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \n escape to represent a newline character or use the "heredoc" multi-line template syntax.
```

## Steps To Reproduce

Create a root.hcl with the following block:

```hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket = "tfstate-${local.aws_account_id}-eu-central-1"

    key            = "${local.aws_account_id}/${path_relative_to_include()}/terraform.tfstate"
    region         = "eu-central-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
    role_arn       = "arn:aws:iam::${local.aws_account_id}:role/deploy"
    external_id    = "a,b"
  }
}
```

Note that the external_id field has a comma , in it.

## Expected behavior

A comma is a valid character for an AWS IAM role external ID. Therefore, we use a random password generator that includes this character in its character set. I expect that external IDs with commas in them are parsed as a literal string character, rather than treating it as a newline or new string.

## Nice to haves

```
ERROR  6 errors occurred:

* s3_assume_role.hcl:1,38-2,1: Invalid multi-line string; Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \n escape to represent a newline character or use the "heredoc" multi-line template syntax., and 1 other diagnostic(s)

* Cannot process module Module ./monitoring (excluded: false, assume applied: false, dependencies: [./aurora]) because one of its dependencies, Module ./aurora (excluded: false, assume applied: false, dependencies: [./base]), finished with an error: Cannot process module Module ./aurora (excluded: false, assume applied: false, dependencies: [./base]) because one of its dependencies, Module ./base (excluded: false, assume applied: false, dependencies: []), finished with an error: s3_assume_role.hcl:1,38-2,1: Invalid multi-line string; Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \n escape to represent a newline character or use the "heredoc" multi-line template syntax., and 1 other diagnostic(s)

* Cannot process module Module ./aurora (excluded: false, assume applied: false, dependencies: [./base]) because one of its dependencies, Module ./base (excluded: false, assume applied: false, dependencies: []), finished with an error: s3_assume_role.hcl:1,38-2,1: Invalid multi-line string; Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \n escape to represent a newline character or use the "heredoc" multi-line template syntax., and 1 other diagnostic(s)

* s3_assume_role.hcl:1,38-2,1: Invalid multi-line string; Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \n escape to represent a newline character or use the "heredoc" multi-line template syntax., and 1 other diagnostic(s)

* s3_assume_role.hcl:1,38-2,1: Invalid multi-line string; Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \n escape to represent a newline character or use the "heredoc" multi-line template syntax., and 1 other diagnostic(s)

* s3_assume_role.hcl:1,38-2,1: Invalid multi-line string; Quoted strings may not be split over multiple lines. To produce a multi-line string, either use the \n escape to represent a newline character or use the "heredoc" multi-line template syntax., and 1 other diagnostic(s)

14:52:31.704 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1
```

## Versions

- Terragrunt version: v0.76.8
- OpenTofu/Terraform version: Terraform 1.11.3
- Environment details (Ubuntu 20.04, Windows 10, etc.): Fedora CoreOS 41

## Additional context

We've had these external IDs for almost a year, but only just found this error popping up relatively recently, so I can assume that this was a regression in recent versions. At this point I don't have concrete timelines on when this started happening and which versions were in use, but if you need more help pinpointing it let me know and I can dig further.

For now, we have worked around this issue by regenerating our external IDs with the offending character removed.


Hints from issue comments:

Solution patch:
diff --git a/codegen/generate.go b/codegen/generate.go
index f3d896c5..fdd27f81 100644
--- a/codegen/generate.go
+++ b/codegen/generate.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"regexp"
 	"sort"
 	"strings"
 
@@ -316,7 +317,7 @@ func RemoteStateConfigToTerraformCode(backend string, config map[string]any, enc
 			// split single line hcl to default multiline file
 			hclValue := strings.TrimSuffix(assumeRoleValue, "}")
 			hclValue = strings.TrimPrefix(hclValue, "{")
-			hclValue = strings.ReplaceAll(hclValue, ",", "\n")
+			hclValue = ReplaceAllCommasOutsideQuotesWithNewLines(hclValue)
 
 			err := hclsimple.Decode("s3_assume_role.hcl", []byte(hclValue), nil, &parsedConfig)
 			if err != nil {
@@ -396,7 +397,7 @@ func RemoteStateConfigToTerraformCode(backend string, config map[string]any, enc
 			// split single line hcl to default multiline file
 			hclValue := strings.TrimSuffix(assumeRoleWithWebIdentityValue, "}")
 			hclValue = strings.TrimPrefix(hclValue, "{")
-			hclValue = strings.ReplaceAll(hclValue, ",", "\n")
+			hclValue = ReplaceAllCommasOutsideQuotesWithNewLines(hclValue)
 
 			err := hclsimple.Decode("s3_assume_role_with_web_identity.hcl", []byte(hclValue), nil, &parsedConfig)
 			if err != nil {
@@ -543,6 +544,41 @@ func convertValue(v any) (ctyjson.SimpleJSONValue, error) {
 	return ctyVal, nil
 }
 
+var (
+	// Regex Explanation:
+	// (          # Start group 1: Match quoted strings
+	//  "         # Match the opening quote
+	//  [^"\\]* # Match zero or more characters that are NOT a quote or backslash
+	//  (?:       # Start non-capturing group (for handling escaped quotes)
+	//    \\.     # Match a backslash followed by ANY character (escaped char)
+	//    [^"\\]* # Match zero or more non-quote/non-backslash chars
+	//  )*        # End non-capturing group, repeat zero or more times
+	//  "         # Match the closing quote
+	// )          # End group 1
+	// |          # OR
+	// (,)        # Start group 2: Match and capture a comma
+	//
+	re = regexp.MustCompile(`("[^"\\]*(?:\\.[^"\\]*)*")|(,)`)
+)
+
+// ReplaceAllCommasOutsideQuotesWithNewLines replaces all commas outside quotes with new lines.
+// This is useful for instances where a single line of HCL content might contain a comma, and we don't
+// want to split the line into multiple lines.
+func ReplaceAllCommasOutsideQuotesWithNewLines(s string) string {
+	output := re.ReplaceAllStringFunc(s, func(match string) string {
+		// Check if the match starts with a quote.
+		// If it does, it's a quoted string (group 1 matched). Return it unchanged.
+		if strings.HasPrefix(match, `"`) {
+			return match
+		}
+
+		// Otherwise, it must be the comma (group 2 matched). Replace it with a newline.
+		return "\n"
+	})
+
+	return output
+}
+
 // GenerateConfigExistsFromString converts a string representation of if_exists into the enum, returning an error if it
 // is not set to one of the known values.
 func GenerateConfigExistsFromString(val string) (GenerateConfigExists, error) {



Test patch:
diff --git a/codegen/generate_test.go b/codegen/generate_test.go
index 6c5970c8..75ef2da7 100644
--- a/codegen/generate_test.go
+++ b/codegen/generate_test.go
@@ -329,3 +329,53 @@ func TestGenerateDisabling(t *testing.T) {
 		})
 	}
 }
+
+func TestReplaceAllCommasOutsideQuotesWithNewLines(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:  "happy-path-basic-replacement",
+			input: `key=value,another=value,third=value`,
+			expected: `key=value
+another=value
+third=value`,
+		},
+		{
+			name:  "comma-inside-quotes",
+			input: `key="value,with,commas",another=value`,
+			expected: `key="value,with,commas"
+another=value`,
+		},
+		{
+			name:  "mixed-quotes-and-commas",
+			input: `key="value,with,commas",simple=value,quoted="hello,world"`,
+			expected: `key="value,with,commas"
+simple=value
+quoted="hello,world"`,
+		},
+		{
+			name:     "empty-string",
+			input:    ``,
+			expected: ``,
+		},
+		{
+			name:     "no-commas",
+			input:    `key=value`,
+			expected: `key=value`,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			actual := codegen.ReplaceAllCommasOutsideQuotesWithNewLines(tc.input)
+			assert.Equal(t, tc.expected, actual)
+		})
+	}
+}
diff --git a/test/fixtures/assume-role/external-id-with-comma/terragrunt.hcl b/test/fixtures/assume-role/external-id-with-comma/terragrunt.hcl
new file mode 100644
index 00000000..921c2b8f
--- /dev/null
+++ b/test/fixtures/assume-role/external-id-with-comma/terragrunt.hcl
@@ -0,0 +1,19 @@
+remote_state {
+  backend  = "s3"
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite_terragrunt"
+  }
+  config = {
+    bucket         = "__FILL_IN_BUCKET_NAME__"
+    dynamodb_table = "__FILL_IN_LOCK_TABLE_NAME__"
+    key            = "${path_relative_to_include()}/terraform.tfstate"
+    region         = "us-west-2"
+    encrypt        = true
+    assume_role    = {
+      role_arn     = get_aws_caller_identity_arn()
+      external_id  = "external_id_123,external_id_456"
+      session_name = "session_name_example"
+    }
+  }
+}
diff --git a/test/integration_aws_oidc_test.go b/test/integration_aws_oidc_test.go
new file mode 100644
index 00000000..f8ec6503
--- /dev/null
+++ b/test/integration_aws_oidc_test.go
@@ -0,0 +1,106 @@
+//go:build awsoidc
+
+// These tests aren't hooked up to CI right now, as
+// we'll soon be moving to a new CI system (GitHub Actions)
+// and we don't want to add complexity to the migration by handling
+// both CircleCI and GitHub Actions at the same time.
+
+package test_test
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terragrunt/test/helpers"
+	"github.com/gruntwork-io/terragrunt/util"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestAwsAssumeRoleWebIdentityFile(t *testing.T) {
+	if os.Getenv("CIRCLECI") != "true" {
+		t.Skip("Skipping test because it requires valid CircleCI OIDC credentials to work")
+	}
+
+	accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
+	secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
+
+	// These tests need to be run without the static key + secret
+	// used by most AWS tests here.
+	t.Setenv("AWS_ACCESS_KEY_ID", "")
+	os.Unsetenv("AWS_ACCESS_KEY_ID")
+	t.Setenv("AWS_SECRET_ACCESS_KEY", "")
+	os.Unsetenv("AWS_SECRET_ACCESS_KEY")
+
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWebIdentityFile)
+	cleanupTerraformFolder(t, tmpEnvPath)
+	testPath := util.JoinPath(tmpEnvPath, testFixtureAssumeRoleWebIdentityFile)
+
+	originalTerragruntConfigPath := util.JoinPath(testFixtureAssumeRoleWebIdentityFile, "terragrunt.hcl")
+	tmpTerragruntConfigFile := util.JoinPath(testPath, "terragrunt.hcl")
+	s3BucketName := "terragrunt-test-bucket-" + strings.ToLower(helpers.UniqueID())
+
+	role := os.Getenv("AWS_TEST_OIDC_ROLE_ARN")
+	require.NotEmpty(t, role)
+	token := os.Getenv("CIRCLE_OIDC_TOKEN_V2")
+	require.NotEmpty(t, token)
+
+	tokenFile := t.TempDir() + "/oidc-token"
+	require.NoError(t, os.WriteFile(tokenFile, []byte(token), 0400))
+
+	defer func() {
+		t.Setenv("AWS_ACCESS_KEY_ID", accessKeyID)
+		t.Setenv("AWS_SECRET_ACCESS_KEY", secretAccessKey)
+
+		helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName, options.WithIAMRoleARN(role), options.WithIAMWebIdentityToken(token))
+	}()
+
+	helpers.CopyAndFillMapPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, map[string]string{
+		"__FILL_IN_BUCKET_NAME__":              s3BucketName,
+		"__FILL_IN_REGION__":                   helpers.TerraformRemoteStateS3Region,
+		"__FILL_IN_ASSUME_ROLE__":              role,
+		"__FILL_IN_IDENTITY_TOKEN_FILE_PATH__": tokenFile,
+	})
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := helpers.RunTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir "+testPath, &stdout, &stderr)
+	require.NoError(t, err)
+
+	output := fmt.Sprintf("%s %s", stderr.String(), stdout.String())
+	assert.Contains(t, output, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.")
+}
+
+func TestAwsAssumeRoleWebIdentityFlag(t *testing.T) {
+	if os.Getenv("CIRCLECI") != "true" {
+		t.Skip("Skipping test because it requires valid CircleCI OIDC credentials to work")
+	}
+
+	// These tests need to be run without the static key + secret
+	// used by most AWS tests here.
+	t.Setenv("AWS_ACCESS_KEY_ID", "")
+	os.Unsetenv("AWS_ACCESS_KEY_ID")
+	t.Setenv("AWS_SECRET_ACCESS_KEY", "")
+	os.Unsetenv("AWS_SECRET_ACCESS_KEY")
+
+	tmp := t.TempDir()
+
+	emptyTerragruntConfigPath := filepath.Join(tmp, "terragrunt.hcl")
+	require.NoError(t, os.WriteFile(emptyTerragruntConfigPath, []byte(""), 0400))
+
+	emptyMainTFPath := filepath.Join(tmp, "main.tf")
+	require.NoError(t, os.WriteFile(emptyMainTFPath, []byte(""), 0400))
+
+	roleARN := os.Getenv("AWS_TEST_OIDC_ROLE_ARN")
+	require.NotEmpty(t, roleARN)
+	token := os.Getenv("CIRCLE_OIDC_TOKEN_V2")
+	require.NotEmpty(t, token)
+
+	helpers.RunTerragrunt(t, "terragrunt apply --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir "+tmp+" --terragrunt-iam-role "+roleARN+" --terragrunt-iam-web-identity-token "+token)
+}
diff --git a/test/integration_aws_test.go b/test/integration_aws_test.go
index e5bad1af..85e9ec07 100644
--- a/test/integration_aws_test.go
+++ b/test/integration_aws_test.go
@@ -37,17 +37,18 @@ import (
 )
 
 const (
-	testFixtureAwsProviderPatch          = "fixtures/aws-provider-patch"
-	testFixtureAwsAccountAlias           = "fixtures/get-aws-account-alias"
-	testFixtureAwsGetCallerIdentity      = "fixtures/get-aws-caller-identity"
-	testFixtureS3Errors                  = "fixtures/s3-errors/"
-	testFixtureAssumeRole                = "fixtures/assume-role/external-id"
-	testFixtureAssumeRoleDuration        = "fixtures/assume-role/duration"
-	testFixtureAssumeRoleWebIdentityFile = "fixtures/assume-role-web-identity/file-path"
-	testFixtureReadIamRole               = "fixtures/read-config/iam_role_in_file"
-	testFixtureOutputFromRemoteState     = "fixtures/output-from-remote-state"
-	testFixtureOutputFromDependency      = "fixtures/output-from-dependency"
-	testFixtureBootstrapS3Backend        = "fixtures/bootstrap-s3-backend"
+	testFixtureAwsProviderPatch                  = "fixtures/aws-provider-patch"
+	testFixtureAwsAccountAlias                   = "fixtures/get-aws-account-alias"
+	testFixtureAwsGetCallerIdentity              = "fixtures/get-aws-caller-identity"
+	testFixtureS3Errors                          = "fixtures/s3-errors/"
+	testFixtureAssumeRole                        = "fixtures/assume-role/external-id"
+	testFixtureAssumeRoleWithExternalIDWithComma = "fixtures/assume-role/external-id-with-comma"
+	testFixtureAssumeRoleDuration                = "fixtures/assume-role/duration"
+	testFixtureAssumeRoleWebIdentityFile         = "fixtures/assume-role-web-identity/file-path"
+	testFixtureReadIamRole                       = "fixtures/read-config/iam_role_in_file"
+	testFixtureOutputFromRemoteState             = "fixtures/output-from-remote-state"
+	testFixtureOutputFromDependency              = "fixtures/output-from-dependency"
+	testFixtureBootstrapS3Backend                = "fixtures/bootstrap-s3-backend"
 
 	qaMyAppRelPath = "qa/my-app"
 )
@@ -1008,89 +1009,6 @@ func TestAwsAssumeRoleDuration(t *testing.T) {
 	assert.Contains(t, output, "no changes are needed.")
 }
 
-func TestAwsAssumeRoleWebIdentityFile(t *testing.T) {
-	if os.Getenv("CIRCLECI") != "true" {
-		t.Skip("Skipping test because it requires valid CircleCI OIDC credentials to work")
-	}
-
-	accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
-	secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
-
-	// These tests need to be run without the static key + secret
-	// used by most AWS tests here.
-	t.Setenv("AWS_ACCESS_KEY_ID", "")
-	os.Unsetenv("AWS_ACCESS_KEY_ID")
-	t.Setenv("AWS_SECRET_ACCESS_KEY", "")
-	os.Unsetenv("AWS_SECRET_ACCESS_KEY")
-
-	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWebIdentityFile)
-	cleanupTerraformFolder(t, tmpEnvPath)
-	testPath := util.JoinPath(tmpEnvPath, testFixtureAssumeRoleWebIdentityFile)
-
-	originalTerragruntConfigPath := util.JoinPath(testFixtureAssumeRoleWebIdentityFile, "terragrunt.hcl")
-	tmpTerragruntConfigFile := util.JoinPath(testPath, "terragrunt.hcl")
-	s3BucketName := "terragrunt-test-bucket-" + strings.ToLower(helpers.UniqueID())
-
-	role := os.Getenv("AWS_TEST_OIDC_ROLE_ARN")
-	require.NotEmpty(t, role)
-	token := os.Getenv("CIRCLE_OIDC_TOKEN_V2")
-	require.NotEmpty(t, token)
-
-	tokenFile := t.TempDir() + "/oidc-token"
-	require.NoError(t, os.WriteFile(tokenFile, []byte(token), 0400))
-
-	defer func() {
-		t.Setenv("AWS_ACCESS_KEY_ID", accessKeyID)
-		t.Setenv("AWS_SECRET_ACCESS_KEY", secretAccessKey)
-
-		helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName, options.WithIAMRoleARN(role), options.WithIAMWebIdentityToken(token))
-	}()
-
-	helpers.CopyAndFillMapPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, map[string]string{
-		"__FILL_IN_BUCKET_NAME__":              s3BucketName,
-		"__FILL_IN_REGION__":                   helpers.TerraformRemoteStateS3Region,
-		"__FILL_IN_ASSUME_ROLE__":              role,
-		"__FILL_IN_IDENTITY_TOKEN_FILE_PATH__": tokenFile,
-	})
-
-	stdout := bytes.Buffer{}
-	stderr := bytes.Buffer{}
-
-	err := helpers.RunTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir "+testPath, &stdout, &stderr)
-	require.NoError(t, err)
-
-	output := fmt.Sprintf("%s %s", stderr.String(), stdout.String())
-	assert.Contains(t, output, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.")
-}
-
-func TestAwsAssumeRoleWebIdentityFlag(t *testing.T) {
-	if os.Getenv("CIRCLECI") != "true" {
-		t.Skip("Skipping test because it requires valid CircleCI OIDC credentials to work")
-	}
-
-	// These tests need to be run without the static key + secret
-	// used by most AWS tests here.
-	t.Setenv("AWS_ACCESS_KEY_ID", "")
-	os.Unsetenv("AWS_ACCESS_KEY_ID")
-	t.Setenv("AWS_SECRET_ACCESS_KEY", "")
-	os.Unsetenv("AWS_SECRET_ACCESS_KEY")
-
-	tmp := t.TempDir()
-
-	emptyTerragruntConfigPath := filepath.Join(tmp, "terragrunt.hcl")
-	require.NoError(t, os.WriteFile(emptyTerragruntConfigPath, []byte(""), 0400))
-
-	emptyMainTFPath := filepath.Join(tmp, "main.tf")
-	require.NoError(t, os.WriteFile(emptyMainTFPath, []byte(""), 0400))
-
-	roleARN := os.Getenv("AWS_TEST_OIDC_ROLE_ARN")
-	require.NotEmpty(t, roleARN)
-	token := os.Getenv("CIRCLE_OIDC_TOKEN_V2")
-	require.NotEmpty(t, token)
-
-	helpers.RunTerragrunt(t, "terragrunt apply --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir "+tmp+" --terragrunt-iam-role "+roleARN+" --terragrunt-iam-web-identity-token "+token)
-}
-
 // Regression testing for https://github.com/gruntwork-io/terragrunt/issues/906
 func TestAwsDependencyOutputSameOutputConcurrencyRegression(t *testing.T) {
 	t.Parallel()
@@ -1278,6 +1196,42 @@ func TestAwsAssumeRole(t *testing.T) {
 	assert.Contains(t, content, "session_name = \"session_name_example\"")
 }
 
+func TestAwsAssumeRoleWithExternalIDWithComma(t *testing.T) {
+	t.Parallel()
+
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWithExternalIDWithComma)
+	helpers.CleanupTerraformFolder(t, tmpEnvPath)
+	testPath := util.JoinPath(tmpEnvPath, testFixtureAssumeRoleWithExternalIDWithComma)
+
+	originalTerragruntConfigPath := util.JoinPath(testFixtureAssumeRoleWithExternalIDWithComma, "terragrunt.hcl")
+	tmpTerragruntConfigFile := util.JoinPath(testPath, "terragrunt.hcl")
+	s3BucketName := "terragrunt-test-bucket-" + strings.ToLower(helpers.UniqueID())
+	lockTableName := "terragrunt-test-locks-" + strings.ToLower(helpers.UniqueID())
+	helpers.CopyTerragruntConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, s3BucketName, lockTableName, "us-east-2")
+
+	helpers.RunTerragrunt(t, "terragrunt validate-inputs -auto-approve --non-interactive --working-dir "+testPath)
+
+	// validate generated backend.tf
+	backendFile := filepath.Join(testPath, "backend.tf")
+	assert.FileExists(t, backendFile)
+
+	content, err := files.ReadFileAsString(backendFile)
+	require.NoError(t, err)
+
+	opts, err := options.NewTerragruntOptionsForTest(testPath)
+	require.NoError(t, err)
+
+	session, err := awshelper.CreateAwsSession(nil, opts)
+	require.NoError(t, err)
+
+	identityARN, err := awshelper.GetAWSIdentityArn(session)
+	require.NoError(t, err)
+
+	assert.Contains(t, content, "role_arn     = \""+identityARN+"\"")
+	assert.Contains(t, content, "external_id  = \"external_id_123,external_id_456\"")
+	assert.Contains(t, content, "session_name = \"session_name_example\"")
+}
+
 func TestAwsInitConfirmation(t *testing.T) {
 	t.Parallel()