Quick take — Wrap google_identity_platform_config in a reusable Terraform module: enable email/MFA sign-in, blocking functions, authorized domains, and quotas for production-grade CIAM on GCP. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.
Quickstart (copy-paste)
Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):
provider "google" {
project = "my-project"
region = "us-central1"
}
module "identity_platform" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-identity-platform?ref=v1.0.0"
project_id = "..." # GCP project ID where Identity Platform is configured.
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
What this module is
Google Cloud Identity Platform is GCP’s managed customer identity and access management (CIAM) service — the productized, enterprise-grade sibling of Firebase Authentication. It gives your web and mobile apps fully-hosted sign-in: email/password, phone, anonymous, OIDC, SAML and social providers, plus multi-factor authentication, blocking Cloud Functions, and SMS/email quota controls — without you running an auth server.
The catch is that the project-level configuration of Identity Platform lives in a single, somewhat awkward singleton resource: google_identity_platform_config. Enabling Identity Platform on a project is itself a one-time action (you flip on the identitytoolkit.googleapis.com API and “upgrade” the project), and the config resource then governs sign-in methods, MFA enforcement, authorized domains, anti-abuse quotas, and the blocking-function triggers that run during sign-up/sign-in.
Wrapping it in a module matters because this is exactly the kind of config that is easy to get subtly wrong and dangerous to drift: forget to lock down authorized domains and you invite redirect abuse; leave SMS region allow-lists open and you eat SMS-pumping fraud; toggle MFA per-environment by hand and prod ends up weaker than staging. A module makes the auth posture declarative, reviewable, and identical across dev/staging/prod, with the safety rails (validations, sane defaults) baked in once.
When to use it
- You are building a B2C or B2B2C app and want managed sign-in (email, phone, social, SAML/OIDC) instead of hand-rolling auth.
- You need MFA, blocking functions, or per-tenant isolation that Firebase Auth’s free tier doesn’t cover and want it under Terraform.
- You run multiple environments and require the auth configuration (authorized domains, MFA enforcement, quotas) to be drift-free and code-reviewed.
- You want anti-abuse controls — SMS region allow-lists and sign-up quotas — pinned in IaC for audit and compliance (SOC 2, PCI).
- Skip it if a single Firebase project configured by hand is enough, or if you need full self-managed IdP control (then look at running your own OIDC provider).
Module structure
terraform-module-gcp-identity-platform/
├── versions.tf # provider + Terraform version pins
├── main.tf # project services + google_identity_platform_config
├── variables.tf # var-driven inputs with validation
└── outputs.tf # config id/name + key attributes
# versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
}
}
# main.tf
locals {
# Identity Platform depends on the Identity Toolkit API being enabled.
required_services = toset([
"identitytoolkit.googleapis.com",
])
}
# Ensure the underlying API is on before configuring it. Without this, the
# google_identity_platform_config apply races the API enablement and fails.
resource "google_project_service" "identity_platform" {
for_each = var.enable_required_apis ? local.required_services : toset([])
project = var.project_id
service = each.value
disable_dependent_services = false
disable_on_destroy = false
}
resource "google_identity_platform_config" "this" {
project = var.project_id
# Domains allowed to receive OAuth redirects / action-code links.
# Keep this tight: stale or wildcard-ish entries enable redirect abuse.
authorized_domains = var.authorized_domains
# Allow new end-users to self-register. Set false to make sign-up
# admin-only (invite flows, internal B2B portals).
autodelete_anonymous_users = var.autodelete_anonymous_users
# Project-wide anti-abuse quota on new account creation.
dynamic "quota" {
for_each = var.signup_quota == null ? [] : [var.signup_quota]
content {
sign_up_quota_config {
quota = quota.value.quota
start_time = quota.value.start_time
quota_duration = quota.value.quota_duration
}
}
}
# Multi-factor authentication posture (TOTP / SMS second factor).
dynamic "mfa" {
for_each = var.mfa == null ? [] : [var.mfa]
content {
state = mfa.value.state
dynamic "provider_configs" {
for_each = mfa.value.enable_totp ? [1] : []
content {
state = mfa.value.state
totp_provider_config {
adjacent_intervals = mfa.value.totp_adjacent_intervals
}
}
}
}
}
# Email / password + link sign-in.
dynamic "sign_in" {
for_each = var.sign_in == null ? [] : [var.sign_in]
content {
allow_duplicate_emails = sign_in.value.allow_duplicate_emails
dynamic "email" {
for_each = sign_in.value.email_enabled ? [1] : []
content {
enabled = true
password_required = sign_in.value.email_password_required
}
}
dynamic "phone_number" {
for_each = sign_in.value.phone_enabled ? [1] : []
content {
enabled = true
test_phone_numbers = sign_in.value.test_phone_numbers
}
}
dynamic "anonymous" {
for_each = sign_in.value.anonymous_enabled ? [1] : []
content {
enabled = true
}
}
}
}
# SMS region allow/deny — the single most effective control against
# SMS-pumping fraud. Default policy below is "deny all, allow listed".
dynamic "sms_region_config" {
for_each = length(var.allowed_sms_regions) == 0 ? [] : [1]
content {
allow_by_default {
disallowed_regions = []
}
allowlist_only {
allowed_regions = var.allowed_sms_regions
}
}
}
# Blocking functions: run a Cloud Function before sign-up / before sign-in.
dynamic "blocking_functions" {
for_each = length(var.blocking_function_triggers) == 0 ? [] : [1]
content {
dynamic "triggers" {
for_each = var.blocking_function_triggers
content {
event_type = triggers.key # "beforeCreate" | "beforeSignIn"
function_uri = triggers.value
}
}
forward_inbound_credentials {
id_token = var.forward_id_token
access_token = var.forward_access_token
refresh_token = var.forward_refresh_token
}
}
}
depends_on = [google_project_service.identity_platform]
}
# variables.tf
variable "project_id" {
description = "GCP project ID where Identity Platform is configured."
type = string
validation {
condition = can(regex("^[a-z][a-z0-9-]{4,28}[a-z0-9]$", var.project_id))
error_message = "project_id must be a valid GCP project ID (6-30 lowercase alphanumerics/hyphens)."
}
}
variable "enable_required_apis" {
description = "Enable identitytoolkit.googleapis.com on the project. Disable if managed elsewhere."
type = bool
default = true
}
variable "authorized_domains" {
description = "Domains allowed to receive OAuth redirects and email action links (e.g. app.example.com)."
type = list(string)
default = ["localhost"]
validation {
condition = alltrue([
for d in var.authorized_domains : can(regex("^[a-zA-Z0-9.-]+$", d))
])
error_message = "authorized_domains must contain only hostnames (no scheme, path, or wildcards)."
}
}
variable "autodelete_anonymous_users" {
description = "Automatically purge anonymous users after 30 days of inactivity."
type = bool
default = true
}
variable "signup_quota" {
description = "Anti-abuse quota on new account creation over a sliding window. null disables."
type = object({
quota = number
start_time = string # RFC3339, e.g. "2026-06-09T00:00:00Z"
quota_duration = string # seconds with 's' suffix, e.g. "7200s"
})
default = null
validation {
condition = var.signup_quota == null ? true : var.signup_quota.quota > 0
error_message = "signup_quota.quota must be a positive number."
}
}
variable "mfa" {
description = "Multi-factor authentication configuration."
type = object({
state = string # "DISABLED" | "ENABLED" | "MANDATORY"
enable_totp = optional(bool, true)
totp_adjacent_intervals = optional(number, 1)
})
default = {
state = "ENABLED"
}
validation {
condition = contains(["DISABLED", "ENABLED", "MANDATORY"], var.mfa.state)
error_message = "mfa.state must be one of DISABLED, ENABLED, MANDATORY."
}
}
variable "sign_in" {
description = "Sign-in methods to enable for end-users."
type = object({
allow_duplicate_emails = optional(bool, false)
email_enabled = optional(bool, true)
email_password_required = optional(bool, true)
phone_enabled = optional(bool, false)
test_phone_numbers = optional(map(string), {})
anonymous_enabled = optional(bool, false)
})
default = {
email_enabled = true
}
}
variable "allowed_sms_regions" {
description = "ISO-3166-1 alpha-2 country codes allowed to receive SMS (allowlist-only). Empty = no SMS region restriction."
type = list(string)
default = []
validation {
condition = alltrue([
for r in var.allowed_sms_regions : can(regex("^[A-Z]{2}$", r))
])
error_message = "allowed_sms_regions must be two-letter uppercase ISO country codes (e.g. IN, US, GB)."
}
}
variable "blocking_function_triggers" {
description = "Map of event type to Cloud Function URI, e.g. { beforeCreate = \"https://...\" }."
type = map(string)
default = {}
validation {
condition = alltrue([
for k in keys(var.blocking_function_triggers) : contains(["beforeCreate", "beforeSignIn"], k)
])
error_message = "blocking_function_triggers keys must be 'beforeCreate' or 'beforeSignIn'."
}
}
variable "forward_id_token" {
description = "Forward the user's ID token to blocking functions."
type = bool
default = false
}
variable "forward_access_token" {
description = "Forward the OAuth access token to blocking functions."
type = bool
default = false
}
variable "forward_refresh_token" {
description = "Forward the OAuth refresh token to blocking functions."
type = bool
default = false
}
# outputs.tf
output "config_id" {
description = "Fully-qualified resource ID of the Identity Platform config."
value = google_identity_platform_config.this.id
}
output "config_name" {
description = "Resource name of the config (projects/{project}/config)."
value = google_identity_platform_config.this.name
}
output "project_id" {
description = "Project ID that Identity Platform is configured on."
value = google_identity_platform_config.this.project
}
output "authorized_domains" {
description = "Effective list of authorized domains for OAuth redirects and action links."
value = google_identity_platform_config.this.authorized_domains
}
output "mfa_state" {
description = "Effective MFA enforcement state (DISABLED / ENABLED / MANDATORY)."
value = try(google_identity_platform_config.this.mfa[0].state, "DISABLED")
}
How to use it
module "identity_platform" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-identity-platform?ref=v1.0.0"
project_id = "kv-shop-prod"
authorized_domains = ["shop.kloudvin.com", "account.kloudvin.com"]
# Make MFA mandatory in production, email+password as the primary method.
mfa = {
state = "MANDATORY"
enable_totp = true
}
sign_in = {
email_enabled = true
email_password_required = true
phone_enabled = true
anonymous_enabled = false
}
# Only allow SMS to the regions you actually serve — blocks SMS pumping.
allowed_sms_regions = ["IN", "US", "GB", "AE"]
# Anti-abuse: cap new signups to 1000 per 2-hour window.
signup_quota = {
quota = 1000
start_time = "2026-06-09T00:00:00Z"
quota_duration = "7200s"
}
# Screen new accounts with a blocking Cloud Function (e.g. domain allow-list).
blocking_function_triggers = {
beforeCreate = google_cloudfunctions2_function.auth_gate.url
}
forward_id_token = true
}
# Downstream: wire the config name into a monitoring alert / IAM audit doc.
resource "google_monitoring_alert_policy" "auth_signups" {
display_name = "Identity Platform signup spike — ${module.identity_platform.project_id}"
combiner = "OR"
conditions {
display_name = "High signup rate"
condition_threshold {
filter = "resource.type=\"identitytoolkit.googleapis.com/Project\" AND metric.type=\"identitytoolkit.googleapis.com/identity_toolkit_request_count\""
comparison = "COMPARISON_GT"
threshold_value = 500
duration = "300s"
aggregations {
alignment_period = "60s"
per_series_aligner = "ALIGN_RATE"
}
}
}
# Reference an output so the alert always tracks the managed config.
user_labels = {
config = replace(module.identity_platform.config_name, "/", "_")
}
}
With Terragrunt
Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.
1. Root config — live/terragrunt.hcl (inherited by every module):
remote_state {
backend = "gcs"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
# ...gcs state bucket/container + key per path...
}
}
2. Module config — live/prod/identity_platform/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-gcp-identity-platform?ref=v1.0.0"
}
inputs = {
project_id = "..."
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/identity_platform && terragrunt apply # this module
terragrunt run-all apply # every module under live/prod
Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
project_id |
string |
— | Yes | GCP project ID where Identity Platform is configured. |
enable_required_apis |
bool |
true |
No | Enable identitytoolkit.googleapis.com on the project. |
authorized_domains |
list(string) |
["localhost"] |
No | Domains allowed for OAuth redirects and email action links. |
autodelete_anonymous_users |
bool |
true |
No | Auto-purge anonymous users after 30 days of inactivity. |
signup_quota |
object |
null |
No | Anti-abuse quota on new account creation over a sliding window. |
mfa |
object |
{ state = "ENABLED" } |
No | MFA posture: DISABLED / ENABLED / MANDATORY, with optional TOTP. |
sign_in |
object |
{ email_enabled = true } |
No | Sign-in methods: email, phone, anonymous, and duplicate-email policy. |
allowed_sms_regions |
list(string) |
[] |
No | ISO-3166-1 alpha-2 codes allowed to receive SMS (allowlist-only). |
blocking_function_triggers |
map(string) |
{} |
No | Map of beforeCreate/beforeSignIn to a Cloud Function URI. |
forward_id_token |
bool |
false |
No | Forward the user’s ID token to blocking functions. |
forward_access_token |
bool |
false |
No | Forward the OAuth access token to blocking functions. |
forward_refresh_token |
bool |
false |
No | Forward the OAuth refresh token to blocking functions. |
Outputs
| Name | Description |
|---|---|
config_id |
Fully-qualified resource ID of the Identity Platform config. |
config_name |
Resource name of the config (projects/{project}/config). |
project_id |
Project ID that Identity Platform is configured on. |
authorized_domains |
Effective list of authorized domains for redirects and action links. |
mfa_state |
Effective MFA enforcement state (DISABLED / ENABLED / MANDATORY). |
Enterprise scenario
A fintech runs a customer wallet app across three GCP projects (-dev, -staging, -prod) and must prove to PCI auditors that production auth is strictly harder than lower environments. They consume this module from a single root module with a per-environment tfvars: prod pins mfa.state = "MANDATORY", restricts allowed_sms_regions to the four markets they operate in, caps signups via signup_quota, and routes beforeCreate to a blocking function that rejects disposable-email domains. Because the entire posture is in version control, an attempted “temporary” MFA downgrade shows up as a Terraform diff in code review and never reaches prod silently.
Best practices
- Keep
authorized_domainsminimal and reviewed. Every entry is a valid OAuth redirect target; stale or overly-broad domains are a phishing/redirect-abuse vector. Never leavelocalhostin a production config. - Default SMS to allowlist-only. SMS-pumping fraud can burn thousands of dollars overnight. Set
allowed_sms_regionsto the countries you actually serve rather than leaving phone sign-in globally open. - Make MFA
MANDATORYin prod,ENABLEDbelow. Treat the gap between environments as intentional and code-reviewed; pair TOTP with phone so users aren’t locked out if one factor fails. - Gate sign-up with blocking functions for B2B/regulated apps. A
beforeCreatetrigger lets you enforce email-domain allow-lists, KYC pre-checks, or rate logic — and only forward the ID/access token (forward_id_token) you genuinely need. - Manage the singleton carefully.
google_identity_platform_configis a per-project singleton — import the existing config (terraform import) rather than risk a second source of truth, and never let click-ops in the console drift it. - Pin the module ref and provider. Consume via
?ref=v1.0.0and keephashicorp/google ~> 5.0inversions.tfso an auth-config change is an explicit, auditable version bump — not an accidental provider upgrade.