diff --git a/.gitlab/README.md.template b/.gitlab/README.md.template
index 1793fc300ff920a4c9dbd03ab93595d710423def..5c9e3a156270960dac68b09d5fe238d7c3b644e5 100644
--- a/.gitlab/README.md.template
+++ b/.gitlab/README.md.template
@@ -199,6 +199,60 @@ include:
 stages: [validate, build, deploy]
 ```
 
+### Configure `id_tokens`
+
+> [!note]
+> Due to [lacking support](https://gitlab.com/gitlab-org/gitlab/-/issues/452451)
+> of map nodes as input value types the configuration for `id_tokens` is somewhat special,
+> but nonetheless super easy. Read along!
+
+To configure [`id_tokens`](https://docs.gitlab.com/ci/yaml/#id_tokens) support you need these
+three things:
+
+1. set the `enable_id_tokens` input to `true`.
+2. configure the `.gitlab-tofu:id_tokens` job with your desired `id_tokens` setup.
+3. (optionally) provide the `.gitlab/ci/setup-id-tokens.sh` script to configure things,
+   like assuming IAM roles.
+
+An example setup may look like this:
+
+```yaml
+include:
+  - component: $CI_SERVER_FQDN/components/opentofu/validate-plan-apply@<VERSION>
+    inputs:
+      # The version must currently be specified explicitly as an input,
+      # to find the correctly associated images. # This can be removed
+      # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
+      version: <VERSION> # component version
+      opentofu_version: <OPENTOFU_VERSION>
+      enable_id_tokens: true
+
+stages: [validate, build, deploy]
+
+.gitlab-tofu:id_tokens:
+  id_tokens:
+    GITLAB_OIDC_TOKEN:
+      aud: https://gitlab.com
+```
+
+Then in the `.gitlab/ci/setup-id-tokens.sh` script you might assume a AWS IAM role:
+
+```shell
+apk add --no-cache aws-cli
+export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" \
+    $(aws sts assume-role-with-web-identity \
+        --role-arn ${GITLAB_CI_ROLE_ARN} \
+        --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}" \
+        --web-identity-token ${GITLAB_OIDC_TOKEN} \
+        --duration-seconds 3600 \
+        --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
+        --output text))
+aws sts get-caller-identity
+```
+
+You might configure the name of the `id_tokens` job and the setup script location
+with the `id_tokens_base_job_name` and `id_tokens_setup_script` inputs, respectively.
+
 ### Access to Terraform Module Registry
 
 Similar to automatically configuring the [GitLab-managed Terraform state backend]
diff --git a/.gitlab/scripts/update-opentofu-versions.sh b/.gitlab/scripts/update-opentofu-versions.sh
index bfd54706701a5ddda38957fb1bdb8007e6e7bd6a..417a7a822b21a4b148cee16b300bedee816d6ec9 100755
--- a/.gitlab/scripts/update-opentofu-versions.sh
+++ b/.gitlab/scripts/update-opentofu-versions.sh
@@ -8,7 +8,7 @@ project_dir="$script_dir/../.."
 echo "Updating template files ..."
 
 templates="templates/*.yml"
-templates_exclude="templates/delete-state.yml templates/module-release.yml"
+templates_exclude="templates/delete-state.yml templates/module-release.yml templates/__internal_id_tokens_base_job.yml"
 
 for relative_template_file in $templates; do
   if echo "$templates_exclude" | grep -q "$relative_template_file"; then continue; fi
diff --git a/README.md b/README.md
index 6c89664dce8d911c6b8acc805485565dd4c7fc8e..4f490c5065775ebde64263861b3436519a502f43 100644
--- a/README.md
+++ b/README.md
@@ -201,6 +201,60 @@ include:
 stages: [validate, build, deploy]
 ```
 
+### Configure `id_tokens`
+
+> [!note]
+> Due to [lacking support](https://gitlab.com/gitlab-org/gitlab/-/issues/452451)
+> of map nodes as input value types the configuration for `id_tokens` is somewhat special,
+> but nonetheless super easy. Read along!
+
+To configure [`id_tokens`](https://docs.gitlab.com/ci/yaml/#id_tokens) support you need these
+three things:
+
+1. set the `enable_id_tokens` input to `true`.
+2. configure the `.gitlab-tofu:id_tokens` job with your desired `id_tokens` setup.
+3. (optionally) provide the `.gitlab/ci/setup-id-tokens.sh` script to configure things,
+   like assuming IAM roles.
+
+An example setup may look like this:
+
+```yaml
+include:
+  - component: $CI_SERVER_FQDN/components/opentofu/validate-plan-apply@<VERSION>
+    inputs:
+      # The version must currently be specified explicitly as an input,
+      # to find the correctly associated images. # This can be removed
+      # once https://gitlab.com/gitlab-org/gitlab/-/issues/438275 is solved.
+      version: <VERSION> # component version
+      opentofu_version: <OPENTOFU_VERSION>
+      enable_id_tokens: true
+
+stages: [validate, build, deploy]
+
+.gitlab-tofu:id_tokens:
+  id_tokens:
+    GITLAB_OIDC_TOKEN:
+      aud: https://gitlab.com
+```
+
+Then in the `.gitlab/ci/setup-id-tokens.sh` script you might assume a AWS IAM role:
+
+```shell
+apk add --no-cache aws-cli
+export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" \
+    $(aws sts assume-role-with-web-identity \
+        --role-arn ${GITLAB_CI_ROLE_ARN} \
+        --role-session-name "GitLabRunner-${CI_PROJECT_ID}-${CI_PIPELINE_ID}" \
+        --web-identity-token ${GITLAB_OIDC_TOKEN} \
+        --duration-seconds 3600 \
+        --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
+        --output text))
+aws sts get-caller-identity
+```
+
+You might configure the name of the `id_tokens` job and the setup script location
+with the `id_tokens_base_job_name` and `id_tokens_setup_script` inputs, respectively.
+
 ### Access to Terraform Module Registry
 
 Similar to automatically configuring the [GitLab-managed Terraform state backend]
diff --git a/templates/__internal_id_tokens_base_job.yml b/templates/__internal_id_tokens_base_job.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8775b65b7ce82228e2a16b3f77d1e541d4f5cbed
--- /dev/null
+++ b/templates/__internal_id_tokens_base_job.yml
@@ -0,0 +1,21 @@
+spec:
+  inputs:
+    # NOTE: see the using templates for their description
+    as: {type: string}
+    id_tokens_base_job_name: {type: string}
+    id_tokens_setup_script: {type: string}
+
+---
+
+
+# NOTE: okay, this is bad, but it's what we got.
+# We have to outsource this job (see the using templates for why we have it in the first place)
+# because GitLab ALWAYS evaluates jobs, even if they are never used.
+# This implies that all base jobs have to exist.
+# And the only way for GitLab NOT to parse a job is to NOT include it.
+# Thus, we simply do not include this if we don't have to.
+'.$[[ inputs.as ]]:id_tokens-setup:true':
+  extends: $[[ inputs.id_tokens_base_job_name ]]
+  before_script:
+    - test -f "$[[ inputs.id_tokens_setup_script ]]" && . $[[ inputs.id_tokens_setup_script ]]
+
diff --git a/templates/apply.yml b/templates/apply.yml
index be1c76c3c087615a4f109cfd8e4fb92ea35d50a9..2539887f50a2a34790eadd37bf868c2919e4b0ca 100644
--- a/templates/apply.yml
+++ b/templates/apply.yml
@@ -99,10 +99,49 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the job.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend this job from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
+include:
+  # NOTE: see the note in that template file for what he heck is going on here.
+  - local: '/templates/__internal_id_tokens_base_job.yml'
+    rules:
+      - if: '"$[[ inputs.enable_id_tokens ]]" == "true"'
+    inputs:
+      as: $[[ inputs.as ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
+
+# NOTE: due to the lacking support of providing map nodes as input values
+# we need to hack support for id_tokens together. It's even worse that id_tokens
+# do NOT have a consistent type - or rather it's child nodes - for example, the `aud`
+# can be a literal string value, a variable or an array. Limitations, bear with me.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/452451
+'.$[[ inputs.as ]]:id_tokens-setup:false':
+  extends: null
+
 '$[[ inputs.as ]]':
+  extends:
+    # NOTE: see the comment above. This is to support the `id_tokens` setup.
+    - '.$[[ inputs.as ]]:id_tokens-setup:$[[ inputs.enable_id_tokens ]]'
   stage: $[[ inputs.stage ]]
   environment:
     name: $GITLAB_TOFU_STATE_NAME
diff --git a/templates/destroy.yml b/templates/destroy.yml
index 45e0997b9e744634972d09500bb049cefbc8a6d9..f70df31914648f6d7666fbe043c408ababf7ac87 100644
--- a/templates/destroy.yml
+++ b/templates/destroy.yml
@@ -103,10 +103,49 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the job.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend this job from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
+include:
+  # NOTE: see the note in that template file for what he heck is going on here.
+  - local: '/templates/__internal_id_tokens_base_job.yml'
+    rules:
+      - if: '"$[[ inputs.enable_id_tokens ]]" == "true"'
+    inputs:
+      as: $[[ inputs.as ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
+
+# NOTE: due to the lacking support of providing map nodes as input values
+# we need to hack support for id_tokens together. It's even worse that id_tokens
+# do NOT have a consistent type - or rather it's child nodes - for example, the `aud`
+# can be a literal string value, a variable or an array. Limitations, bear with me.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/452451
+'.$[[ inputs.as ]]:id_tokens-setup:false':
+  extends: null
+
 '$[[ inputs.as ]]':
+  extends:
+    # NOTE: see the comment above. This is to support the `id_tokens` setup.
+    - '.$[[ inputs.as ]]:id_tokens-setup:$[[ inputs.enable_id_tokens ]]'
   stage: $[[ inputs.stage ]]
   environment:
     name: $GITLAB_TOFU_STATE_NAME
diff --git a/templates/full-pipeline.yml b/templates/full-pipeline.yml
index 290e81bb2925a828d76275a7c2aa2b78ef390dfb..98403b142cd9a413082558b551db2d11bcaf83b8 100644
--- a/templates/full-pipeline.yml
+++ b/templates/full-pipeline.yml
@@ -202,6 +202,24 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the pipeline.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend the jobs from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
@@ -241,6 +259,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/test.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "true"'
@@ -265,6 +286,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -289,6 +313,9 @@ include:
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/apply.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -310,6 +337,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/destroy.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -330,6 +360,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/delete-state.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -418,6 +451,9 @@ stages:
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
           auto_define_backend: $[[ inputs.auto_define_backend ]]
+          enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+          id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+          id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/graph.yml b/templates/graph.yml
index c78cde0ae86b353bdd6d2a1719f1dd817cc31e6c..ed881e57b45f5d13a2347b1deef9accba82bec64 100644
--- a/templates/graph.yml
+++ b/templates/graph.yml
@@ -80,6 +80,13 @@ spec:
       default: []
       type: array
       description: 'Defines the `needs` of the job.'
+    rules:
+      # FIXME: eventually, we'll want to define `null` as the default,
+      # but this is NOT support yet, see
+      # https://gitlab.com/gitlab-org/gitlab/-/issues/440468
+      default: [{when: on_success}]
+      type: array
+      description: 'Defines the `rules` of the job.'
     cache_policy:
       default: pull-push
       type: string
@@ -100,12 +107,52 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the job.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend this job from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
+include:
+  # NOTE: see the note in that template file for what he heck is going on here.
+  - local: '/templates/__internal_id_tokens_base_job.yml'
+    rules:
+      - if: '"$[[ inputs.enable_id_tokens ]]" == "true"'
+    inputs:
+      as: $[[ inputs.as ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
+
+# NOTE: due to the lacking support of providing map nodes as input values
+# we need to hack support for id_tokens together. It's even worse that id_tokens
+# do NOT have a consistent type - or rather it's child nodes - for example, the `aud`
+# can be a literal string value, a variable or an array. Limitations, bear with me.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/452451
+'.$[[ inputs.as ]]:id_tokens-setup:false':
+  extends: null
+
 '$[[ inputs.as ]]':
+  extends:
+    # NOTE: see the comment above. This is to support the `id_tokens` setup.
+    - '.$[[ inputs.as ]]:id_tokens-setup:$[[ inputs.enable_id_tokens ]]'
   stage: $[[ inputs.stage ]]
   needs: $[[ inputs.needs ]]
+  rules: $[[ inputs.rules ]]
   cache:
     key: "$__CACHE_KEY_HACK"
     policy: $[[ inputs.cache_policy ]]
diff --git a/templates/job-templates.yml b/templates/job-templates.yml
index eaa2d306161fccc811d3318729eef823ccab3066..6f69de66adb5eeb5e285aea6b8788cedb75c6eab 100644
--- a/templates/job-templates.yml
+++ b/templates/job-templates.yml
@@ -107,6 +107,24 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the pipeline.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend the jobs from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
@@ -139,6 +157,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/graph.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]graph'
@@ -154,6 +175,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/test.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]test'
@@ -171,6 +195,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/plan.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]plan'
@@ -191,6 +218,9 @@ include:
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/apply.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]apply'
@@ -209,6 +239,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/destroy.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]destroy'
@@ -227,6 +260,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/delete-state.yml'
     inputs:
       as: '$[[ inputs.job_name_prefix ]]delete-state'
diff --git a/templates/plan.yml b/templates/plan.yml
index 73b4a5118416234e0b2bfe08d384d34baeda9690..52b21866060213206455fabaaf5866f0bfde9f10 100644
--- a/templates/plan.yml
+++ b/templates/plan.yml
@@ -113,9 +113,37 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the job.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend this job from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
+include:
+  # NOTE: see the note in that template file for what he heck is going on here.
+  - local: '/templates/__internal_id_tokens_base_job.yml'
+    rules:
+      - if: '"$[[ inputs.enable_id_tokens ]]" == "true"'
+    inputs:
+      as: $[[ inputs.as ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
+
 # NOTE: the two following jobs are necessary to implement the abstraction logic
 # required for the `warning_on_non_empty_plan` input.
 # Without any kind of flow control support for the GitLab CI YAML we cannot infer
@@ -142,11 +170,21 @@ spec:
     # but we still want to upload all the artifacts.
     when: always
 
+# NOTE: due to the lacking support of providing map nodes as input values
+# we need to hack support for id_tokens together. It's even worse that id_tokens
+# do NOT have a consistent type - or rather it's child nodes - for example, the `aud`
+# can be a literal string value, a variable or an array. Limitations, bear with me.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/452451
+'.$[[ inputs.as ]]:id_tokens-setup:false':
+  extends: null
+
 '$[[ inputs.as ]]':
-  stage: $[[ inputs.stage ]]
   extends:
     # NOTE: see the comment above. This is to support the `warning_on_non_empty_plan` input.
     - '.$[[ inputs.as ]]:detailed_exitcode:warning:$[[ inputs.warning_on_non_empty_plan ]]'
+    # NOTE: see the comment above. This is to support the `id_tokens` setup.
+    - '.$[[ inputs.as ]]:id_tokens-setup:$[[ inputs.enable_id_tokens ]]'
+  stage: $[[ inputs.stage ]]
   environment:
     name: $[[ inputs.state_name ]]
     action: prepare
diff --git a/templates/test.yml b/templates/test.yml
index a76e3293c24b73d6d05dc913cc4e1951152aa795..124ff3737ed84c24a6baf88081d20eced6ec946a 100644
--- a/templates/test.yml
+++ b/templates/test.yml
@@ -102,13 +102,52 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the job.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend this job from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
+include:
+  # NOTE: see the note in that template file for what he heck is going on here.
+  - local: '/templates/__internal_id_tokens_base_job.yml'
+    rules:
+      - if: '"$[[ inputs.enable_id_tokens ]]" == "true"'
+    inputs:
+      as: $[[ inputs.as ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
+
+# NOTE: due to the lacking support of providing map nodes as input values
+# we need to hack support for id_tokens together. It's even worse that id_tokens
+# do NOT have a consistent type - or rather it's child nodes - for example, the `aud`
+# can be a literal string value, a variable or an array. Limitations, bear with me.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/452451
+'.$[[ inputs.as ]]:id_tokens-setup:false':
+  extends: null
+
 '$[[ inputs.as ]]':
   stage: $[[ inputs.stage ]]
   needs: $[[ inputs.needs ]]
   rules: $[[ inputs.rules ]]
+  extends:
+    # NOTE: see the comment above. This is to support the `id_tokens` setup.
+    - '.$[[ inputs.as ]]:id_tokens-setup:$[[ inputs.enable_id_tokens ]]'
   cache:
     key: "$__CACHE_KEY_HACK"
     policy: $[[ inputs.cache_policy ]]
diff --git a/templates/validate-plan-apply.yml b/templates/validate-plan-apply.yml
index f1c50051c4fbb540644bcf1cbab08ec7602a38f6..f72f9c9af12413c91d345247b27bbeaef7a9e600 100644
--- a/templates/validate-plan-apply.yml
+++ b/templates/validate-plan-apply.yml
@@ -166,6 +166,24 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the pipeline.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend the jobs from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
@@ -206,6 +224,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -231,6 +252,9 @@ include:
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/apply.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -253,6 +277,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
 
 
 # NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
@@ -311,6 +338,9 @@ stages:
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
           auto_define_backend: $[[ inputs.auto_define_backend ]]
+          enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+          id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+          id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/validate-plan-destroy.yml b/templates/validate-plan-destroy.yml
index 1d303fd6eaef9113cd84dadbc85c78b7b7614f63..b21f69b94ca7d9526a46f59beac41ae59fca8ca2 100644
--- a/templates/validate-plan-destroy.yml
+++ b/templates/validate-plan-destroy.yml
@@ -172,6 +172,24 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the pipeline.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend the jobs from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
@@ -212,6 +230,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -238,6 +259,9 @@ include:
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/destroy.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -261,6 +285,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/delete-state.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -344,6 +371,9 @@ stages:
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
           auto_define_backend: $[[ inputs.auto_define_backend ]]
+          enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+          id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+          id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/validate-plan.yml b/templates/validate-plan.yml
index ed976b6f333474f60bed15a95a26b4bcec0b4559..136e9d7107fce3b44473714264735c6af69e6432 100644
--- a/templates/validate-plan.yml
+++ b/templates/validate-plan.yml
@@ -150,6 +150,24 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the pipeline.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend the jobs from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 
 ---
 
@@ -190,6 +208,9 @@ include:
       auto_encryption_passphrase: $[[ inputs.auto_encryption_passphrase ]]
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
   - local: '/templates/plan.yml'
     rules:
       - if: '"$[[ inputs.trigger_in_child_pipeline ]]" == "false"'
@@ -215,6 +236,9 @@ include:
       auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
       allow_developer_role: $[[ inputs.allow_developer_role_to_plan ]]
       auto_define_backend: $[[ inputs.auto_define_backend ]]
+      enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
 
 
 # NOTE: the following configuration is only used if `trigger_in_child_pipeline` is enabled.
@@ -270,6 +294,9 @@ stages:
           auto_encryption_enable_migration_from_unencrypted: $[[ inputs.auto_encryption_enable_migration_from_unencrypted ]]
           allow_developer_role_to_plan: $[[ inputs.allow_developer_role_to_plan ]]
           auto_define_backend: $[[ inputs.auto_define_backend ]]
+          enable_id_tokens: $[[ inputs.enable_id_tokens ]]
+          id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+          id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
           trigger_in_child_pipeline: false
     forward:
       yaml_variables: true
diff --git a/templates/validate.yml b/templates/validate.yml
index 14154a195b7b0b668e41a8ad6861c94f22c8d6e1..ef0a042005e26a961b2dda085ca99ac7c46019fa 100644
--- a/templates/validate.yml
+++ b/templates/validate.yml
@@ -99,10 +99,50 @@ spec:
       default: false
       type: boolean
       description: 'Whether to automatically define the HTTP backend configuration block.'
-
+    enable_id_tokens:
+      default: false
+      type: boolean
+      description: |
+       Whether to enable `id_tokens` support for the job.
+       This works by extending a hidden base job configuration with the `id_tokens` field.
+       The job should only contain the `id_tokens` field, the rest may be overridden.
+       The job name can be configured with `id_tokens_base_job_name` if necessary.
+       If the script given in `id_tokens_setup_script` exists, it will be sourced so that setup actions can be performed,
+       like assuming an IAM role of your cloud provider.
+    id_tokens_base_job_name:
+      default: '.gitlab-tofu:id_tokens'
+      type: string
+      description: 'Name of the hidden base job containing the `id_tokens` configuration to extend this job from. Make sure to only configure `id_tokens`, everything else might be overridden.'
+    id_tokens_setup_script:
+      default: '.gitlab/ci/setup-id-tokens.sh'
+      type: string
+      description: 'Path to a shell script that is sourced when `enable_id_tokens` is `true`.'
 ---
 
+include:
+  # NOTE: see the note in that template file for what he heck is going on here.
+  - local: '/templates/__internal_id_tokens_base_job.yml'
+    rules:
+      - if: '"$[[ inputs.enable_id_tokens ]]" == "true"'
+    inputs:
+      as: $[[ inputs.as ]]
+      id_tokens_base_job_name: $[[ inputs.id_tokens_base_job_name ]]
+      id_tokens_setup_script: $[[ inputs.id_tokens_setup_script ]]
+
+# NOTE: due to the lacking support of providing map nodes as input values
+# we need to hack support for id_tokens together. It's even worse that id_tokens
+# do NOT have a consistent type - or rather it's child nodes - for example, the `aud`
+# can be a literal string value, a variable or an array. Limitations, bear with me.
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/452451
+# Oh and yes, it gets worse, the `true` counterpart for this job is in __internal_id_tokens_base_job.yml
+# See the note there for why ...
+'.$[[ inputs.as ]]:id_tokens-setup:false':
+  extends: null
+
 '$[[ inputs.as ]]':
+  extends:
+    # NOTE: see the comment above. This is to support the `id_tokens` setup.
+    - '.$[[ inputs.as ]]:id_tokens-setup:$[[ inputs.enable_id_tokens ]]'
   stage: $[[ inputs.stage ]]
   rules: $[[ inputs.rules ]]
   cache:
diff --git a/tests/iac-id-tokens/backend.tf b/tests/iac-id-tokens/backend.tf
new file mode 100644
index 0000000000000000000000000000000000000000..1736bf13b6ce62319d00b07243000e4ffea76c3d
--- /dev/null
+++ b/tests/iac-id-tokens/backend.tf
@@ -0,0 +1,3 @@
+terraform {
+  backend "http" {}
+}
diff --git a/tests/iac-id-tokens/id-tokens-setup.sh b/tests/iac-id-tokens/id-tokens-setup.sh
new file mode 100644
index 0000000000000000000000000000000000000000..77f39597fbbed45de543ec582092561f52f4d93a
--- /dev/null
+++ b/tests/iac-id-tokens/id-tokens-setup.sh
@@ -0,0 +1 @@
+touch id-tokens-setup-ran
diff --git a/tests/iac-id-tokens/main.tf b/tests/iac-id-tokens/main.tf
new file mode 100644
index 0000000000000000000000000000000000000000..aa8705c6f46153325b06b6113fdab94804fa7df4
--- /dev/null
+++ b/tests/iac-id-tokens/main.tf
@@ -0,0 +1,41 @@
+module "random_pet" {
+  source = "./modules/random-pet"
+}
+
+resource "local_file" "foo" {
+  content  = "foo!"
+  filename = "${path.module}/foo.bar"
+}
+
+locals {
+  ts = plantimestamp()
+}
+
+// NOTE: always force a change.
+resource "null_resource" "this" {
+  triggers = {
+    timestamp = local.ts
+  }
+}
+
+variable "ci_project_name" {
+  type    = string
+  default = "default"
+}
+
+variable "test_variable" {
+  type    = string
+  default = "default value"
+}
+
+output "project_name" {
+  value = var.ci_project_name
+}
+
+output "test_variable" {
+  value = var.test_variable
+}
+
+output "this_always_changes" {
+  value = local.ts
+}
diff --git a/tests/iac-id-tokens/modules/random-pet/main.tf b/tests/iac-id-tokens/modules/random-pet/main.tf
new file mode 100644
index 0000000000000000000000000000000000000000..382c4e673853109279a8d53323b2aad80a8848e2
--- /dev/null
+++ b/tests/iac-id-tokens/modules/random-pet/main.tf
@@ -0,0 +1,12 @@
+terraform {
+  required_providers {
+    random = {
+      source  = "hashicorp/random"
+      version = "3.1.2"
+    }
+  }
+}
+
+resource "random_pet" "random_pet" {
+  length = var.length
+}
diff --git a/tests/iac-id-tokens/modules/random-pet/outputs.tf b/tests/iac-id-tokens/modules/random-pet/outputs.tf
new file mode 100644
index 0000000000000000000000000000000000000000..6a771003bd661f665b2e89b1964e8d48dd6cd764
--- /dev/null
+++ b/tests/iac-id-tokens/modules/random-pet/outputs.tf
@@ -0,0 +1,3 @@
+output "random_pet" {
+  value = random_pet.random_pet.id
+}
diff --git a/tests/iac-id-tokens/modules/random-pet/variables.tf b/tests/iac-id-tokens/modules/random-pet/variables.tf
new file mode 100644
index 0000000000000000000000000000000000000000..3e65d0a1e2d133e7616dee62a920cfadf46a1914
--- /dev/null
+++ b/tests/iac-id-tokens/modules/random-pet/variables.tf
@@ -0,0 +1,4 @@
+variable "length" {
+  default = 1
+  type    = number
+}
\ No newline at end of file
diff --git a/tests/iac-id-tokens/tests/main.tftest.hcl b/tests/iac-id-tokens/tests/main.tftest.hcl
new file mode 100644
index 0000000000000000000000000000000000000000..fedf96e4c458eee084740b05980a52ffe64362a1
--- /dev/null
+++ b/tests/iac-id-tokens/tests/main.tftest.hcl
@@ -0,0 +1,6 @@
+run "test" {
+  assert {
+    condition     = file(local_file.foo.filename) == "foo!"
+    error_message = "Incorrect content in ${local_file.foo.filename}"
+  }
+}
diff --git a/tests/iac-id-tokens/varfile.integration-test.tfvars b/tests/iac-id-tokens/varfile.integration-test.tfvars
new file mode 100644
index 0000000000000000000000000000000000000000..a9e6ed522698bc6187641fb135c39346cd4223e2
--- /dev/null
+++ b/tests/iac-id-tokens/varfile.integration-test.tfvars
@@ -0,0 +1 @@
+test_variable = "varfile integration test"
diff --git a/tests/integration-tests/IdTokens.gitlab-ci.yml b/tests/integration-tests/IdTokens.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7d4078d45a74932fc66319c63e0385a0872d8514
--- /dev/null
+++ b/tests/integration-tests/IdTokens.gitlab-ci.yml
@@ -0,0 +1,112 @@
+include:
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/validate@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_GITLAB_TOFU_ROOT_DIR
+      stage: validate
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      rules: [{when: always}]
+      enable_id_tokens: true
+      id_tokens_setup_script: $TEST_GITLAB_TOFU_ROOT_DIR/id-tokens-setup.sh
+
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/test@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_GITLAB_TOFU_ROOT_DIR
+      stage: test
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      rules: [{when: always}]
+      enable_id_tokens: true
+      id_tokens_setup_script: $TEST_GITLAB_TOFU_ROOT_DIR/id-tokens-setup.sh
+
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/graph@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_GITLAB_TOFU_ROOT_DIR
+      stage: graph
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      rules: [{when: always}]
+      enable_id_tokens: true
+      id_tokens_setup_script: $TEST_GITLAB_TOFU_ROOT_DIR/id-tokens-setup.sh
+
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/plan@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_GITLAB_TOFU_ROOT_DIR
+      stage: plan
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      rules: [{when: always}]
+      enable_id_tokens: true
+      id_tokens_setup_script: $TEST_GITLAB_TOFU_ROOT_DIR/id-tokens-setup.sh
+
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/apply@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_GITLAB_TOFU_ROOT_DIR
+      stage: apply
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      rules: [{when: always}]
+      enable_id_tokens: true
+      id_tokens_setup_script: $TEST_GITLAB_TOFU_ROOT_DIR/id-tokens-setup.sh
+
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/destroy@$CI_COMMIT_SHA
+    inputs:
+      image_registry_base: $GITLAB_OPENTOFU_IMAGE_BASE
+      version: $CI_COMMIT_SHA
+      base_os: $GITLAB_OPENTOFU_BASE_IMAGE_OS
+      opentofu_version: $OPENTOFU_VERSION
+      root_dir: $TEST_GITLAB_TOFU_ROOT_DIR
+      stage: destroy
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      rules: [{when: always}]
+      enable_id_tokens: true
+      id_tokens_setup_script: $TEST_GITLAB_TOFU_ROOT_DIR/id-tokens-setup.sh
+
+  # For CI Terraform state cleanup
+  - component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/delete-state@$CI_COMMIT_SHA
+    inputs:
+      state_name: $TEST_GITLAB_TOFU_STATE_NAME
+      rules: [{when: always}]
+
+stages: [validate, test, graph, plan, apply, destroy, cleanup]
+
+.gitlab-tofu:id_tokens:
+  id_tokens:
+    GITLAB_OIDC_TOKEN:
+      aud: https://gitlab.com
+
+.check-id-tokens-setup-ran: &check_id_tokens_setup_ran
+  - test -f id-tokens-setup-ran
+
+validate:
+  after_script: *check_id_tokens_setup_ran
+
+test:
+  after_script: *check_id_tokens_setup_ran
+
+graph:
+  after_script: *check_id_tokens_setup_ran
+
+plan:
+  after_script: *check_id_tokens_setup_ran
+
+apply:
+  after_script: *check_id_tokens_setup_ran
+
+destroy:
+  after_script: *check_id_tokens_setup_ran
diff --git a/tests/integration.gitlab-ci.yml b/tests/integration.gitlab-ci.yml
index f5ed0b8e88b23543c0a6c8f2a335c8457b73ae6d..6f3be6dec59e66aba46d447e5132ea6a7c0dd80f 100644
--- a/tests/integration.gitlab-ci.yml
+++ b/tests/integration.gitlab-ci.yml
@@ -175,3 +175,21 @@ destroy-job-template:
         GITLAB_OPENTOFU_BASE_IMAGE_OS:
           - alpine
           - debian
+
+id-tokens:
+  stage: test-integration
+  variables:
+    OPENTOFU_VERSION: $LATEST_OPENTOFU_VERSION
+    TEST_GITLAB_TOFU_STATE_NAME: ci-integration-$CI_JOB_NAME_SLUG-$CI_PIPELINE_IID-$CI_NODE_INDEX
+    TEST_GITLAB_TOFU_ROOT_DIR: tests/iac-id-tokens
+  trigger:
+    include: tests/integration-tests/$PIPELINE_NAME.gitlab-ci.yml
+    strategy: depend
+  parallel:
+    matrix:
+      - PIPELINE_NAME:
+          - IdTokens
+        GITLAB_OPENTOFU_BASE_IMAGE_OS:
+          - alpine
+          - debian
+