diff --git a/docs/resources/oauth2_client.md b/docs/resources/oauth2_client.md index a975043..fda126f 100644 --- a/docs/resources/oauth2_client.md +++ b/docs/resources/oauth2_client.md @@ -15,6 +15,21 @@ OAuth2 clients are used for machine-to-machine authentication or user-facing OAu ~> **Important:** The `client_secret` is only returned when the client is first created. Store it securely immediately after creation. It cannot be retrieved later, including after `terraform import`. +## Custom Client ID + +By default, a random `client_id` is generated when the client is created. You can specify a custom `client_id` for consistency across environments: + +```hcl +resource "ory_oauth2_client" "api" { + client_id = "my-api-client" + client_name = "API Client" + grant_types = ["client_credentials"] + scope = "read write" +} +``` + +~> **Note:** Changing the `client_id` after creation forces the resource to be destroyed and recreated. The `client_id` must be unique within the project. + ## Example Usage ```terraform @@ -141,6 +156,14 @@ resource "ory_oauth2_client" "cli_tool" { scope = "openid offline_access" } +# Client with a custom client_id (useful for consistency across environments) +resource "ory_oauth2_client" "custom_id" { + client_id = "my-api-client" + client_name = "API Client with Custom ID" + grant_types = ["client_credentials"] + scope = "api:read api:write" +} + # Same-apply: Create project and OAuth2 client together # Use resource-level credentials when the project doesn't exist yet resource "ory_oauth2_client" "same_apply" { @@ -329,6 +352,7 @@ terraform import ory_oauth2_client.api - `backchannel_logout_session_required` (Boolean) Whether the client requires a session identifier in back-channel logout notifications. - `backchannel_logout_uri` (String) OpenID Connect back-channel logout URI. - `client_credentials_grant_access_token_lifespan` (String) Access token lifespan for client credentials grant (e.g., '1h', '30m'). +- `client_id` (String) The OAuth2 client ID. If not specified, a random ID will be generated. Once set, changing this value forces recreation of the resource. - `client_uri` (String) URL of the client's homepage. - `contacts` (List of String) List of contact email addresses for the client maintainers. - `device_authorization_grant_access_token_lifespan` (String) Access token lifespan for device authorization grant (e.g., '1h'). @@ -364,6 +388,5 @@ terraform import ory_oauth2_client.api ### Read-Only -- `client_id` (String) The OAuth2 client ID. - `client_secret` (String, Sensitive) The OAuth2 client secret. Only returned on creation. - `id` (String) Internal Terraform ID (same as client_id). diff --git a/examples/resources/ory_oauth2_client/resource.tf b/examples/resources/ory_oauth2_client/resource.tf index 5b8708c..d3305a6 100644 --- a/examples/resources/ory_oauth2_client/resource.tf +++ b/examples/resources/ory_oauth2_client/resource.tf @@ -121,6 +121,14 @@ resource "ory_oauth2_client" "cli_tool" { scope = "openid offline_access" } +# Client with a custom client_id (useful for consistency across environments) +resource "ory_oauth2_client" "custom_id" { + client_id = "my-api-client" + client_name = "API Client with Custom ID" + grant_types = ["client_credentials"] + scope = "api:read api:write" +} + # Same-apply: Create project and OAuth2 client together # Use resource-level credentials when the project doesn't exist yet resource "ory_oauth2_client" "same_apply" { diff --git a/internal/resources/oauth2client/resource.go b/internal/resources/oauth2client/resource.go index b611659..cfbb4af 100644 --- a/internal/resources/oauth2client/resource.go +++ b/internal/resources/oauth2client/resource.go @@ -115,6 +115,14 @@ resource "ory_oauth2_client" "api" { token_endpoint_auth_method = "client_secret_post" } +# With a custom client_id +resource "ory_oauth2_client" "custom" { + client_id = "my-api-client" + client_name = "API Client with Custom ID" + grant_types = ["client_credentials"] + scope = "read write" +} + output "client_id" { value = ory_oauth2_client.api.client_id } @@ -149,10 +157,12 @@ func (r *OAuth2ClientResource) Schema(ctx context.Context, req resource.SchemaRe }, }, "client_id": schema.StringAttribute{ - Description: "The OAuth2 client ID.", + Description: "The OAuth2 client ID. If not specified, a random ID will be generated. Once set, changing this value forces recreation of the resource.", + Optional: true, Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), }, }, "client_secret": schema.StringAttribute{ @@ -407,6 +417,10 @@ func (r *OAuth2ClientResource) Create(ctx context.Context, req resource.CreateRe Scope: ory.PtrString(plan.Scope.ValueString()), } + if !plan.ClientID.IsNull() && !plan.ClientID.IsUnknown() { + oauthClient.ClientId = ory.PtrString(plan.ClientID.ValueString()) + } + if !plan.GrantTypes.IsNull() && !plan.GrantTypes.IsUnknown() { var grantTypes []string resp.Diagnostics.Append(plan.GrantTypes.ElementsAs(ctx, &grantTypes, false)...) diff --git a/internal/resources/oauth2client/resource_test.go b/internal/resources/oauth2client/resource_test.go index 867fe4f..15a889f 100644 --- a/internal/resources/oauth2client/resource_test.go +++ b/internal/resources/oauth2client/resource_test.go @@ -3,7 +3,9 @@ package oauth2client_test import ( + "fmt" "testing" + "time" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -227,6 +229,51 @@ func TestAccOAuth2ClientResource_withResourceCredentials(t *testing.T) { }) } +func TestAccOAuth2ClientResource_withCustomClientID(t *testing.T) { + clientID := fmt.Sprintf("tf-acc-test-%d", time.Now().UnixNano()) + + acctest.RunTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccPreCheck(t) }, + ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + // Create with custom client_id + { + Config: acctest.LoadTestConfig(t, "testdata/with_custom_client_id.tf.tmpl", map[string]string{ + "Name": "Test Client Custom ID", + "ClientID": clientID, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("ory_oauth2_client.test", "client_id", clientID), + resource.TestCheckResourceAttr("ory_oauth2_client.test", "id", clientID), + resource.TestCheckResourceAttr("ory_oauth2_client.test", "client_name", "Test Client Custom ID"), + resource.TestCheckResourceAttr("ory_oauth2_client.test", "scope", "api:read"), + resource.TestCheckResourceAttrSet("ory_oauth2_client.test", "client_secret"), + ), + }, + // ImportState + { + ResourceName: "ory_oauth2_client.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"client_secret"}, + }, + // Update (change name/scope, client_id stays the same) + { + Config: acctest.LoadTestConfig(t, "testdata/with_custom_client_id_updated.tf.tmpl", map[string]string{ + "Name": "Test Client Custom ID Updated", + "ClientID": clientID, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("ory_oauth2_client.test", "client_id", clientID), + resource.TestCheckResourceAttr("ory_oauth2_client.test", "id", clientID), + resource.TestCheckResourceAttr("ory_oauth2_client.test", "client_name", "Test Client Custom ID Updated"), + resource.TestCheckResourceAttr("ory_oauth2_client.test", "scope", "api:read api:write"), + ), + }, + }, + }) +} + func TestAccOAuth2ClientResource_withTokenLifespans(t *testing.T) { acctest.RunTest(t, resource.TestCase{ PreCheck: func() { acctest.AccPreCheck(t) }, diff --git a/internal/resources/oauth2client/testdata/with_custom_client_id.tf.tmpl b/internal/resources/oauth2client/testdata/with_custom_client_id.tf.tmpl new file mode 100644 index 0000000..32d75ee --- /dev/null +++ b/internal/resources/oauth2client/testdata/with_custom_client_id.tf.tmpl @@ -0,0 +1,8 @@ +resource "ory_oauth2_client" "test" { + client_id = "[[ .ClientID ]]" + client_name = "[[ .Name ]]" + + grant_types = ["client_credentials"] + response_types = ["token"] + scope = "api:read" +} diff --git a/internal/resources/oauth2client/testdata/with_custom_client_id_updated.tf.tmpl b/internal/resources/oauth2client/testdata/with_custom_client_id_updated.tf.tmpl new file mode 100644 index 0000000..b8ef5f7 --- /dev/null +++ b/internal/resources/oauth2client/testdata/with_custom_client_id_updated.tf.tmpl @@ -0,0 +1,8 @@ +resource "ory_oauth2_client" "test" { + client_id = "[[ .ClientID ]]" + client_name = "[[ .Name ]]" + + grant_types = ["client_credentials"] + response_types = ["token"] + scope = "api:read api:write" +} diff --git a/templates/resources/oauth2_client.md.tmpl b/templates/resources/oauth2_client.md.tmpl index d7f0b81..da80796 100644 --- a/templates/resources/oauth2_client.md.tmpl +++ b/templates/resources/oauth2_client.md.tmpl @@ -15,6 +15,21 @@ OAuth2 clients are used for machine-to-machine authentication or user-facing OAu ~> **Important:** The `client_secret` is only returned when the client is first created. Store it securely immediately after creation. It cannot be retrieved later, including after `terraform import`. +## Custom Client ID + +By default, a random `client_id` is generated when the client is created. You can specify a custom `client_id` for consistency across environments: + +```hcl +resource "ory_oauth2_client" "api" { + client_id = "my-api-client" + client_name = "API Client" + grant_types = ["client_credentials"] + scope = "read write" +} +``` + +~> **Note:** Changing the `client_id` after creation forces the resource to be destroyed and recreated. The `client_id` must be unique within the project. + ## Example Usage {{ tffile "examples/resources/ory_oauth2_client/resource.tf" }}