feat: http provider signing key addition (#10641)

# Which Problems Are Solved

HTTP Request to HTTP providers for Email or SMS are not signed.

# How the Problems Are Solved

Add a Signing Key to the HTTP Provider resources, which is then used to
generate a header to sign the payload.

# Additional Changes

Additional tests for query side of the SMTP provider.

# Additional Context

Closes #10067

---------

Co-authored-by: Marco A. <marco@zitadel.com>
(cherry picked from commit 8909b9a2a6)
This commit is contained in:
Stefan Benz
2025-09-08 13:00:04 +02:00
committed by Livio Spring
parent d2d94ea088
commit 1a7cd6e1af
36 changed files with 2113 additions and 132 deletions

View File

@@ -201,7 +201,12 @@ func (wm *IAMSMTPConfigWriteModel) NewChangedEvent(ctx context.Context, aggregat
return changeEvent, true, nil
}
func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id, description, endpoint string) (*instance.SMTPConfigHTTPChangedEvent, bool, error) {
func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id, description, endpoint string,
signingKey *crypto.CryptoValue,
) (*instance.SMTPConfigHTTPChangedEvent, bool, error) {
changes := make([]instance.SMTPConfigHTTPChanges, 0)
var err error
if wm.HTTPConfig == nil {
@@ -217,6 +222,10 @@ func (wm *IAMSMTPConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggr
if wm.HTTPConfig.Endpoint != endpoint {
changes = append(changes, instance.ChangeSMTPConfigHTTPEndpoint(endpoint))
}
// if signingkey is set, update it as it is encrypted
if signingKey != nil {
changes = append(changes, instance.ChangeSMTPConfigHTTPSigningKey(signingKey))
}
if len(changes) == 0 {
return nil, false, nil
}
@@ -251,7 +260,8 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigAddedEvent(e *instance.SMTPCo
func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigHTTPAddedEvent(e *instance.SMTPConfigHTTPAddedEvent) {
wm.Description = e.Description
wm.HTTPConfig = &HTTPConfig{
Endpoint: e.Endpoint,
Endpoint: e.Endpoint,
SigningKey: e.SigningKey,
}
wm.State = domain.SMTPConfigStateInactive
// If ID has empty value we're dealing with the old and unique smtp settings
@@ -313,6 +323,9 @@ func (wm *IAMSMTPConfigWriteModel) reduceSMTPConfigHTTPChangedEvent(e *instance.
if e.Endpoint != nil {
wm.HTTPConfig.Endpoint = *e.Endpoint
}
if e.SigningKey != nil {
wm.HTTPConfig.SigningKey = e.SigningKey
}
// If ID has empty value we're dealing with the old and unique smtp settings
// These would be the default values for ID and State

View File

@@ -157,6 +157,7 @@ type AddSMSHTTP struct {
Description string
Endpoint string
SigningKey string
}
func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (err error) {
@@ -174,6 +175,12 @@ func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (er
return err
}
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smsEncryption) //nolint
if err != nil {
return err
}
config.SigningKey = code.PlainCode()
err = c.pushAppendAndReduce(ctx,
smsConfigWriteModel,
instance.NewSMSConfigHTTPAddedEvent(
@@ -182,6 +189,7 @@ func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (er
config.ID,
config.Description,
config.Endpoint,
code.Crypted,
),
)
if err != nil {
@@ -196,8 +204,10 @@ type ChangeSMSHTTP struct {
ResourceOwner string
ID string
Description *string
Endpoint *string
Description *string
Endpoint *string
ExpirationSigningKey bool
SigningKey *string
}
func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTTP) (err error) {
@@ -214,12 +224,24 @@ func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTT
if !smsConfigWriteModel.State.Exists() || smsConfigWriteModel.HTTP == nil {
return zerrors.ThrowNotFound(nil, "COMMAND-6NW4I5Kqzj", "Errors.SMSConfig.NotFound")
}
var changedSigningKey *crypto.CryptoValue
if config.ExpirationSigningKey {
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smtpEncryption) //nolint
if err != nil {
return err
}
changedSigningKey = code.Crypted
config.SigningKey = &code.Plain
}
changedEvent, hasChanged, err := smsConfigWriteModel.NewHTTPChangedEvent(
ctx,
InstanceAggregateFromWriteModel(&smsConfigWriteModel.WriteModel),
config.ID,
config.Description,
config.Endpoint)
config.Endpoint,
changedSigningKey)
if err != nil {
return err
}

View File

@@ -27,7 +27,8 @@ type TwilioConfig struct {
}
type HTTPConfig struct {
Endpoint string
Endpoint string
SigningKey *crypto.CryptoValue
}
func NewIAMSMSConfigWriteModel(instanceID, id string) *IAMSMSConfigWriteModel {
@@ -82,7 +83,8 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error {
continue
}
wm.HTTP = &HTTPConfig{
Endpoint: e.Endpoint,
Endpoint: e.Endpoint,
SigningKey: e.SigningKey,
}
wm.Description = e.Description
wm.State = domain.SMSConfigStateInactive
@@ -96,6 +98,9 @@ func (wm *IAMSMSConfigWriteModel) Reduce() error {
if e.Endpoint != nil {
wm.HTTP.Endpoint = *e.Endpoint
}
if e.SigningKey != nil {
wm.HTTP.SigningKey = e.SigningKey
}
case *instance.SMSConfigTwilioActivatedEvent:
if wm.ID != e.ID {
wm.State = domain.SMSConfigStateInactive
@@ -189,7 +194,13 @@ func (wm *IAMSMSConfigWriteModel) NewTwilioChangedEvent(ctx context.Context, agg
return changeEvent, true, nil
}
func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggregate *eventstore.Aggregate, id string, description, endpoint *string) (*instance.SMSConfigHTTPChangedEvent, bool, error) {
func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
id string,
description, endpoint *string,
signingKey *crypto.CryptoValue,
) (*instance.SMSConfigHTTPChangedEvent, bool, error) {
changes := make([]instance.SMSConfigHTTPChanges, 0)
var err error
@@ -203,6 +214,10 @@ func (wm *IAMSMSConfigWriteModel) NewHTTPChangedEvent(ctx context.Context, aggre
if endpoint != nil && wm.HTTP.Endpoint != *endpoint {
changes = append(changes, instance.ChangeSMSConfigHTTPEndpoint(*endpoint))
}
// if signingkey is set, update it as it is encrypted
if signingKey != nil {
changes = append(changes, instance.ChangeSMSConfigHTTPSigningKey(signingKey))
}
if len(changes) == 0 {
return nil, false, nil

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
@@ -306,9 +307,11 @@ func TestCommandSide_ChangeSMSConfigTwilio(t *testing.T) {
func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
alg crypto.EncryptionAlgorithm
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
alg crypto.EncryptionAlgorithm
}
type args struct {
ctx context.Context
@@ -351,10 +354,18 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
"providerid",
"description",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args: args{
ctx: context.Background(),
@@ -374,9 +385,11 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
smsEncryption: tt.fields.alg,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
smsEncryption: tt.fields.alg,
}
err := r.AddSMSConfigHTTP(tt.args.ctx, tt.args.http)
if tt.res.err == nil {
@@ -394,7 +407,9 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
}
type args struct {
ctx context.Context
@@ -474,6 +489,12 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
"providerid",
"description",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -505,6 +526,12 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
"providerid",
"description",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -514,17 +541,26 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
"providerid",
"endpoint2",
"description2",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("87654321"),
},
),
),
),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args: args{
ctx: context.Background(),
http: &ChangeSMSHTTP{
ResourceOwner: "INSTANCE",
ID: "providerid",
Description: gu.Ptr("description2"),
Endpoint: gu.Ptr("endpoint2"),
ResourceOwner: "INSTANCE",
ID: "providerid",
Description: gu.Ptr("description2"),
Endpoint: gu.Ptr("endpoint2"),
ExpirationSigningKey: true,
},
},
res: res{
@@ -537,7 +573,9 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
}
err := r.ChangeSMSConfigHTTP(tt.args.ctx, tt.args.http)
if tt.res.err == nil {
@@ -707,6 +745,12 @@ func TestCommandSide_ActivateSMSConfig(t *testing.T) {
"providerid",
"description",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -917,6 +961,12 @@ func TestCommandSide_DeactivateSMSConfig(t *testing.T) {
"providerid",
"description",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
eventFromEventPusher(
@@ -1083,6 +1133,12 @@ func TestCommandSide_RemoveSMSConfig(t *testing.T) {
"providerid",
"description",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -1141,10 +1197,11 @@ func newSMSConfigTwilioChangedEvent(ctx context.Context, id, sid, senderName, de
return event
}
func newSMSConfigHTTPChangedEvent(ctx context.Context, id, endpoint, description string) *instance.SMSConfigHTTPChangedEvent {
func newSMSConfigHTTPChangedEvent(ctx context.Context, id, endpoint, description string, signingKey *crypto.CryptoValue) *instance.SMSConfigHTTPChangedEvent {
changes := []instance.SMSConfigHTTPChanges{
instance.ChangeSMSConfigHTTPEndpoint(endpoint),
instance.ChangeSMSConfigHTTPDescription(description),
instance.ChangeSMSConfigHTTPSigningKey(signingKey),
}
event, _ := instance.NewSMSConfigHTTPChangedEvent(ctx,
&instance.NewAggregate("INSTANCE").Aggregate,

View File

@@ -230,6 +230,7 @@ type AddSMTPConfigHTTP struct {
Description string
Endpoint string
SigningKey string
}
func (c *Commands) AddSMTPConfigHTTP(ctx context.Context, config *AddSMTPConfigHTTP) (err error) {
@@ -248,12 +249,19 @@ func (c *Commands) AddSMTPConfigHTTP(ctx context.Context, config *AddSMTPConfigH
return err
}
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smtpEncryption) //nolint
if err != nil {
return err
}
config.SigningKey = code.PlainCode()
err = c.pushAppendAndReduce(ctx, smtpConfigWriteModel, instance.NewSMTPConfigHTTPAddedEvent(
ctx,
InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel),
config.ID,
config.Description,
config.Endpoint,
code.Crypted,
))
if err != nil {
return err
@@ -267,8 +275,10 @@ type ChangeSMTPConfigHTTP struct {
ResourceOwner string
ID string
Description string
Endpoint string
Description string
Endpoint string
ExpirationSigningKey bool
SigningKey *string
}
func (c *Commands) ChangeSMTPConfigHTTP(ctx context.Context, config *ChangeSMTPConfigHTTP) (err error) {
@@ -288,12 +298,23 @@ func (c *Commands) ChangeSMTPConfigHTTP(ctx context.Context, config *ChangeSMTPC
return zerrors.ThrowNotFound(nil, "COMMAND-xIrdledqv4", "Errors.SMTPConfig.NotFound")
}
var changedSigningKey *crypto.CryptoValue
if config.ExpirationSigningKey {
code, err := c.newSigningKey(ctx, c.eventstore.Filter, c.smtpEncryption) //nolint
if err != nil {
return err
}
changedSigningKey = code.Crypted
config.SigningKey = &code.Plain
}
changedEvent, hasChanged, err := smtpConfigWriteModel.NewHTTPChangedEvent(
ctx,
InstanceAggregateFromWriteModel(&smtpConfigWriteModel.WriteModel),
config.ID,
config.Description,
config.Endpoint,
changedSigningKey,
)
if err != nil {
return err

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
@@ -902,8 +903,10 @@ func TestCommandSide_ChangeSMTPConfigPassword(t *testing.T) {
func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
eventstore func(t *testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
idGenerator id.Generator
}
type args struct {
http *AddSMTPConfigHTTP
@@ -944,10 +947,18 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
"configid",
"test",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args: args{
http: &AddSMTPConfigHTTP{
@@ -966,8 +977,10 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
}
err := r.AddSMTPConfigHTTP(context.Background(), tt.args.http)
if tt.res.err == nil {
@@ -986,7 +999,9 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
eventstore func(t *testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
}
type args struct {
http *ChangeSMTPConfigHTTP
@@ -1063,6 +1078,12 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
"ID",
"test",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -1094,6 +1115,12 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
"ID",
"",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -1103,16 +1130,25 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
"ID",
"test",
"endpoint2",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("87654321"),
},
),
),
),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
},
args: args{
http: &ChangeSMTPConfigHTTP{
ResourceOwner: "INSTANCE",
ID: "ID",
Description: "test",
Endpoint: "endpoint2",
ResourceOwner: "INSTANCE",
ID: "ID",
Description: "test",
Endpoint: "endpoint2",
ExpirationSigningKey: true,
},
},
res: res{
@@ -1125,7 +1161,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore(t),
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
}
err := r.ChangeSMTPConfigHTTP(context.Background(), tt.args.http)
if tt.res.err == nil {
@@ -1300,6 +1338,12 @@ func TestCommandSide_ActivateSMTPConfig(t *testing.T) {
"ID",
"test",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -1511,6 +1555,12 @@ func TestCommandSide_DeactivateSMTPConfig(t *testing.T) {
"ID",
"test",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
eventFromEventPusher(
@@ -1677,6 +1727,12 @@ func TestCommandSide_RemoveSMTPConfig(t *testing.T) {
"ID",
"test",
"endpoint",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("12345678"),
},
),
),
),
@@ -2051,10 +2107,11 @@ func newSMTPConfigChangedEvent(ctx context.Context, id, description string, tls
return event
}
func newSMTPConfigHTTPChangedEvent(ctx context.Context, id, description, endpoint string) *instance.SMTPConfigHTTPChangedEvent {
func newSMTPConfigHTTPChangedEvent(ctx context.Context, id, description, endpoint string, signingKey *crypto.CryptoValue) *instance.SMTPConfigHTTPChangedEvent {
changes := []instance.SMTPConfigHTTPChanges{
instance.ChangeSMTPConfigHTTPDescription(description),
instance.ChangeSMTPConfigHTTPEndpoint(endpoint),
instance.ChangeSMTPConfigHTTPSigningKey(signingKey),
}
event, _ := instance.NewSMTPConfigHTTPChangeEvent(ctx,
&instance.NewAggregate("INSTANCE").Aggregate,