Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions docs/resources/json_web_key_set.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ JSON Web Keys are used for signing and encrypting tokens. This resource generate

-> **Plan:** Available on all Ory Network plans.

~> **Note:** This resource is **immutable**. Any change to `set_id`, `key_id`, `algorithm`, or `use` will destroy the existing key set and create a new one. Private keys in the old set will be permanently lost.
~> **Note:** This resource is **immutable**. Any change to `project_id`, `set_id`, `key_id`, `algorithm`, or `use` will destroy the existing key set and create a new one. Private keys in the old set will be permanently lost.

## Algorithms

Expand Down Expand Up @@ -46,20 +46,21 @@ Most configurations only need `use = "sig"`.
## Example Usage

```terraform
# RSA signing key set
# RSA signing key set (project_id from provider config)
resource "ory_json_web_key_set" "signing" {
set_id = "token-signing-keys"
key_id = "rsa-sig-1"
algorithm = "RS256"
use = "sig"
}

# ECDSA signing key set (smaller, faster)
# ECDSA signing key set with explicit project_id
resource "ory_json_web_key_set" "ecdsa_signing" {
set_id = "ecdsa-signing-keys"
key_id = "ec-sig-1"
algorithm = "ES256"
use = "sig"
project_id = var.ory_project_id
set_id = "ecdsa-signing-keys"
key_id = "ec-sig-1"
algorithm = "ES256"
use = "sig"
}

# Encryption key set
Expand Down Expand Up @@ -98,9 +99,10 @@ On read, the provider extracts `algorithm`, `use`, and `key_id` from the **first

## Import

Import using the set ID:
Import using the format `project_id/set_id` or just `set_id` (uses provider's project_id):

```shell
terraform import ory_json_web_key_set.signing <project-id>/token-signing-keys
terraform import ory_json_web_key_set.signing token-signing-keys
```

Expand All @@ -116,6 +118,10 @@ After import, `key_id` is populated from the first key in the set. If the set co
- `set_id` (String) The ID of the JSON Web Key Set.
- `use` (String) The intended use: sig (signature) or enc (encryption).

### Optional

- `project_id` (String) The project ID. If not set, uses the provider's project_id.

### Read-Only

- `id` (String) Internal Terraform ID (same as set_id).
Expand Down
13 changes: 7 additions & 6 deletions examples/resources/ory_json_web_key_set/resource.tf
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# RSA signing key set
# RSA signing key set (project_id from provider config)
resource "ory_json_web_key_set" "signing" {
set_id = "token-signing-keys"
key_id = "rsa-sig-1"
algorithm = "RS256"
use = "sig"
}

# ECDSA signing key set (smaller, faster)
# ECDSA signing key set with explicit project_id
resource "ory_json_web_key_set" "ecdsa_signing" {
set_id = "ecdsa-signing-keys"
key_id = "ec-sig-1"
algorithm = "ES256"
use = "sig"
project_id = var.ory_project_id
set_id = "ecdsa-signing-keys"
key_id = "ec-sig-1"
algorithm = "ES256"
use = "sig"
}

# Encryption key set
Expand Down
29 changes: 29 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,35 @@ func (c *OryClient) WorkspaceID() string {
return c.config.WorkspaceID
}

// ResolveProjectSlug resolves a project ID to its slug via the console API.
// This allows resources to accept project_id and auto-resolve the slug needed
// for project API operations (e.g., JWK, OAuth2).
func (c *OryClient) ResolveProjectSlug(ctx context.Context, projectID string) (string, error) {
if c.consoleClient == nil {
return "", fmt.Errorf("console API client not configured: workspace_api_key is required to resolve project_id to slug")
}
project, err := c.GetProject(ctx, projectID)
if err != nil {
return "", fmt.Errorf("resolving project slug for project %s: %w", projectID, err)
}
return project.GetSlug(), nil
}

// ProjectClientForProject returns a project-scoped client for the given project ID.
// It resolves the project slug via the console API and uses the provider's project API key.
func (c *OryClient) ProjectClientForProject(ctx context.Context, projectID string) (*OryClient, error) {
slug, err := c.ResolveProjectSlug(ctx, projectID)
if err != nil {
return nil, err
}
apiKey := c.config.ProjectAPIKey
if apiKey == "" {
return nil, fmt.Errorf("project_api_key is required for project API operations (JWK, OAuth2, etc.): " +
"set it on the provider or via ORY_PROJECT_API_KEY environment variable")
}
return c.WithProjectCredentials(slug, apiKey), nil
}

// WithProjectCredentials returns a new OryClient that uses the given project
// credentials. The returned client shares the console client with the parent
// but has its own isolated project client (lazily initialized).
Expand Down
100 changes: 89 additions & 11 deletions internal/resources/jwk/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
Expand Down Expand Up @@ -38,6 +39,7 @@ type JWKResource struct {
// JWKResourceModel describes the resource data model.
type JWKResourceModel struct {
ID types.String `tfsdk:"id"`
ProjectID types.String `tfsdk:"project_id"`
SetID types.String `tfsdk:"set_id"`
KeyID types.String `tfsdk:"key_id"`
Algorithm types.String `tfsdk:"algorithm"`
Expand All @@ -61,10 +63,11 @@ generate and manage custom key sets for your Ory project.

` + "```hcl" + `
resource "ory_json_web_key_set" "signing" {
set_id = "my-signing-keys"
key_id = "sig-key-1"
algorithm = "RS256"
use = "sig"
project_id = var.ory_project_id
set_id = "my-signing-keys"
key_id = "sig-key-1"
algorithm = "RS256"
use = "sig"
}
` + "```" + `

Expand All @@ -81,9 +84,10 @@ resource "ory_json_web_key_set" "encryption" {

## Import

JWK sets can be imported using their set ID:
JWK sets can be imported using the format ` + "`project_id/set_id`" + ` or just ` + "`set_id`" + `:

` + "```shell" + `
terraform import ory_json_web_key_set.signing <project-id>/my-signing-keys
terraform import ory_json_web_key_set.signing my-signing-keys
` + "```" + `
`
Expand All @@ -100,6 +104,15 @@ func (r *JWKResource) Schema(ctx context.Context, req resource.SchemaRequest, re
stringplanmodifier.UseStateForUnknown(),
},
},
"project_id": schema.StringAttribute{
Description: "The project ID. If not set, uses the provider's project_id.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
Comment on lines +107 to +115
"set_id": schema.StringAttribute{
Description: "The ID of the JSON Web Key Set.",
Required: true,
Expand Down Expand Up @@ -168,13 +181,23 @@ func (r *JWKResource) Create(ctx context.Context, req resource.CreateRequest, re
return
}

projectID := r.resolveProjectID(plan.ProjectID)
projectClient, err := r.resolveProjectClient(ctx, projectID)
if err != nil {
resp.Diagnostics.AddError(
"Error Resolving Project",
err.Error(),
)
return
}

body := ory.CreateJsonWebKeySet{
Alg: plan.Algorithm.ValueString(),
Kid: plan.KeyID.ValueString(),
Use: plan.Use.ValueString(),
}

jwks, err := r.client.CreateJsonWebKeySet(ctx, plan.SetID.ValueString(), body)
jwks, err := projectClient.CreateJsonWebKeySet(ctx, plan.SetID.ValueString(), body)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating JSON Web Key Set",
Expand All @@ -184,6 +207,7 @@ func (r *JWKResource) Create(ctx context.Context, req resource.CreateRequest, re
}

plan.ID = types.StringValue(plan.SetID.ValueString())
plan.ProjectID = types.StringValue(projectID)

// Serialize the keys to JSON
if len(jwks.Keys) > 0 {
Expand All @@ -204,9 +228,18 @@ func (r *JWKResource) Read(ctx context.Context, req resource.ReadRequest, resp *
return
}

jwks, err := r.client.GetJsonWebKeySet(ctx, state.SetID.ValueString())
projectID := r.resolveProjectID(state.ProjectID)
projectClient, err := r.resolveProjectClient(ctx, projectID)
if err != nil {
resp.Diagnostics.AddError(
"Error Resolving Project",
err.Error(),
)
return
}

jwks, err := projectClient.GetJsonWebKeySet(ctx, state.SetID.ValueString())
if err != nil {
// Check if it's a 404
resp.Diagnostics.AddError(
"Error Reading JSON Web Key Set",
"Could not read JWK set "+state.SetID.ValueString()+": "+err.Error(),
Expand All @@ -219,6 +252,8 @@ func (r *JWKResource) Read(ctx context.Context, req resource.ReadRequest, resp *
return
}

state.ProjectID = types.StringValue(projectID)

// Serialize the keys to JSON
keysJSON, err := json.Marshal(jwks)
if err == nil {
Expand Down Expand Up @@ -253,7 +288,17 @@ func (r *JWKResource) Delete(ctx context.Context, req resource.DeleteRequest, re
return
}

err := r.client.DeleteJsonWebKeySet(ctx, state.SetID.ValueString())
projectID := r.resolveProjectID(state.ProjectID)
projectClient, err := r.resolveProjectClient(ctx, projectID)
if err != nil {
resp.Diagnostics.AddError(
"Error Resolving Project",
err.Error(),
)
return
}

err = projectClient.DeleteJsonWebKeySet(ctx, state.SetID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Deleting JSON Web Key Set",
Expand All @@ -264,6 +309,39 @@ func (r *JWKResource) Delete(ctx context.Context, req resource.DeleteRequest, re
}

func (r *JWKResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("set_id"), req.ID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
// Import ID format: project_id/set_id or just set_id (uses provider's project_id)
id := req.ID
var projectID, setID string

if strings.Contains(id, "/") {
parts := strings.SplitN(id, "/", 2)
projectID = parts[0]
setID = parts[1]
} else {
setID = id
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("set_id"), setID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), setID)...)
if projectID != "" {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectID)...)
}
}

// resolveProjectID returns the project ID from the resource attribute or falls back to the provider's project_id.
func (r *JWKResource) resolveProjectID(tfProjectID types.String) string {
if !tfProjectID.IsNull() && !tfProjectID.IsUnknown() {
return tfProjectID.ValueString()
}
Comment on lines +333 to +335
return r.client.ProjectID()
}

// resolveProjectClient returns a client configured for the given project.
// If project_id is provided, it resolves the slug via the console API.
// Otherwise, it falls back to the provider's project credentials.
func (r *JWKResource) resolveProjectClient(ctx context.Context, projectID string) (*client.OryClient, error) {
if projectID != "" {
return r.client.ProjectClientForProject(ctx, projectID)
}
return r.client, nil
}
Comment on lines +339 to 347
24 changes: 21 additions & 3 deletions internal/resources/jwk/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,53 @@
package jwk_test

import (
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"

"github.com/ory/terraform-provider-ory/internal/acctest"
)

func importStateJWKID(s *terraform.State) (string, error) {
rs, ok := s.RootModule().Resources["ory_json_web_key_set.test"]
if !ok {
return "", fmt.Errorf("resource not found: ory_json_web_key_set.test")
}
projectID := rs.Primary.Attributes["project_id"]
setID := rs.Primary.Attributes["set_id"]
return fmt.Sprintf("%s/%s", projectID, setID), nil
}

func TestAccJWKResource_basic(t *testing.T) {
projectID := os.Getenv("ORY_PROJECT_ID")

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.AccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories(),
Steps: []resource.TestStep{
// Create and Read
{
Config: acctest.LoadTestConfig(t, "testdata/basic.tf.tmpl", nil),
Config: acctest.LoadTestConfig(t, "testdata/basic.tf.tmpl", map[string]string{
"ProjectID": projectID,
}),
Comment on lines 26 to +37
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("ory_json_web_key_set.test", "id"),
resource.TestCheckResourceAttr("ory_json_web_key_set.test", "project_id", projectID),
resource.TestCheckResourceAttr("ory_json_web_key_set.test", "set_id", "tf-test-jwks"),
resource.TestCheckResourceAttr("ory_json_web_key_set.test", "key_id", "tf-test-key"),
resource.TestCheckResourceAttr("ory_json_web_key_set.test", "algorithm", "RS256"),
resource.TestCheckResourceAttr("ory_json_web_key_set.test", "use", "sig"),
resource.TestCheckResourceAttrSet("ory_json_web_key_set.test", "keys"),
),
},
// ImportState
// ImportState using composite ID: project_id/set_id
{
ResourceName: "ory_json_web_key_set.test",
ImportState: true,
ImportStateId: "tf-test-jwks",
ImportStateIdFunc: importStateJWKID,
ImportStateVerify: true,
},
},
Expand Down
9 changes: 5 additions & 4 deletions internal/resources/jwk/testdata/basic.tf.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
resource "ory_json_web_key_set" "test" {
set_id = "tf-test-jwks"
key_id = "tf-test-key"
algorithm = "RS256"
use = "sig"
project_id = "[[ .ProjectID ]]"
set_id = "tf-test-jwks"
key_id = "tf-test-key"
algorithm = "RS256"
use = "sig"
}
5 changes: 3 additions & 2 deletions templates/resources/json_web_key_set.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ JSON Web Keys are used for signing and encrypting tokens. This resource generate

-> **Plan:** Available on all Ory Network plans.

~> **Note:** This resource is **immutable**. Any change to `set_id`, `key_id`, `algorithm`, or `use` will destroy the existing key set and create a new one. Private keys in the old set will be permanently lost.
~> **Note:** This resource is **immutable**. Any change to `project_id`, `set_id`, `key_id`, `algorithm`, or `use` will destroy the existing key set and create a new one. Private keys in the old set will be permanently lost.

## Algorithms

Expand Down Expand Up @@ -70,9 +70,10 @@ On read, the provider extracts `algorithm`, `use`, and `key_id` from the **first

## Import

Import using the set ID:
Import using the format `project_id/set_id` or just `set_id` (uses provider's project_id):

```shell
terraform import ory_json_web_key_set.signing <project-id>/token-signing-keys
terraform import ory_json_web_key_set.signing token-signing-keys
```

Expand Down