Skip to content
Open
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
85 changes: 84 additions & 1 deletion docs/resources/event_stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ resource "auth0_event_stream" "my_event_stream_webhook" {
}
}
}

# Creates an event stream of type webhook with write-only token
resource "auth0_event_stream" "my_event_stream_webhook_wo" {
name = "my-webhook-wo"
destination_type = "webhook"
subscriptions = [
"user.created",
"user.updated"
]

webhook_configuration {
webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net"

webhook_authorization {
method = "bearer"
token_wo = var.webhook_token # From sensitive variable
token_wo_version = 1
}
}
}
```

<!-- schema generated by tfplugindocs -->
Expand Down Expand Up @@ -98,9 +118,72 @@ Required:
Optional:

- `password` (String, Sensitive) The password for `basic` authentication. Required when `method` is set to `basic`.
- `token` (String, Sensitive) The token used for `bearer` authentication. Required when `method` is set to `bearer`.
- `token` (String, Sensitive) The token used for `bearer` authentication. Required when `method` is set to `bearer`. **Note**: This value is stored in Terraform state. For enhanced security, consider using `token_wo` instead.
- `token_wo` (String, Sensitive, Write-Only) The token used for `bearer` authentication (write-only). This value is not stored in Terraform state and provides enhanced security. Required when `method` is set to `bearer` and `token` is not provided. Must be used together with `token_wo_version`.
- `token_wo_version` (Number) Version number for the write-only token. Increment this value when the token changes to trigger an update. Required when `token_wo` is provided.
- `username` (String) The username for `basic` authentication. Required when `method` is set to `basic`.

## Write-Only Token Support

The `token_wo` field allows you to provide a bearer token without storing it in Terraform state, providing enhanced security for sensitive tokens.

### Key Features

- **Not stored in state**: The `token_wo` value is never stored in Terraform state files
- **Not in plan files**: Write-only values are excluded from plan files
- **Version tracking**: Use `token_wo_version` to track changes and trigger updates

### Usage

When using `token_wo`, you must also provide `token_wo_version`. To update the token:

1. Change the `token_wo` value
2. Increment `token_wo_version` (e.g., from `1` to `2`)
3. Run `terraform apply`

### Example: Updating a Write-Only Token

```terraform
# Initial creation
resource "auth0_event_stream" "my_event_stream_webhook_wo" {
name = "my-webhook-wo"
destination_type = "webhook"
subscriptions = ["user.created", "user.updated"]

webhook_configuration {
webhook_endpoint = "https://example.com/webhook"
webhook_authorization {
method = "bearer"
token_wo = var.webhook_token
token_wo_version = 1
}
}
}

# When token needs to be updated
resource "auth0_event_stream" "my_event_stream_webhook_wo" {
name = "my-webhook-wo"
destination_type = "webhook"
subscriptions = ["user.created", "user.updated"]

webhook_configuration {
webhook_endpoint = "https://example.com/webhook"
webhook_authorization {
method = "bearer"
token_wo = var.webhook_token # New token value
token_wo_version = 2 # Incremented to trigger update
}
}
}
```

### Important Notes

- `token_wo` and `token` are mutually exclusive - you cannot use both at the same time
- When `token_wo` is provided, `token_wo_version` is required
- The `token_wo` value cannot be retrieved after resource creation
- Always increment `token_wo_version` when the token changes to ensure Terraform detects the update

## Import

Import is supported using the following syntax:
Expand Down
28 changes: 28 additions & 0 deletions examples/resources/auth0_event_stream/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,31 @@ resource "auth0_event_stream" "my_event_stream_webhook" {
}
}
}

# Creates an event stream of type webhook with write-only token
# This example shows how to use write-only token for enhanced security
# The token value is not stored in Terraform state
variable "webhook_token" {
description = "The webhook token (sensitive, write-only)"
type = string
sensitive = true
}

resource "auth0_event_stream" "my_event_stream_webhook_wo" {
name = "my-webhook-wo"
destination_type = "webhook"
subscriptions = [
"user.created",
"user.updated"
]

webhook_configuration {
webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net"

webhook_authorization {
method = "bearer"
token_wo = var.webhook_token
token_wo_version = 1
}
}
}
22 changes: 20 additions & 2 deletions internal/auth0/eventstream/expand.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,26 @@ func expandEventStreamDestination(data *schema.ResourceData) *management.EventSt
authMap["password"] = v
}
} else if method == "bearer" {
if v, ok := auth["token"].(string); ok {
authMap["token"] = v
// For write-only token, read from raw config to ensure we get the value
// even if it's not in state (since write-only fields are not stored)
cfg := data.GetRawConfig()
webhookCfgRaw := cfg.GetAttr("webhook_configuration")
if !webhookCfgRaw.IsNull() && webhookCfgRaw.LengthInt() > 0 {
authRaw := webhookCfgRaw.Index(cty.NumberIntVal(0)).GetAttr("webhook_authorization")
if !authRaw.IsNull() && authRaw.LengthInt() > 0 {
tokenWORaw := authRaw.Index(cty.NumberIntVal(0)).GetAttr("token_wo")
if !tokenWORaw.IsNull() {
if tokenWO := value.String(tokenWORaw); tokenWO != nil && *tokenWO != "" {
authMap["token"] = *tokenWO
}
}
}
}
// Fallback to regular token if write-only token is not provided
if _, hasToken := authMap["token"]; !hasToken {
if v, ok := auth["token"].(string); ok && v != "" {
authMap["token"] = v
}
}
}

Expand Down
17 changes: 13 additions & 4 deletions internal/auth0/eventstream/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,23 @@ func flattenEventStreamDestination(data *schema.ResourceData, dest *management.E
if auth["method"] == "basic" {
authMap["username"] = auth["username"]

// Token is not returned from the API, so we get it from config if available.
// Password is not returned from the API, so we get it from config if available.
if p := data.Get("webhook_configuration.0.webhook_authorization.0.password"); p != nil {
authMap["password"] = p
}
} else if auth["method"] == "bearer" {
// Token is not returned from the API, so we get it from config if available.
if t := data.Get("webhook_configuration.0.webhook_authorization.0.token"); t != nil {
authMap["token"] = t
// For write-only token: preserve version from state, but never read token_wo value
// token_wo is write-only, so it won't be in state after first read
// We check if token_wo_version exists to determine if write-only token is being used
if version := data.Get("webhook_configuration.0.webhook_authorization.0.token_wo_version"); version != nil {
authMap["token_wo_version"] = version
// Do not set token_wo - it's write-only and should never be in state
} else {
// For backward compatibility: preserve regular token from config if it exists
// (only when write-only token is not being used)
if t := data.Get("webhook_configuration.0.webhook_authorization.0.token"); t != nil {
authMap["token"] = t
}
}
}

Expand Down
84 changes: 80 additions & 4 deletions internal/auth0/eventstream/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package eventstream

import (
"context"
"fmt"

"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"

"github.com/auth0/terraform-provider-auth0/internal/config"
internalError "github.com/auth0/terraform-provider-auth0/internal/error"
"github.com/auth0/terraform-provider-auth0/internal/value"
)

var webhookConfig = &schema.Resource{
Expand Down Expand Up @@ -47,10 +50,31 @@ var webhookConfig = &schema.Resource{
Description: "The password for `basic` authentication. Required when `method` is set to `basic`.",
},
"token": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The token used for `bearer` authentication. Required when `method` is set to `bearer`.",
Type: schema.TypeString,
Optional: true,
Sensitive: true,
Description: "The token used for `bearer` authentication. Required when `method` is set to `bearer`. " +
"**Note**: This value is stored in Terraform state. For enhanced security, consider using `token_wo` instead.",
ConflictsWith: []string{"webhook_configuration.0.webhook_authorization.0.token_wo"},
},
"token_wo": {
Type: schema.TypeString,
Optional: true,
Sensitive: true,
WriteOnly: true,
Description: "The token used for `bearer` authentication (write-only). " +
"This value is not stored in Terraform state and provides enhanced security. " +
"Required when `method` is set to `bearer` and `token` is not provided. " +
"Must be used together with `token_wo_version`.",
ConflictsWith: []string{"webhook_configuration.0.webhook_authorization.0.token"},
},
"token_wo_version": {
Type: schema.TypeInt,
Optional: true,
Description: "Version number for the write-only token. " +
"Increment this value when the token changes to trigger an update. " +
"Required when `token_wo` is provided.",
RequiredWith: []string{"webhook_configuration.0.webhook_authorization.0.token_wo"},
},
},
},
Expand Down Expand Up @@ -84,6 +108,7 @@ func NewResource() *schema.Resource {
ReadContext: readEventStream,
UpdateContext: updateEventStream,
DeleteContext: deleteEventStream,
CustomizeDiff: validateWebhookAuthorization,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Expand Down Expand Up @@ -192,3 +217,54 @@ func deleteEventStream(ctx context.Context, data *schema.ResourceData, m interfa

return nil
}

// validateWebhookAuthorization validates webhook authorization configuration.
// Ensures that when method is "bearer", either token or token_wo is provided (but not both).
func validateWebhookAuthorization(ctx context.Context, diff *schema.ResourceDiff, m interface{}) error {
webhookCfgList, ok := diff.Get("webhook_configuration").([]interface{})
if !ok || len(webhookCfgList) == 0 {
return nil
}

webhookCfg := webhookCfgList[0].(map[string]interface{})
authList, ok := webhookCfg["webhook_authorization"].([]interface{})
if !ok || len(authList) == 0 {
return nil
}

auth := authList[0].(map[string]interface{})
method, ok := auth["method"].(string)
if !ok || method != "bearer" {
return nil
}

// Check if both token and token_wo are provided
hasToken := false
hasTokenWO := false

if token, ok := auth["token"].(string); ok && token != "" {
hasToken = true
}

// For write-only fields, we need to check the raw config since they're not in state
cfg := diff.GetRawConfig()
webhookCfgRaw := cfg.GetAttr("webhook_configuration")
if !webhookCfgRaw.IsNull() && webhookCfgRaw.LengthInt() > 0 {
authRaw := webhookCfgRaw.Index(cty.NumberIntVal(0)).GetAttr("webhook_authorization")
if !authRaw.IsNull() && authRaw.LengthInt() > 0 {
tokenWORaw := authRaw.Index(cty.NumberIntVal(0)).GetAttr("token_wo")
if !tokenWORaw.IsNull() {
if tokenWO := value.String(tokenWORaw); tokenWO != nil && *tokenWO != "" {
hasTokenWO = true
}
}
}
}

// Ensure at least one token is provided
if !hasToken && !hasTokenWO {
return fmt.Errorf("when `method` is `bearer`, either `token` or `token_wo` must be provided")
}

return nil
}
72 changes: 72 additions & 0 deletions internal/auth0/eventstream/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,75 @@ func TestAccEventStream(t *testing.T) {
},
})
}

const testEventStreamCreateWebhookWithWriteOnlyToken = `
resource "auth0_event_stream" "my_event_stream_webhook_wo" {
name = "{{.testName}}-my-webhook-wo"
destination_type = "webhook"
subscriptions = ["user.created", "user.updated"]

webhook_configuration {
webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net"
webhook_authorization {
method = "bearer"
token_wo = "initial-write-only-token"
token_wo_version = 1
}
}
}
`

const testEventStreamUpdateWebhookWithWriteOnlyToken = `
resource "auth0_event_stream" "my_event_stream_webhook_wo" {
name = "{{.testName}}-my-webhook-wo"
destination_type = "webhook"
subscriptions = ["user.updated"]

webhook_configuration {
webhook_endpoint = "https://eof28wtn4v4506o.m.pipedream.net"
webhook_authorization {
method = "bearer"
token_wo = "updated-write-only-token"
token_wo_version = 2
}
}
}
`

func TestAccEventStreamWithWriteOnlyToken(t *testing.T) {
acctest.Test(t, resource.TestCase{
Steps: []resource.TestStep{
{
Config: acctest.ParseTestName(testEventStreamCreateWebhookWithWriteOnlyToken, t.Name()),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "name", fmt.Sprintf("%s-my-webhook-wo", t.Name())),
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "destination_type", "webhook"),
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_endpoint", "https://eof28wtn4v4506o.m.pipedream.net"),
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.method", "bearer"),
// Verify token_wo is NOT in state (write-only)
resource.TestCheckNoResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo"),
// Verify token_wo_version IS in state
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo_version", "1"),
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.#", "2"),
resource.TestCheckTypeSetElemAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.*", "user.created"),
resource.TestCheckTypeSetElemAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.*", "user.updated"),
),
},
// Update with new token and incremented version
{
Config: acctest.ParseTestName(testEventStreamUpdateWebhookWithWriteOnlyToken, t.Name()),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "name", fmt.Sprintf("%s-my-webhook-wo", t.Name())),
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "destination_type", "webhook"),
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.method", "bearer"),
// Verify token_wo is still NOT in state
resource.TestCheckNoResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo"),
// Verify token_wo_version was updated
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "webhook_configuration.0.webhook_authorization.0.token_wo_version", "2"),
resource.TestCheckResourceAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.#", "1"),
resource.TestCheckTypeSetElemAttr("auth0_event_stream.my_event_stream_webhook_wo", "subscriptions.*", "user.updated"),
),
},
},
})
}
Loading