Deployments and Testing/CI/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 at developer.hashicorp.com/terraform/intro.

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.

Prerequisites

Note

The Terraform provider works with a paid Descope license (Pro +). For licensing questions, contact support@descope.com.

  • Terraform CLI 1.0 or later installed.
  • A Management Key from Company Settings. Set the scope to All Projects if you intend to create new projects via Terraform.

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

Authentication

Configure authentication methods:

  authentication = {
    magic_link = {
        expiration_time = "1 hour"
    }
    password = {
        lock = true
        lock_attempts = 3
        min_length = 8
    }
    sso = {
        merge_users = true
        redirect_url = var.descope_redirect_url
    }
  }

Full authentication 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"
    } ]
  }

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

Configure global settings for Single Sign-On across all tenants in your project:

  authentication = {
    sso = {
      # Merge SSO users with existing accounts of the same email
      merge_users = true
 
      # Allow SSO roles to override a user's existing roles
      allow_override_roles = true
 
      # Prioritize group-based role mappings over direct role assignments
      groups_priority = true
 
      # Enforce that SSO domains are always specified
      require_sso_domains = true
 
      # Require a groups attribute name in SSO configuration
      require_groups_attribute_name = true
 
      # Block login if the user's email domain doesn't match the configured SSO domains
      block_if_email_domain_mismatch = true
 
      # Mark the user's email as unverified during SSO authentication
      mark_email_as_unverified = false
 
      # Define required Descope attributes when receiving SSO information
      mandatory_user_attributes = [
        { id = "email" },
        { id = "name" },
        { id = "department", custom = true },
      ]
 
      # Configure the SSO Suite portal appearance
      sso_suite_settings = {
        style_id                  = "my-brand-style"
        hide_scim                 = false
        hide_saml                 = false
        hide_oidc                 = false
        hide_groups_mapping       = false
        hide_domains              = false
        force_domain_verification = false
      }
 
      # Use a custom email connector for SSO Suite 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 your invitation.</p>"
            active  = true
          }
        ]
      }
    }
  }

Note

When using email_service, the connector must be set to an existing HTTP connector defined in your connectors block. If any template has active = true, the connector cannot be "Descope" (the built-in default). Each template requires either html_body (default) or plain_text_body with use_plain_text_body = true. Template names must be unique and cannot be "System".

Full SSO settings schema reference

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
    }
    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
        force_domain_verification = false
      }
    }
  }
 
  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-name>"]
  }
}
 
output "pipeline_key_value" {
  value     = descope_management_key.pipeline_key.cleartext
  sensitive = true
}
# Project-scoped key
resource "descope_management_key" "staging_key" {
  name = "Staging Key"
 
  rebac = {
    project_roles = [
      {
        project_ids = ["<project-id>"]
        roles       = ["<role-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?