Deployments and TestingCI/CD Tools

Terraform Provider

Terraform is an infrastructure-as-code tool that lets you define your environment configuration in .tf files and apply it consistently across development, staging, and production. Instead of configuring environments by hand, you declare a desired state and let Terraform manage it. Read more here.

Descope publishes a Terraform provider for managing projects and their configuration.

Important

Terraform is best suited for managing infrastructure and configuration that should be consistent across environments. Dynamic elements of a project—such as individual users, tenants, SSO connections, and SCIM configurations—are not typically managed by Terraform. These are unique to each project or environment and are generally handled through the Descope Console, SDKs, or APIs, and not as infrastructure-as-code.

Project-level SSO settings, SSO Setup Suite options, and the hosted Admin Portal are managed in Terraform — see SSO Settings and Admin Portal below.

Only resources explicitly declared in your configuration will be affected when you run terraform apply. Omitting a block (e.g. connectors) means Terraform will not touch it, while adding a block (even if empty) will cause Terraform to manage it and remove anything not declared.

Prerequisites

The Terraform provider requires a paid Descope license (Pro+). Contact support@descope.com with any licensing questions.

Using the Terraform Provider

Provider Configuration

Declare the Descope provider in your .tf file:

terraform {
  required_providers {
    descope = {
      source  = "descope/descope"
      version = "~> 0.3"
    }
  }
}

Warning

Never hardcode your management key in Terraform configuration files—this risks exposing it in version control. Use environment variables or a secrets manager instead.

VariableDescription
DESCOPE_MANAGEMENT_KEYA valid management key for your Descope company
DESCOPE_BASE_URLOverride the Descope API base URL (optional, for testing)
export DESCOPE_MANAGEMENT_KEY="K2..."

With those set, the provider block needs no additional configuration:

provider "descope" {}

Run terraform init to download the provider:

terraform init

If you need to configure credentials explicitly (e.g. in a module):

variable "descope_management_key" {
  type      = string
  sensitive = true
}

provider "descope" {
  management_key = var.descope_management_key
}

Creating a Project

Add a project resource to your .tf file:

resource "descope_project" "myproject" {
  name        = "project-name"
  environment = "production"
  tags        = ["foo", "bar"]
}

Attributes like tags support dynamically computed values:

variable "additional_project_tags" {
    type     = list(string)
    nullable = false
}

resource "descope_project" "myproject" {
    name = "project-name"
    tags = [
      "foo",
      ...var.additional_project_tags
    ]
}

Examples

Each example below is an attribute inside the descope_project resource.

Project Settings

Configure project-level settings:

  project_settings = {
    refresh_token_expiration = "3 weeks"
    enable_inactivity = true
    inactivity_time = "1 hour"
  }

Full project settings schema reference

Invite Settings

Configure user invitation behavior:

  invite_settings = {
    require_invitation    = true
    invite_url            = "https://example.com/invite"
    add_magiclink_token   = true
    expire_invited_users  = true
    invite_expiration     = "2 weeks"
  }

The expire_invited_users flag causes invited user accounts to expire if the invitation is not accepted within the invite_expiration duration. The invite_expiration field accepts human-readable durations such as "2 weeks" or "4 days", with a minimum value of "1 hour". Use it alongside expire_invited_users and/or add_magiclink_token.

Full invite settings schema reference

Authorization

Configure permissions and roles:

  authorization = {
    permissions = [
      {
        name = "test-permission"
        description = "this is a test"
      }
    ]
    roles = [
      {
        name = "test-role"
        description = "this is a test"
        permissions = ["test-permission"]
      }
    ]
  }

Full authorization schema reference

FGA Schema

Use the fga attribute to manage your project's Fine-Grained Authorization schema as code. In the Descope console, open the FGA tab and use the Code view to copy the schema text. You can paste it inline or load it from a file:

  authorization = {
    # Inline
    fga = "model AuthZ\n..."

    # Or from a file
    fga = file("${path.module}/fga-schema.txt")
  }

Note

The fga value must start with "model AuthZ". Use the code view in the FGA tab — not the visual editor — to copy the schema text.

Full FGA schema reference

Authentication

Configure authentication methods:

  authentication = {
    magic_link = {
        expiration_time = "1 hour"
    }
    password = {
        lock = true
        lock_attempts = 3
        min_length = 8
        disallow_email_match = true
        disallowed_characters = "'\""
        enforce_strength = "very_strong"
    }
    sso = {
        merge_users = true
        redirect_url = var.descope_redirect_url
    }
  }

Full authentication schema reference

Passkeys

Use the passkeys block to configure passkey authentication. The android_fingerprints field restricts which Android apps may use passkeys by their SHA-256 APK key hash fingerprint (colon-separated hex format):

  authentication = {
    passkeys = {
      top_level_domain    = "example.com"
      android_fingerprints = [
        "AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67"
      ]
    }
  }

Full passkeys schema reference

Attributes

Configure custom attributes for users and tenants:

  attributes = {
    user = [
      {
        name = "test attribute user"
        type = "string"
      }
    ]
    tenant = [
      {
        name = "test attribute tenant"
        type = "multiselect"
        select_options = ["A", "B"]
      }
    ]
  }

Full attributes schema reference

Connectors

Connectors support bearer token auth and role-based auth:

  connectors = {
    # Bearer Token Authentication Example
    http = [ {
      name = "Test HTTP"
      description = "A Description"
      base_url = var.http_connector_base_url
      use_static_ips = false
      authentication = {
        bearer_token = var.http_connector_secret
      }
    } ]

    # Role-Based Authentication Example
    aws_s3 = [ {
      name        = "S3 Audit Connector"
      description = "A Description"
      auth_type   = "assumeRole"
      role_arn    = "arn:aws:iam::YOUR_ACCOUNT_ID:role/your-connector-role"
      external_id = "YOUR_EXTERNAL_ID"
      region      = "us-east-1"
      bucket      = "your-audit-logs-bucket"
    } ]

    # reCAPTCHA Enterprise Example
    recaptcha_enterprise = [ {
      name        = "reCAPTCHA Enterprise"
      description = "Bot protection"
      project_id  = var.recaptcha_project_id
      site_key    = var.recaptcha_site_key
      api_key     = var.recaptcha_api_key
      action      = "login"
    } ]
  }

Note

For AWS connector role setup requirements, including trust policy configuration, refer to the specific connector documentation (S3, SES, etc.).

Full connectors schema reference

JWT Templates

Use jwt_templates to configure custom JWT claim templates. You can include a description, control which standard claims are included, and add security features like JTI:

  jwt_templates = {
    user_templates = [
      {
        name        = "app-claims"
        description = "Adds subscription tier and org context to user JWTs"
        template    = jsonencode({
          tier   = "@user.customAttributes.subscriptionTier"
          org_id = "@user.tenants[0].tenantId"
        })

        # Exclude the permissions claim to keep tokens lean
        exclude_permission_claim = true

        # Add a unique JWT ID for replay attack prevention
        add_jti_claim = true

        # Move the user ID to a new dsub claim, allowing sub to be customized
        override_subject_claim = true
      }
    ]
  }

Full JWT templates schema reference

SSO Settings and Admin Portal

Use Terraform to manage project-level SSO (authentication.sso), SSO Setup Suite options (sso_suite_settings), invite emails, and the hosted Admin Portal (admin_portal).

Provider schema source of truth: authentication/sso and adminportal under the project docs tree.

In Terraform (descope_project)Not in Terraform (use Setup Suite / Console / Management API)
Project SSO auth method settings (authentication.sso)Per-tenant SAML/OIDC connections (IdP metadata, certs, maps)
SSO Setup Suite UI options (sso_suite_settings)Per-tenant SCIM tokens and base URLs
Default post-auth redirect URLIndividual tenants and users
SSO invite email templatesDay-to-day customer IdP changes
Admin Portal enablement, style, and widget list

Automate per-tenant connections with the Management SSO SDKs or tenants SSO API, or hand customers the SSO Setup Suite.

Project SSO Settings

These map to Project-level SSO settings in the Console.

  authentication = {
    sso = {
      # Disable direct SSO API/SDK starts (Flows that use SSO still work)
      disabled = false

      # Merge SSO logins into existing users with the same email
      merge_users = true

      # Default post-auth redirect (tenant / SDK / API can override)
      redirect_url = "https://app.example.com/auth/callback"

      # Same SSO domain allowed on more than one tenant
      allow_duplicate_domains = false

      # SSO-mapped roles replace the user's existing roles
      allow_override_roles = true

      # Prefer group-based role maps when priorities conflict
      groups_priority = true

      # Tenant SSO configs must include an SSO domain
      require_sso_domains = true

      # Tenant SSO configs must set a groups attribute name
      require_groups_attribute_name = true

      # Reject login if assertion email domain ≠ configured SSO domains
      block_if_email_domain_mismatch = true

      # Leave email unverified after SSO
      mark_email_as_unverified = false

      # Only allow mapping attributes listed below
      limit_mapping_to_mandatory_attributes = false

      # Attributes that must be present / mappable from the IdP
      # (id = Console "Machine Name"; set custom = true for custom attrs)
      mandatory_user_attributes = [
        { id = "email" },
        { id = "name" },
        { id = "department", custom = true },
      ]

      # SSO Setup Suite — see next subsection
      sso_suite_settings = {
        style_id                  = "customer-facing-style"
        hide_scim                 = false
        hide_saml                 = false
        hide_oidc                 = false
        hide_groups_mapping       = false
        hide_domains              = false
        hide_jit_guide            = false
        force_domain_verification = false
        show_help_contact         = true
        support_email             = "it-support@example.com"
      }

      # Optional: custom connector for SSO invite emails
      email_service = {
        connector = "My Email Connector"
        templates = [
          {
            name      = "SSO Invite"
            subject   = "You've been invited to sign in"
            html_body = "<p>Click <a href='{{link}}'>here</a> to accept.</p>"
            active    = true
          }
        ]
      }
    }
  }
AttributePurpose
disabledBlocks direct SSO via API/SDK; does not disable SSO inside Flows
merge_usersMerge SSO users into existing accounts with the same email (merging guide)
redirect_urlProject default post-auth URL (settings); required for IdP-initiated unless set per tenant
allow_duplicate_domainsAllow the same SSO domain on multiple tenants
allow_override_rolesLet SSO group → role maps override existing roles
groups_priorityEnable groups priority when resolving role maps
require_sso_domainsForce an SSO domain on each tenant SSO config
require_groups_attribute_nameForce a groups attribute name on each tenant SSO config
block_if_email_domain_mismatchFail login when email domain ≠ SSO domains
mark_email_as_unverifiedDo not mark email verified after SSO
mandatory_user_attributesRequired Descope attributes from SSO (id, optional custom)
limit_mapping_to_mandatory_attributesOnly allow maps into those mandatory attributes
sso_suite_settingsHosted Setup Suite UI (below)
email_serviceConnector + templates for SSO invite emails

Registry: authentication.sso nested schema.

Note

For email_service, the connector must be an existing HTTP connector in your connectors block. If any template has active = true, the connector cannot be "Descope". Each template needs html_body or plain_text_body with use_plain_text_body = true. Template names must be unique and cannot be "System".

SSO Setup Suite Settings

Controls what customer IT admins see in the SSO Setup Suite (mirrors SSO Suite settings in the Console).

AttributePurpose
style_idStyle applied to the suite (must exist in the project)
hide_saml / hide_oidcHide SAML or OIDC setup (cannot both be true)
hide_scimHide SCIM in the suite
hide_groups_mappingHide group mapping UI
hide_domainsHide SSO domains UI
hide_jit_guideHide the JIT provisioning guide in the hosted UI
force_domain_verificationRequire DNS domain verification (incompatible with hide_domains = true)
show_help_contactShow a help/support contact in the suite
support_emailEmail shown as that support contact

Admin Portal

Configures the hosted Admin Portal — which widgets appear and which style to use.

  admin_portal = {
    enabled  = true
    style_id = "admin-portal-style"

    # type + widget_id must match widgets that already exist in the project
    # (create/export them under Widgets in the Console, or via the widgets block)
    widgets = [
      {
        type      = "users"
        widget_id = "user-management-widget"
      },
      {
        type      = "roles"
        widget_id = "role-management-widget"
      },
    ]
  }
AttributePurpose
enabledTurn the hosted Admin Portal on or off
style_idStyle for the portal
widgetsOrdered list of widgets (type + widget_id, both required). At least one widget is required when enabled = true

type is the widget type string from the Console (common examples from the provider: users, roles). widget_id is the ID of the widget instance you want to host. Create those widgets under Widgets / Admin Widgets (you can also manage widget JSON via the project's widgets map — see the Registry widgets schema).

Registry: admin_portal nested schema.

If Descope is the IdP for other apps (outbound SAML/OIDC), configure those under applications — see Applications and Federated Apps. That is separate from tenant SSO (customers' IdPs signing into your app).

Flows and Styles

If you've designed custom flows in the Descope console, you can export and load them via Terraform:

  1. In the Descope console, go to Authentication Flows
  2. Open the flow you want to manage, click the export button, and save the JSON file (e.g., flows/sign-up-or-in.json)
  3. Optionally export your flow styles from the same screen and save as flows/styles.json
  4. Reference the files in your configuration:
  flows = {
    "sign-up-or-in" = {
      data = file("${path.module}/flows/sign-up-or-in.json")
    }
  }

  styles = {
    data = file("${path.module}/flows/styles.json")
  }

Full flows schema reference · Full styles schema reference

Full Terraform Plan Example

variable "http_connector_base_url" {
  type = string
}

variable "http_connector_secret" {
  type      = string
  sensitive = true
}

variable "s3_role_arn" {
  type = string
}

variable "s3_external_id" {
  type = string
}

terraform {
  required_providers {
    descope = {
      source  = "descope/descope"
      version = "~> 0.3"
    }
  }
}

provider "descope" {}

resource "descope_project" "my-project" {
  name        = "my-project"
  environment = "production"
  tags        = ["production", "team-auth"]

  project_settings = {
    refresh_token_expiration = "3 weeks"
    enable_inactivity        = true
    inactivity_time          = "2 hours"
  }

  invite_settings = {
    require_invitation   = true
    invite_url           = "https://example.com/invite"
    add_magiclink_token  = true
    expire_invited_users = true
    invite_expiration    = "2 weeks"
  }

  authentication = {
    magic_link = {
      expiration_time = "1 hour"
    }
    password = {
      lock                  = true
      lock_attempts         = 3
      min_length            = 8
      disallow_email_match  = true
      disallowed_characters = "'\""
    }
    sso = {
      merge_users                     = true
      redirect_url                    = "https://example.com/auth/callback"
      allow_override_roles            = true
      groups_priority                 = true
      require_sso_domains             = true
      require_groups_attribute_name   = true
      block_if_email_domain_mismatch  = true
      mark_email_as_unverified        = false
      mandatory_user_attributes = [
        { id = "email" },
        { id = "name" },
        { id = "department", custom = true },
      ]
      sso_suite_settings = {
        style_id                  = "my-brand-style"
        hide_scim                 = false
        hide_saml                 = false
        hide_oidc                 = false
        hide_groups_mapping       = false
        hide_domains              = false
        hide_jit_guide            = false
        force_domain_verification = false
        show_help_contact         = true
        support_email             = "it-support@example.com"
      }
    }
  }

  admin_portal = {
    enabled  = true
    style_id = "admin-portal-style"
    widgets = [
      { type = "users", widget_id = "user-management-widget" },
      { type = "roles", widget_id = "role-management-widget" },
    ]
  }

  attributes = {
    user = [
      {
        name = "subscriptionTier"
        type = "string"
      }
    ]
    tenant = [
      {
        name           = "plan"
        type           = "multiselect"
        select_options = ["free", "pro", "enterprise"]
      }
    ]
  }

  authorization = {
    permissions = [
      {
        name        = "read:data"
        description = "Read access to project data"
      },
      {
        name        = "write:data"
        description = "Write access to project data"
      }
    ]
    roles = [
      {
        name        = "viewer"
        description = "Read-only access"
        permissions = ["read:data"]
      },
      {
        name        = "editor"
        description = "Read and write access"
        permissions = ["read:data", "write:data"]
      }
    ]
  }

  applications = {
    oidc_applications = [
      {
        name                 = "My Web App"
        description          = "Primary OIDC application"
        force_authentication = false
        claims               = ["sub", "exp", "email"]
      }
    ]
    saml_applications = [
      {
        name                        = "My SAML App"
        description                 = "Enterprise SAML integration"
        force_authentication        = false
        default_signature_algorithm = "sha256"
        manual_configuration = {
          acs_url   = "https://example.com/saml/acs"
          entity_id = "https://example.com"
        }
      }
    ]
  }

  jwt_templates = {
    user_templates = [
      {
        name        = "app-claims"
        description = "Adds subscription tier and org context to user JWTs"
        template = jsonencode({
          tier   = "@user.customAttributes.subscriptionTier"
          org_id = "@user.tenants[0].tenantId"
        })
        exclude_permission_claim = true
        add_jti_claim            = true
        override_subject_claim   = true
      }
    ]
  }

  connectors = {
    http = [
      {
        name           = "Internal API"
        description    = "Backend service connector"
        base_url       = var.http_connector_base_url
        use_static_ips = false
        authentication = {
          bearer_token = var.http_connector_secret
        }
      }
    ]
    aws_s3 = [
      {
        name        = "S3 Audit Logs"
        description = "Audit log storage"
        auth_type   = "assumeRole"
        role_arn    = var.s3_role_arn
        external_id = var.s3_external_id
        region      = "us-east-1"
        bucket      = "my-audit-logs-bucket"
      }
    ]
  }

  flows = {
    "sign-up-or-in" = {
      data = file("${path.module}/flows/sign-up-or-in.json")
    }
  }

  styles = {
    data = file("${path.module}/flows/styles.json")
  }
}

Additional Resources

Users and tenants are generally not managed via Terraform, but some dynamic resources have dedicated resource types. Defining them as code keeps access control auditable and consistent across environments.

Management Keys

Use descope_management_key to manage Descope Management Keys as code, alongside the rest of your project configuration.

Important

The raw key value (cleartext) is only available immediately after creation and cannot be retrieved later. Store it in a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault) right after terraform apply.

Keys can be scoped to restrict access at the company level, per project, or by project tag:

# Company-level key (access to all projects)
resource "descope_management_key" "pipeline_key" {
  name        = "CI/CD Pipeline Key"
  description = "Used by the deployment pipeline to manage users"

  rebac = {
    company_roles = ["<role-machine-name>"]
  }
}

output "pipeline_key_value" {
  value     = descope_management_key.pipeline_key.cleartext
  sensitive = true
}

Important

Role names must use the machine name format (kebab-case), not the human-readable format from the Descope Console. For example, use company-asset-mgmt-read-write instead of "Asset Management - Read & Write". See the Management Key Roles documentation for the complete list of machine names.

# Project-scoped key
resource "descope_management_key" "staging_key" {
  name = "Staging Key"

  rebac = {
    project_roles = [
      {
        project_ids = ["<project-id>"]
        roles       = ["<role-machine-name>"]
      }
    ]
  }
}

Full management key schema reference

Descopers (Console Users)

Use descope_descoper to manage Descopers as code. Roles can be scoped to the entire company, to specific projects, or to all projects with a given tag.

Available roles: admin, developer, support, auditor.

# Company admin
resource "descope_descoper" "admin" {
  email = "admin@example.com"
  name  = "Alice Admin"

  rbac = {
    is_company_admin = true
  }
}

# Developer scoped to specific projects
resource "descope_descoper" "developer" {
  email = "dev@example.com"
  name  = "Bob Dev"

  rbac = {
    project_roles = [
      {
        role        = "developer"
        project_ids = ["P123abc", "P456def"]
      }
    ]
  }
}

# Support access for all production-tagged projects
resource "descope_descoper" "support" {
  email = "support@example.com"

  rbac = {
    tag_roles = [
      {
        role = "support"
        tags = ["production"]
      }
    ]
  }
}

Full descoper schema reference

Applications

Like users and tenants, applications are dynamic resources that vary per environment. The Terraform provider supports two types: Federated Apps (First Party Applications, configured inside descope_project) and Inbound Apps (Third Party Applications with Scopes and Consent, managed as a standalone descope_inbound_app resource).

Federated Apps

Use the applications block inside descope_project to configure OIDC and SAML applications for outbound SSO integrations.

  applications = {
    oidc_applications = [
      {
        name                 = "My Web App"
        description          = "Primary OIDC application"
        force_authentication = false
        claims               = ["sub", "exp", "email"]
      }
    ]
    saml_applications = [
      {
        name                       = "My SAML App"
        description                = "Enterprise SAML integration"
        force_authentication       = false
        default_signature_algorithm = "sha256"
        manual_configuration = {
          acs_url   = "https://example.com/saml/acs"
          entity_id = "https://example.com"
        }
      }
    ]
  }

Full applications schema reference

Inbound Apps

Use descope_inbound_app to manage third-party applications that authenticate users via Descope as an OAuth 2.0 identity provider. OAuth clients, MCP server configurations, and partner integrations all benefit from being version-controlled alongside your project.

resource "descope_inbound_app" "my_oauth_client" {
  project_id  = descope_project.myproject.id
  name        = "My OAuth Client"
  description = "OAuth client application"
  logo_url    = "https://example.com/logo.png"
  login_page_url = "https://api.descope.com"
  approved_callback_urls = [
    "https://myapp.com/callback",
    "http://localhost:3000/callback"
  ]
  permissions_scopes = [
    {
      name        = "read:profile"
      description = "Read the user's profile information"
      values      = ["read:data"]
    },
    {
      name        = "write:profile"
      description = "Modify the user's profile information"
      optional    = true
      values      = ["write:data"]
    }
  ]
  attributes_scopes = [
    {
      name        = "email"
      description = "The user's email address"
      values      = ["email"]
    }
  ]
}

Scopes (permissions_scopes, attributes_scopes, connections_scopes) each take the same shape:

FieldRequiredDescription
nameYesUnique identifier for the scope
descriptionYesDescription shown during the consent flow
valuesNoIdentifiers of the underlying permissions, attributes, or connections this scope grants
optionalNoWhen true, the user may decline to grant this scope during authorization

Full inbound app schema reference

Using Terraform Within Your Environment

Terraform tracks your Descope project in a state file. Store it somewhere your team can access — remote backends like S3 or Terraform Cloud work well.

  • Run terraform plan to preview changes before applying.
  • Run terraform apply to apply them.
Was this helpful?

On this page