diff --git a/docs/resources/project_config.md b/docs/resources/project_config.md index e6296c2..7ba0fe6 100644 --- a/docs/resources/project_config.md +++ b/docs/resources/project_config.md @@ -51,10 +51,11 @@ resource "ory_project_config" "secure" { password_max_breaches = 0 # Authentication Methods - enable_password = true - enable_code = true - enable_oidc = true # Required for social providers (Google, GitHub, etc.) - enable_passkey = true + enable_password = true + enable_code = true + code_mfa_enabled = true # Enable code as a second factor for MFA + enable_oidc = true # Required for social providers (Google, GitHub, etc.) + enable_passkey = true # Flow Controls enable_registration = true @@ -340,6 +341,7 @@ Some Ory project settings are not yet available through this resource. For setti - `account_experience_name` (String) Application name shown in the hosted login UI. - `account_experience_stylesheet` (String) Custom CSS stylesheet for the hosted login UI. - `allowed_return_urls` (List of String) List of allowed return URLs. +- `code_mfa_enabled` (Boolean) Enable the code method as a second factor for MFA. When enabled, users can use one-time codes as a second authentication factor. - `cors_admin_enabled` (Boolean) Enable CORS for the admin API. - `cors_admin_origins` (List of String) Allowed CORS origins for the admin API. - `cors_enabled` (Boolean) Enable CORS for the public API. diff --git a/examples/resources/ory_project_config/resource.tf b/examples/resources/ory_project_config/resource.tf index befb14f..fb6e815 100644 --- a/examples/resources/ory_project_config/resource.tf +++ b/examples/resources/ory_project_config/resource.tf @@ -28,10 +28,11 @@ resource "ory_project_config" "secure" { password_max_breaches = 0 # Authentication Methods - enable_password = true - enable_code = true - enable_oidc = true # Required for social providers (Google, GitHub, etc.) - enable_passkey = true + enable_password = true + enable_code = true + code_mfa_enabled = true # Enable code as a second factor for MFA + enable_oidc = true # Required for social providers (Google, GitHub, etc.) + enable_passkey = true # Flow Controls enable_registration = true diff --git a/internal/resources/projectconfig/resource.go b/internal/resources/projectconfig/resource.go index 6554756..76faab7 100644 --- a/internal/resources/projectconfig/resource.go +++ b/internal/resources/projectconfig/resource.go @@ -95,6 +95,7 @@ type ProjectConfigResourceModel struct { // Auth methods EnablePassword types.Bool `tfsdk:"enable_password"` EnableCode types.Bool `tfsdk:"enable_code"` + CodeMFAEnabled types.Bool `tfsdk:"code_mfa_enabled"` EnableOIDC types.Bool `tfsdk:"enable_oidc"` EnableTOTP types.Bool `tfsdk:"enable_totp"` EnableWebAuthn types.Bool `tfsdk:"enable_webauthn"` @@ -475,6 +476,11 @@ func (r *ProjectConfigResource) Schema(ctx context.Context, req resource.SchemaR Description: "Enable code-based authentication.", Optional: true, }, + "code_mfa_enabled": schema.BoolAttribute{ + Description: "Enable the code method as a second factor for MFA. " + + "When enabled, users can use one-time codes as a second authentication factor.", + Optional: true, + }, "enable_oidc": schema.BoolAttribute{ Description: "Enable OIDC (OpenID Connect) social sign-in. Must be enabled for social providers (e.g. Google, GitHub) to work.", Optional: true, @@ -997,6 +1003,7 @@ func (r *ProjectConfigResource) buildPatches(ctx context.Context, plan *ProjectC methodMappings := map[*types.Bool]string{ &plan.EnablePassword: "/services/identity/config/selfservice/methods/password/enabled", &plan.EnableCode: "/services/identity/config/selfservice/methods/code/enabled", + &plan.CodeMFAEnabled: "/services/identity/config/selfservice/methods/code/mfa_enabled", &plan.EnableOIDC: "/services/identity/config/selfservice/methods/oidc/enabled", &plan.EnableTOTP: "/services/identity/config/selfservice/methods/totp/enabled", &plan.EnableWebAuthn: "/services/identity/config/selfservice/methods/webauthn/enabled", @@ -1576,6 +1583,7 @@ func (r *ProjectConfigResource) readProjectConfig(ctx context.Context, project * methodReadMappings := map[*types.Bool][]string{ &state.EnablePassword: {"selfservice", "methods", "password", "enabled"}, &state.EnableCode: {"selfservice", "methods", "code", "enabled"}, + &state.CodeMFAEnabled: {"selfservice", "methods", "code", "mfa_enabled"}, &state.EnableOIDC: {"selfservice", "methods", "oidc", "enabled"}, &state.EnableTOTP: {"selfservice", "methods", "totp", "enabled"}, &state.EnableWebAuthn: {"selfservice", "methods", "webauthn", "enabled"}, diff --git a/internal/resources/projectconfig/resource_test.go b/internal/resources/projectconfig/resource_test.go index c1280c0..515194d 100644 --- a/internal/resources/projectconfig/resource_test.go +++ b/internal/resources/projectconfig/resource_test.go @@ -159,6 +159,51 @@ func TestAccProjectConfigResource_mfaPolicy(t *testing.T) { }) } +func TestAccProjectConfigResource_codeMFA(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.AccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + // Create with code MFA enabled + { + Config: acctest.LoadTestConfig(t, "testdata/code_mfa.tf.tmpl", map[string]string{"CodeMFAEnabled": "true"}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("ory_project_config.test", "id"), + resource.TestCheckResourceAttr("ory_project_config.test", "enable_code", "true"), + resource.TestCheckResourceAttr("ory_project_config.test", "code_mfa_enabled", "true"), + ), + }, + // ImportState — config fields are ignored because import only sets + // id/project_id; Read only refreshes fields that are non-null in + // state, so config attributes won't be populated until apply. + { + ResourceName: "ory_project_config.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "enable_code", "code_mfa_enabled", + "cors_enabled", "password_min_length", + "smtp_connection_uri", + }, + }, + // Update to disabled + { + Config: acctest.LoadTestConfig(t, "testdata/code_mfa.tf.tmpl", map[string]string{"CodeMFAEnabled": "false"}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("ory_project_config.test", "enable_code", "true"), + resource.TestCheckResourceAttr("ory_project_config.test", "code_mfa_enabled", "false"), + ), + }, + // Verify no perpetual diff + { + Config: acctest.LoadTestConfig(t, "testdata/code_mfa.tf.tmpl", map[string]string{"CodeMFAEnabled": "false"}), + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) +} + func TestAccProjectConfigResource_oidc(t *testing.T) { acctest.RequireSocialProviderTests(t) resource.Test(t, resource.TestCase{ diff --git a/internal/resources/projectconfig/testdata/code_mfa.tf.tmpl b/internal/resources/projectconfig/testdata/code_mfa.tf.tmpl new file mode 100644 index 0000000..545c109 --- /dev/null +++ b/internal/resources/projectconfig/testdata/code_mfa.tf.tmpl @@ -0,0 +1,4 @@ +resource "ory_project_config" "test" { + enable_code = true + code_mfa_enabled = [[ .CodeMFAEnabled ]] +}