Merge commit from fork

* Add DenyLists parsing

* Remove unneeded returned error

* Plug global denylist into Command

* app creation: apply denylist to backchannel logout URI

* Inject denylist to backchannel logout worker

* webhook config: validate against blocked URLs

* Add notificationsWebhook denylist target

* command: Add SMTP endpoint validation against blocklist

* command: Add SMS endpoint validation against blocklist

* Validate webhook endpoint against denylist on channel notification

* Remove unused tests

* handle deprecated denylists

* remove unintended denylist entry in deprecated list

* use single http client

* fix tests

* update comments

* fixes

* cleanup

* address comments

* fix merge

---------

Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
Marco A.
2026-06-15 15:36:14 +02:00
committed by GitHub
co-authored by Livio Spring
parent d184e976fc
commit 8e82ec1cb9
97 changed files with 1834 additions and 285 deletions
+5 -1
View File
@@ -77,6 +77,7 @@ LogStore:
Enabled: true
Executions:
# Deprecated: use HTTPClient.DenyList
DenyList: # ZITADEL_EXECUTIONS_DENYLIST (comma separated list)
Projections:
@@ -129,4 +130,7 @@ OIDC:
DefaultLogoutURLV2: "/logout?post_logout_redirect=" # ZITADEL_OIDC_DEFAULTLOGOUTURLV2
SAML:
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
DefaultLoginURLV2: "/login?authRequest=" # ZITADEL_SAML_DEFAULTLOGINURLV2
HTTPClient:
DenyList:
+35 -10
View File
@@ -598,12 +598,9 @@ Executions:
MaxTtl: 5m # ZITADEL_EXECUTIONS_MAXTTL
# List of domains and IPs that are not valid execution target's endpoints
# Wildcard sub domains are currently unsupported
# Deprecated: use HTTPClient.DenyList instead. If both are set, this list will be merged into the HTTPClient.DenyList.
DenyList: # ZITADEL_EXECUTIONS_DENYLIST (comma separated list)
- localhost
- "127.0.0.0/8"
- "::1"
- "0.0.0.0"
- "::"
Auth:
# See Projections.BulkLimit
@@ -1033,12 +1030,8 @@ SystemDefaults:
Actions:
HTTP:
# Wildcard sub domains are currently unsupported
# Deprecated: use HTTPClient.DenyList instead. If both are set, this list will be merged into the HTTPClient.DenyList.
DenyList: # ZITADEL_ACTIONS_HTTP_DENYLIST (comma separated list)
- localhost
- "127.0.0.0/8"
- "::1"
- "0.0.0.0"
- "::"
LogStore:
Access:
@@ -1072,6 +1065,38 @@ Eventstore:
# Maximum amount of push retries in case of primary key violation on the sequence
MaxRetries: 5 #ZITADEL_EVENTSTORE_MAXRETRIES
HTTPClient:
# Sets the limit of the response body size in bytes, which will be processed by the HTTP client.
# If the response body exceeds this limit, the client will return an error.
# The default value is 20MB.
MaxBodySize: 20971520 # ZITADEL_HTTPCLIENT_MAXBODYSIZE
# Sets the timeout for the HTTP client.
Timeout: 30s # ZITADEL_HTTPCLIENT_TIMEOUT
# Sets the number of allowed redirects for the HTTP client.
MaxRedirects: 5 # ZITADEL_HTTPCLIENT_MAXREDIRECTS
# Specifies if the HTTP client should follow redirects from HTTPS to HTTP.
AllowHTTPSDowngrade: false # ZITADEL_HTTPCLIENT_ALLOWHTTPSDOWNGRADE
# Specifies the list of IP addresses or CIDR ranges that are not allowed to be called by the HTTP client.
# Note that hosts / domains are matched best-case and wildcards are not supported.
# CRITICAL SECURITY NOTE: Domain rules (like "localhost") protect application-layer string matches.
# Underneath, the network dialer relies exclusively on the CIDR ranges below to block resolved IPs and prevent DNS Rebinding.
# Do not remove the CIDR subnets even if a domain mapping exists.
DenyList: # ZITADEL_HTTPCLIENT_DENYLIST (comma separated list)
- localhost
- "0.0.0.0/8" # Unspecified IPv4 address range / Local network routing trick protection
- "10.0.0.0/8" # Class A private networks
- "100.64.0.0/10" # Carrier-Grade NAT space
- "127.0.0.0/8" # Loopback addresses (IPv4)
- "169.254.0.0/16" # Link-local subnets (Covers Cloud Metadata Endpoints like AWS/GCP 169.254.169.254)
- "172.16.0.0/12" # Class B private networks
- "192.168.0.0/16" # Class C private networks
- "198.18.0.0/15" # Benchmark/inter-network communication testing
- "::/128" # Unspecified IPv6 address
- "::1/128" # IPv6 loopback address
- "::ffff:0:0/96" # IPv4-mapped IPv6 addresses (Prevents IPv6 encapsulation bypasses)
- "fc00::/7" # Unique local addresses / IPv6 equivalent to private IPv4 subnets
- "fe80::/10" # Link-local addresses / IPv6 equivalent to cloud metadata subnets
# The DefaultInstance section defines the default values for each new virtual instance that is created.
# Check out https://zitadel.com/docs/concepts/structure/instance#multiple-virtual-instances for more information about virtual instances.
# For the initial setup, the default values are used to create the first instance.
+32
View File
@@ -211,3 +211,35 @@ FirstInstance:
Log:
Level: info
HTTPClient:
# Sets the limit of the response body size in bytes, which will be processed by the HTTP client.
# If the response body exceeds this limit, the client will return an error.
# The default value is 20MB.
MaxBodySize: 20971520 # ZITADEL_HTTPCLIENT_MAXBODYSIZE
# Sets the timeout for the HTTP client.
Timeout: 30s # ZITADEL_HTTPCLIENT_TIMEOUT
# Sets the number of allowed redirects for the HTTP client.
MaxRedirects: 5 # ZITADEL_HTTPCLIENT_MAXREDIRECTS
# Specifies if the HTTP client should follow redirects from HTTPS to HTTP.
AllowHTTPSDowngrade: false # ZITADEL_HTTPCLIENT_ALLOWHTTPSDOWNGRADE
# Specifies the list of IP addresses or CIDR ranges that are not allowed to be called by the HTTP client.
# Note that hosts / domains are matched best-case and wildcards are not supported.
# CRITICAL SECURITY NOTE: Domain rules (like "localhost") protect application-layer string matches.
# Underneath, the network dialer relies exclusively on the CIDR ranges below to block resolved IPs and prevent DNS Rebinding.
# Do not remove the CIDR subnets even if a domain mapping exists.
DenyList: # ZITADEL_HTTPCLIENT_DENYLIST (comma separated list)
- localhost
- "0.0.0.0/8" # Unspecified IPv4 address range / Local network routing trick protection
- "10.0.0.0/8" # Class A private networks
- "100.64.0.0/10" # Carrier-Grade NAT space
- "127.0.0.0/8" # Loopback addresses (IPv4)
- "169.254.0.0/16" # Link-local subnets (Covers Cloud Metadata Endpoints like AWS/GCP 169.254.169.254)
- "172.16.0.0/12" # Class B private networks
- "192.168.0.0/16" # Class C private networks
- "198.18.0.0/15" # Benchmark/inter-network communication testing
- "::/128" # Unspecified IPv6 address
- "::1/128" # IPv6 loopback address
- "::ffff:0:0/96" # IPv4-mapped IPv6 addresses (Prevents IPv6 encapsulation bypasses)
- "fc00::/7" # Unique local addresses / IPv6 equivalent to private IPv4 subnets
- "fe80::/10" # Link-local addresses / IPv6 equivalent to cloud metadata subnets
+8 -4
View File
@@ -5,7 +5,6 @@ import (
"database/sql"
"errors"
"fmt"
"net/http"
"sync"
"time"
@@ -22,6 +21,7 @@ import (
admin_handler "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/handler"
admin_view "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing/view"
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/api/ui/login"
auth_es "github.com/zitadel/zitadel/internal/auth/repository/eventsourcing"
@@ -34,7 +34,6 @@ import (
"github.com/zitadel/zitadel/internal/config/systemdefaults"
crypto_db "github.com/zitadel/zitadel/internal/crypto/database"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
@@ -109,6 +108,7 @@ type ProjectionsConfig struct {
WebAuthNName string
DefaultInstance command.InstanceSetup
AssetStorage static_config.AssetStorageConfig
HTTPClient http.ClientConfig
}
func migrateProjectionsFlags(cmd *cobra.Command) {
@@ -187,6 +187,9 @@ func projections(
DisplayName: config.WebAuthNName,
ExternalSecure: config.ExternalSecure,
}
httpClient := config.HTTPClient.NewClient()
commands, err := command.StartCommands(ctx,
es,
cacheConnectors,
@@ -206,7 +209,7 @@ func projections(
keys.SAML,
keys.Target,
keys.OIDC,
&http.Client{},
httpClient,
func(ctx context.Context, permission, orgID, resourceID string) (err error) {
return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID)
},
@@ -216,7 +219,7 @@ func projections(
config.OIDC.DefaultRefreshTokenIdleExpiration,
config.DefaultInstance.SecretGenerators,
config.Login.DefaultPaths,
[]denylist.AddressChecker{},
config.HTTPClient.DenyList,
)
logging.OnError(ctx, err).Fatal("unable to start commands")
@@ -246,6 +249,7 @@ func projections(
keys.SMTP,
keys.SMS,
nil,
httpClient,
)
config.Auth.Spooler.Client = client
+5 -2
View File
@@ -3,6 +3,7 @@ package setup
import (
"context"
"fmt"
"net/http"
"os"
"strings"
@@ -47,6 +48,8 @@ type FirstInstance struct {
externalPort uint16
domain string
defaultPaths *login.DefaultPaths
httpClient *http.Client
denylist []denylist.AddressChecker
}
func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error {
@@ -89,7 +92,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
nil,
nil,
oidcEncryption,
nil,
mig.httpClient,
nil,
nil,
0,
@@ -97,7 +100,7 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error
0,
nil,
mig.defaultPaths,
[]denylist.AddressChecker{},
mig.denylist,
)
if err != nil {
return err
+4
View File
@@ -19,6 +19,7 @@ import (
"github.com/zitadel/zitadel/cmd/hooks"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/cache/connector"
@@ -64,6 +65,7 @@ type Config struct {
WebAuthNName string
Telemetry *handlers.TelemetryPusherConfig
SystemAPIUsers map[string]*authz.SystemAPIUser
HTTPClient http.ClientConfig
}
type InitProjections struct {
@@ -114,6 +116,8 @@ func NewConfig(cmd *cobra.Command, v *viper.Viper) (*Config, instrumentation.Shu
id.Configure(config.Machine)
config.HTTPClient.MergeDeprecatedDenylists(nil, config.Executions.DenyList)
// Copy the global role permissions mappings to the instance until we allow instance-level configuration over the API.
config.DefaultInstance.RolePermissionMappings = config.InternalAuthZ.RolePermissionMappings
+1 -2
View File
@@ -7,7 +7,6 @@ import (
"github.com/zitadel/zitadel/internal/cache/connector"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/eventstore"
)
@@ -61,7 +60,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context, _ eventstore.Event
0,
nil,
&login.DefaultPaths{},
[]denylist.AddressChecker{},
nil,
)
if err != nil {
+9 -3
View File
@@ -170,6 +170,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
MaxRetries: config.Eventstore.MaxRetries,
}))
httpClient := config.HTTPClient.NewClient()
steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient.DB}
steps.s2AssetsTable = &AssetTable{dbClient: dbClient.DB}
@@ -187,6 +189,8 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.FirstInstance.externalSecure = config.ExternalSecure
steps.FirstInstance.externalPort = config.ExternalPort
steps.FirstInstance.defaultPaths = config.Login.DefaultPaths
steps.FirstInstance.httpClient = httpClient
steps.FirstInstance.denylist = config.HTTPClient.DenyList
steps.s5LastFailed = &LastFailed{dbClient: dbClient.DB}
steps.s6OwnerRemoveColumns = &OwnerRemoveColumns{dbClient: dbClient.DB}
@@ -315,7 +319,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
}
}
commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config)
commands, _, _, _ := startCommandsQueries(ctx, eventstoreClient, eventstoreV4, dbClient, masterKey, config, httpClient)
steps.s59SetupWebkeys = &SetupWebkeys{eventstore: eventstoreClient, commands: commands}
repeatableSteps := []migration.RepeatableMigration{
@@ -454,6 +458,7 @@ func startCommandsQueries(
dbClient *database.DB,
masterKey string,
config *Config,
httpClient *http.Client,
) (
*command.Commands,
*query.Queries,
@@ -572,7 +577,7 @@ func startCommandsQueries(
keys.SAML,
keys.Target,
keys.OIDC,
&http.Client{},
httpClient,
permissionCheck,
sessionTokenVerifier,
config.OIDC.DefaultAccessTokenLifetime,
@@ -580,7 +585,7 @@ func startCommandsQueries(
config.OIDC.DefaultRefreshTokenIdleExpiration,
config.DefaultInstance.SecretGenerators,
config.Login.DefaultPaths,
config.Executions.DenyList,
config.HTTPClient.DenyList,
)
logging.OnError(ctx, err).Fatal("unable to start commands")
@@ -610,6 +615,7 @@ func startCommandsQueries(
keys.SMTP,
keys.SMS,
q,
httpClient,
)
return commands, queries, adminView, authView
+6 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/actions"
admin_es "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/api/saml"
@@ -87,6 +88,7 @@ type Config struct {
Quotas *QuotasConfig
Telemetry *handlers.TelemetryPusherConfig
ServicePing *serviceping.Config
HTTPClient *http.ClientConfig
}
type QuotasConfig struct {
@@ -121,9 +123,12 @@ func NewConfig(cmd *cobra.Command, v *viper.Viper) (*Config, instrumentation.Shu
}
id.Configure(config.Machine)
var actionsDenylist []denylist.AddressChecker
if config.Actions != nil {
actions.SetHTTPConfig(&config.Actions.HTTP)
actionsDenylist = config.Actions.HTTP.DenyList
}
config.HTTPClient.MergeDeprecatedDenylists(actionsDenylist, config.Executions.DenyList)
err = config.SystemDefaults.Validate()
if err != nil {
+27 -4
View File
@@ -272,6 +272,8 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
new_domain.SetWebAuthNConfig(webAuthNConfig)
httpClient := config.HTTPClient.NewClient()
commands, err := command.StartCommands(ctx,
eventstoreClient,
cacheConnectors,
@@ -291,7 +293,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
keys.SAML,
keys.Target,
keys.OIDC,
&http.Client{},
httpClient,
permissionCheck,
sessionTokenVerifier,
config.OIDC.DefaultAccessTokenLifetime,
@@ -299,7 +301,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
config.OIDC.DefaultRefreshTokenIdleExpiration,
config.DefaultInstance.SecretGenerators,
config.Login.DefaultPaths,
config.Executions.DenyList,
config.HTTPClient.DenyList,
)
if err != nil {
return fmt.Errorf("cannot start commands: %w", err)
@@ -345,12 +347,14 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
keys.SMTP,
keys.SMS,
q,
httpClient,
)
notification.Start(ctx)
execution.Register(
ctx,
config.Executions,
httpClient,
q,
keys.Target,
queries.GetActiveSigningWebKey,
@@ -390,6 +394,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
keys,
permissionCheck,
cacheConnectors,
httpClient,
)
if err != nil {
return err
@@ -435,6 +440,7 @@ func startAPIs(
keys *encryption.EncryptionKeys,
permissionCheck domain.PermissionCheck,
cacheConnectors connector.Connectors,
httpClient *http.Client,
) (*api.API, error) {
repo := struct {
authz_repo.Repository
@@ -492,7 +498,7 @@ func startAPIs(
keys.Target,
translator,
config.Instrumentation.Trace.TrustRemoteSpans,
config.Executions.DenyList,
httpClient,
)
if err != nil {
return nil, fmt.Errorf("error creating api %w", err)
@@ -675,13 +681,29 @@ func startAPIs(
config.Log.Slog(),
config.SystemDefaults.SecretHasher,
federatedLogoutsCache,
httpClient,
)
if err != nil {
return nil, fmt.Errorf("unable to start oidc provider: %w", err)
}
apis.RegisterHandlerPrefixes(oidcServer, oidcPrefixes...)
samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, keys.Target, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, limitingAccessInterceptor)
samlProvider, err := saml.NewProvider(
config.SAML,
config.ExternalSecure,
commands,
queries,
authRepo,
keys.OIDC,
keys.SAML,
keys.Target,
eventstore,
dbClient,
instanceInterceptor.Handler,
userAgentInterceptor,
limitingAccessInterceptor,
httpClient,
)
if err != nil {
return nil, fmt.Errorf("unable to start saml provider: %w", err)
}
@@ -726,6 +748,7 @@ func startAPIs(
keys.CSRFCookieKey,
cacheConnectors,
federatedLogoutsCache,
httpClient,
)
if err != nil {
return nil, fmt.Errorf("unable to start login: %w", err)
+2 -20
View File
@@ -5,22 +5,19 @@ import (
"context"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/dop251/goja"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/zerrors"
)
func WithHTTP(ctx context.Context) Option {
func WithHTTP(ctx context.Context, client *http.Client) Option {
return func(c *runConfig) {
c.modules["zitadel/http"] = func(runtime *goja.Runtime, module *goja.Object) {
requireHTTP(ctx, &http.Client{Transport: &transport{lookup: net.LookupIP}}, runtime, module)
requireHTTP(ctx, client, runtime, module)
}
}
}
@@ -170,18 +167,3 @@ func parseHeaders(headers *goja.Object) http.Header {
}
return h
}
type transport struct {
lookup func(string) ([]net.IP, error)
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
if httpConfig == nil || len(httpConfig.DenyList) == 0 {
return http.DefaultTransport.RoundTrip(req)
}
if err := denylist.IsHostBlocked(httpConfig.DenyList, req.URL, t.lookup); err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "ACTIO-N72d0", "host is denied")
}
return http.DefaultTransport.RoundTrip(req)
}
-6
View File
@@ -8,12 +8,6 @@ import (
"github.com/zitadel/zitadel/internal/denylist"
)
func SetHTTPConfig(config *HTTPConfig) {
httpConfig = config
}
var httpConfig *HTTPConfig
type HTTPConfig struct {
DenyList []denylist.AddressChecker
}
+5 -6
View File
@@ -26,7 +26,6 @@ import (
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
@@ -58,7 +57,7 @@ type API struct {
targetEncryptionAlgorithm crypto.EncryptionAlgorithm
translator *i18n.Translator
connectOTELInterceptor *otelconnect.Interceptor
actionV2DenyList []denylist.AddressChecker
httpClient *http.Client
}
func (a *API) ListGrpcServices() []string {
@@ -113,7 +112,7 @@ func New(
targetEncryptionAlgorithm crypto.EncryptionAlgorithm,
translator *i18n.Translator,
trustRemoteSpans bool,
deniedIPList []denylist.AddressChecker,
httpClient *http.Client,
) (_ *API, err error) {
api := &API{
port: port,
@@ -129,10 +128,10 @@ func New(
connectServices: make(map[string][]string),
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
translator: translator,
actionV2DenyList: deniedIPList,
httpClient: httpClient,
}
api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService(), targetEncryptionAlgorithm, api.translator, deniedIPList)
api.grpcServer = server.CreateServer(api.verifier, systemAuthz, authZ, queries, externalDomain, tlsConfig, accessInterceptor.AccessService(), targetEncryptionAlgorithm, api.translator, httpClient)
api.grpcGateway, err = server.CreateGateway(ctx, port, hostHeaders, accessInterceptor, tlsConfig)
if err != nil {
return nil, err
@@ -224,7 +223,7 @@ func (a *API) registerConnectServer(service server.ConnectServer) {
connect_middleware.AuthorizationInterceptor(a.verifier, a.systemAuthZ, a.authConfig),
connect_middleware.TranslationHandler(),
connect_middleware.QuotaExhaustedInterceptor(a.accessInterceptor.AccessService(), system_pb.SystemService_ServiceDesc.ServiceName),
connect_middleware.ExecutionHandler(a.targetEncryptionAlgorithm, a.queries.GetActiveSigningWebKey, a.actionV2DenyList),
connect_middleware.ExecutionHandler(a.targetEncryptionAlgorithm, a.queries.GetActiveSigningWebKey, a.httpClient),
connect_middleware.ValidationHandler(),
connect_middleware.ServiceHandler(),
connect_middleware.ActivityInterceptor(),
@@ -13,7 +13,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/execution"
target_domain "github.com/zitadel/zitadel/internal/execution/target"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
@@ -27,12 +26,12 @@ var headersToForward = map[string]bool{
strings.ToLower(http_utils.Origin): true,
}
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) connect.UnaryInterceptorFunc {
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) connect.UnaryInterceptorFunc {
return func(handler connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (_ connect.AnyResponse, err error) {
requestTargets := execution.QueryExecutionTargetsForRequest(ctx, req.Spec().Procedure)
handledReq, err := executeTargetsForRequest(ctx, requestTargets, req.Spec().Procedure, req, alg, activeSigningKey, deniedIPList)
handledReq, err := executeTargetsForRequest(ctx, requestTargets, req.Spec().Procedure, req, alg, activeSigningKey, httpClient)
if err != nil {
return nil, err
}
@@ -43,12 +42,12 @@ func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution
}
responseTargets := execution.QueryExecutionTargetsForResponse(ctx, req.Spec().Procedure)
return executeTargetsForResponse(ctx, responseTargets, req.Spec().Procedure, handledReq, response, alg, activeSigningKey, deniedIPList)
return executeTargetsForResponse(ctx, responseTargets, req.Spec().Procedure, handledReq, response, alg, activeSigningKey, httpClient)
}
}
}
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ connect.AnyRequest, err error) {
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ connect.AnyRequest, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -68,14 +67,14 @@ func executeTargetsForRequest(ctx context.Context, targets []target_domain.Targe
Headers: SetRequestHeaders(req.Header()),
}
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
if err != nil {
return nil, err
}
return req, nil
}
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, resp connect.AnyResponse, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ connect.AnyResponse, err error) {
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req connect.AnyRequest, resp connect.AnyResponse, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ connect.AnyResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -96,7 +95,7 @@ func executeTargetsForResponse(ctx context.Context, targets []target_domain.Targ
Headers: SetRequestHeaders(req.Header()),
}
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
_, err = execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
if err != nil {
return nil, err
}
@@ -75,9 +75,7 @@ func newMockContextInfoResponse(fullMethod, request, response string) *ContextIn
}
func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
deniedLocalhost, err := denylist.NewHostChecker("127.0.0.1")
require.NoError(t, err)
deniedIPs := []denylist.AddressChecker{deniedLocalhost}
deniedIPs := []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.1")}
type target struct {
reqBody execution.ContextInfo
@@ -668,7 +666,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
tt.args.req,
nil,
tt.args.getActiveSigningWebKey,
tt.args.deniedIPs,
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
)
if tt.res.wantErr {
@@ -774,9 +772,7 @@ func validateJWEPayload(t *testing.T) func(expected, sent []byte) bool {
}
func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
deniedLocalhost, err := denylist.NewHostChecker("127.0.0.1")
require.NoError(t, err)
deniedIPs := []denylist.AddressChecker{deniedLocalhost}
deniedIPs := []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.1")}
type target struct {
reqBody execution.ContextInfo
sleep time.Duration
@@ -942,7 +938,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
tt.args.resp,
nil,
mockGetActiveSigningWebKey(),
tt.args.deniedIPs,
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
)
if tt.res.wantErr {
@@ -13,17 +13,16 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server/connect_middleware"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/execution"
target_domain "github.com/zitadel/zitadel/internal/execution/target"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) grpc.UnaryServerInterceptor {
func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
requestTargets := execution.QueryExecutionTargetsForRequest(ctx, info.FullMethod)
// call targets otherwise return req
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req, alg, activeSigningKey, deniedIPList)
handledReq, err := executeTargetsForRequest(ctx, requestTargets, info.FullMethod, req, alg, activeSigningKey, httpClient)
if err != nil {
return nil, err
}
@@ -34,11 +33,11 @@ func ExecutionHandler(alg crypto.EncryptionAlgorithm, activeSigningKey execution
}
responseTargets := execution.QueryExecutionTargetsForResponse(ctx, info.FullMethod)
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response, alg, activeSigningKey, deniedIPList)
return executeTargetsForResponse(ctx, responseTargets, info.FullMethod, handledReq, response, alg, activeSigningKey, httpClient)
}
}
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req any, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ interface{}, err error) {
func executeTargetsForRequest(ctx context.Context, targets []target_domain.Target, fullMethod string, req any, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -59,10 +58,10 @@ func executeTargetsForRequest(ctx context.Context, targets []target_domain.Targe
Headers: connect_middleware.SetRequestHeaders(md),
}
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
}
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req, resp interface{}, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, deniedIPList []denylist.AddressChecker) (_ interface{}, err error) {
func executeTargetsForResponse(ctx context.Context, targets []target_domain.Target, fullMethod string, req, resp interface{}, alg crypto.EncryptionAlgorithm, activeSigningKey execution.GetActiveSigningWebKey, httpClient *http.Client) (_ interface{}, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -84,7 +83,7 @@ func executeTargetsForResponse(ctx context.Context, targets []target_domain.Targ
Headers: connect_middleware.SetRequestHeaders(md),
}
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, deniedIPList)
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, httpClient)
}
var _ execution.ContextInfo = &ContextInfoRequest{}
@@ -64,9 +64,7 @@ func newMockContextInfoResponse(fullMethod, request, response string) *ContextIn
}
func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
deniedLocalhost, err := denylist.NewHostChecker("127.0.0.1")
require.NoError(t, err)
deniedIPs := []denylist.AddressChecker{deniedLocalhost}
deniedIPs := []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.1")}
type target struct {
reqBody execution.ContextInfo
@@ -656,7 +654,7 @@ func Test_executeTargetsForGRPCFullMethod_request(t *testing.T) {
tt.args.req,
nil,
tt.args.getActiveSigningWebKey,
tt.args.deniedIPs,
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
)
if tt.res.wantErr {
@@ -762,9 +760,7 @@ func validateJWEPayload(t *testing.T) func(expected, sent []byte) bool {
}
func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
deniedLocalhost, err := denylist.NewHostChecker("127.0.0.1")
require.NoError(t, err)
deniedIPs := []denylist.AddressChecker{deniedLocalhost}
deniedIPs := []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.1")}
type target struct {
reqBody execution.ContextInfo
@@ -930,7 +926,7 @@ func Test_executeTargetsForGRPCFullMethod_response(t *testing.T) {
tt.args.resp,
nil,
mockGetActiveSigningWebKey(),
tt.args.deniedIPs,
&http.Client{Transport: denylist.NewHTTPTransport(tt.args.deniedIPs)},
)
if tt.res.wantErr {
+2 -3
View File
@@ -16,7 +16,6 @@ import (
grpc_api "github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/i18n"
"github.com/zitadel/zitadel/internal/logstore"
"github.com/zitadel/zitadel/internal/logstore/record"
@@ -69,7 +68,7 @@ func CreateServer(
accessSvc *logstore.Service[*record.AccessLog],
targetEncAlg crypto.EncryptionAlgorithm,
translator *i18n.Translator,
deniedIPList []denylist.AddressChecker,
httpClient *http.Client,
) *grpc.Server {
metricTypes := []metrics.MetricType{metrics.MetricTypeTotalCount, metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode}
serverOptions := []grpc.ServerOption{
@@ -87,7 +86,7 @@ func CreateServer(
middleware.AuthorizationInterceptor(verifier, systemAuthz, authConfig),
middleware.TranslationHandler(),
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
middleware.ExecutionHandler(targetEncAlg, queries.GetActiveSigningWebKey, deniedIPList),
middleware.ExecutionHandler(targetEncAlg, queries.GetActiveSigningWebKey, httpClient),
middleware.ValidationHandler(),
middleware.ServiceHandler(),
middleware.ActivityInterceptor(),
+131
View File
@@ -0,0 +1,131 @@
package http
import (
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/zitadel/zitadel/internal/denylist"
)
type ClientConfig struct {
MaxBodySize int64
Timeout time.Duration
MaxRedirects int
AllowHTTPSDowngrade bool
DenyList []denylist.AddressChecker
}
// NewClient returns a new http.Client with the configured settings.
// The client is protected against DNS rebinding attacks, redirects, HTTPs downgrades, and response body size limits.
func (c *ClientConfig) NewClient() *http.Client {
baseTransport := denylist.NewHTTPTransport(c.DenyList)
protectedTransport := &MaxBytesRoundTripper{
Underlying: baseTransport,
MaxBytes: c.MaxBodySize,
}
return &http.Client{
Transport: protectedTransport,
Timeout: c.Timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= c.MaxRedirects {
return ErrTooManyRedirects
}
if !c.AllowHTTPSDowngrade && len(via) > 0 {
prev := via[len(via)-1]
if prev != nil && prev.URL != nil && req.URL != nil &&
strings.EqualFold(prev.URL.Scheme, "https") &&
!strings.EqualFold(req.URL.Scheme, "https") {
return ErrHTTPsDowngrade
}
}
return denylist.IsURLBlocked(c.DenyList, req.URL, nil)
},
}
}
// MergeDeprecatedDenylists merges the two deprecated (actions) denylists into the main denylist.
func (c *ClientConfig) MergeDeprecatedDenylists(actionsV1, actionsV2 []denylist.AddressChecker) {
c.DenyList = append(c.DenyList, actionsV1...)
c.DenyList = append(c.DenyList, actionsV2...)
}
var (
// ErrResponseTooLarge is returned when the response body exceeds the configured limit.
ErrResponseTooLarge = errors.New("response body exceeded maximum allowed size")
//ErrTooManyRedirects is returned when the number of redirects exceeds the configured limit.
ErrTooManyRedirects = errors.New("stopped after too many redirects")
// ErrHTTPsDowngrade is returned when the client attempts to downgrade to HTTP.
ErrHTTPsDowngrade = errors.New("redirect downgrade from https to http is not allowed")
)
// MaxBytesRoundTripper wraps an existing RoundTripper to protect against OOM.
type MaxBytesRoundTripper struct {
Underlying http.RoundTripper
MaxBytes int64
}
func (m *MaxBytesRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
transport := m.Underlying
if transport == nil {
transport = http.DefaultTransport
}
resp, err := transport.RoundTrip(req)
if err != nil {
return nil, err
}
if resp.ContentLength > m.MaxBytes {
resp.Body.Close()
return nil, fmt.Errorf("%w: Content-Length is %d (limit %d)", ErrResponseTooLarge, resp.ContentLength, m.MaxBytes)
}
resp.Body = &strictMaxBytesReader{
limitReader: io.LimitReader(resp.Body, m.MaxBytes+1), // We initialize with +1 to detect overflows during Read
closer: resp.Body,
limit: m.MaxBytes,
}
return resp, nil
}
var _ http.RoundTripper = (*MaxBytesRoundTripper)(nil)
// strictMaxBytesReader enforces a hard limit and returns an explicit error if exceeded.
type strictMaxBytesReader struct {
limitReader io.Reader
closer io.Closer
limit int64
bytesRead int64
}
func (s *strictMaxBytesReader) Read(p []byte) (int, error) {
n, err := s.limitReader.Read(p)
s.bytesRead += int64(n)
if s.bytesRead > s.limit {
// Because LimitReader stops at limit+1, excess is mathematically guaranteed to be 1
safeN := n - 1
if safeN < 0 {
safeN = 0
}
// This instantly slices off that single offending byte from the view of the caller
p = p[:safeN]
return safeN, ErrResponseTooLarge
}
return n, err
}
func (s *strictMaxBytesReader) Close() error {
return s.closer.Close()
}
var _ io.ReadCloser = (*strictMaxBytesReader)(nil)
+144
View File
@@ -0,0 +1,144 @@
package http
import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/denylist"
)
func TestClientConfig_NewClient_Allowed(t *testing.T) {
t.Parallel()
// Spin up a simple local test server to serve a dummy response
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello world"))
}))
defer server.Close()
// An empty denylist permits all safe destinations
cfg := &ClientConfig{
MaxBodySize: 1024,
Timeout: 2 * time.Second,
MaxRedirects: 3,
DenyList: []denylist.AddressChecker{},
}
client := cfg.NewClient()
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
assert.NoError(t, err)
resp, err := client.Do(req)
assert.NoError(t, err)
if resp != nil {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, "hello world", string(body))
}
}
func TestClientConfig_Redirect_HTTPSDowngradeBlocked(t *testing.T) {
t.Parallel()
cfg := &ClientConfig{
MaxBodySize: 1024,
Timeout: 2 * time.Second,
MaxRedirects: 3,
AllowHTTPSDowngrade: false, // Explicitly disallow downgrades
DenyList: []denylist.AddressChecker{},
}
client := cfg.NewClient()
// Mock a historical request chain moving from a secure schema to an unencrypted target
viaReq, _ := http.NewRequest(http.MethodGet, "https://secure-identity.com/oauth", nil)
currentReq, _ := http.NewRequest(http.MethodGet, "http://insecure-identity.com/callback", nil)
err := client.CheckRedirect(currentReq, []*http.Request{viaReq})
assert.ErrorIs(t, err, ErrHTTPsDowngrade)
}
func TestMaxBytesRoundTripper_EnforcesCap(t *testing.T) {
t.Parallel()
// Spin up a local server delivering an exact 10-byte payload response string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("0123456789"))
}))
defer server.Close()
tests := []struct {
name string
maxBytes int64
expectError error
}{
{
name: "body cleanly within threshold limit",
maxBytes: 15,
expectError: nil,
},
{
name: "body matches boundary size exactly",
maxBytes: 10,
expectError: nil,
},
{
name: "body exceeds allowable limit",
maxBytes: 5,
expectError: ErrResponseTooLarge,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &ClientConfig{
MaxBodySize: tt.maxBytes,
Timeout: 2 * time.Second,
MaxRedirects: 3,
DenyList: []denylist.AddressChecker{},
}
client := cfg.NewClient()
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, server.URL, nil)
resp, err := client.Do(req)
if tt.expectError != nil {
// Depending on optimization, errors can trip directly on call or during chunk reading
if err != nil {
assert.ErrorIs(t, err, tt.expectError)
return
}
defer resp.Body.Close()
_, readErr := io.ReadAll(resp.Body)
assert.ErrorIs(t, readErr, tt.expectError)
} else {
assert.NoError(t, err)
defer resp.Body.Close()
body, readErr := io.ReadAll(resp.Body)
assert.NoError(t, readErr)
assert.Equal(t, 10, len(body))
}
})
}
}
func TestClientConfig_MergeDeprecatedDenylists(t *testing.T) {
t.Parallel()
cfg := &ClientConfig{DenyList: []denylist.AddressChecker{denylist.NewHostChecker("1.1.1.1")}}
v1 := []denylist.AddressChecker{denylist.NewHostChecker("2.2.2.2")}
v2 := []denylist.AddressChecker{denylist.NewHostChecker("3.3.3.3")}
cfg.MergeDeprecatedDenylists(v1, v2)
assert.Len(t, cfg.DenyList, 3)
}
+2
View File
@@ -127,6 +127,7 @@ func NewServer(
fallbackLogger *slog.Logger,
hashConfig crypto.HashConfig,
federatedLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
httpClient *http.Client,
) (*Server, error) {
opConfig, err := createOPConfig(config, defaultLogoutRedirectURI, cryptoKey)
if err != nil {
@@ -189,6 +190,7 @@ func NewServer(
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
opCrypto: alg,
assetAPIPrefix: assets.AssetAPI(),
httpClient: httpClient,
}
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
server.Handler = op.RegisterLegacyServer(server,
+1
View File
@@ -44,6 +44,7 @@ type Server struct {
opCrypto op.Crypto
assetAPIPrefix func(ctx context.Context) string
httpClient *http.Client
}
func endpoints(endpointConfig *EndpointConfig) op.Endpoints {
+2 -2
View File
@@ -424,7 +424,7 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
apiFields,
action.Script,
action.Name,
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx, s.httpClient), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
@@ -461,7 +461,7 @@ func (s *Server) userinfoFlows(ctx context.Context, qu *query.OIDCUserInfo, user
UserGrants: qu.UserGrants,
}
resp, err := execution.CallTargets(ctx, executionTargets, info, s.targetEncryptionAlgorithm, s.query.GetActiveSigningWebKey, s.command.ActionsV2DenyList)
resp, err := execution.CallTargets(ctx, executionTargets, info, s.targetEncryptionAlgorithm, s.query.GetActiveSigningWebKey, s.httpClient)
if err != nil {
return err
}
+4
View File
@@ -48,6 +48,7 @@ func NewProvider(
instanceHandler,
userAgentCookie func(http.Handler) http.Handler,
accessHandler *middleware.AccessInterceptor,
httpClient *http.Client,
) (*Provider, error) {
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
@@ -63,6 +64,7 @@ func NewProvider(
fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
conf.DefaultLoginURLV2,
ContextToIssuer,
httpClient,
)
if err != nil {
return nil, err
@@ -125,6 +127,7 @@ func newStorage(
defaultLoginURL string,
defaultLoginURLV2 string,
contextToIssuer func(context.Context) string,
actionHttpClient *http.Client,
) (*Storage, error) {
return &Storage{
encAlg: encAlg,
@@ -138,6 +141,7 @@ func newStorage(
defaultLoginURL: defaultLoginURL,
defaultLoginURLv2: defaultLoginURLV2,
contextToIssuer: contextToIssuer,
httpClient: actionHttpClient,
}, nil
}
+4 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
@@ -66,6 +67,7 @@ type Storage struct {
defaultLoginURL string
defaultLoginURLv2 string
contextToIssuer func(context.Context) string
httpClient *http.Client
}
func (p *Storage) GetEntityByID(ctx context.Context, entityID string) (*serviceprovider.ServiceProvider, error) {
@@ -380,7 +382,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
apiFields,
action.Script,
action.Name,
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx))...,
append(actions.ActionToOptions(action), actions.WithHTTP(actionCtx, p.httpClient))...,
)
cancel()
if err != nil {
@@ -401,7 +403,7 @@ func (p *Storage) getCustomAttributes(ctx context.Context, user *query.User, use
UserGrants: userGrants.UserGrants,
}
resp, err := execution.CallTargets(ctx, executionTargets, info, p.targetEncAlg, p.query.GetActiveSigningWebKey, p.command.ActionsV2DenyList)
resp, err := execution.CallTargets(ctx, executionTargets, info, p.targetEncAlg, p.query.GetActiveSigningWebKey, p.httpClient)
if err != nil {
return nil, err
}
+4 -4
View File
@@ -152,7 +152,7 @@ func (l *Login) runPostExternalAuthenticationActions(
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
@@ -226,7 +226,7 @@ func (l *Login) runPostInternalAuthenticationActions(
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
@@ -346,7 +346,7 @@ func (l *Login) runPreCreationActions(
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
@@ -423,7 +423,7 @@ func (l *Login) runPostCreationActions(
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx, l.httpClient), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
@@ -1053,6 +1053,7 @@ func (l *Login) googleProvider(ctx context.Context, identityProvider *query.IDPT
secret,
l.baseURL(ctx)+EndpointExternalLoginCallback,
identityProvider.GoogleIDPTemplate.Scopes,
l.httpClient,
)
}
@@ -1079,6 +1080,7 @@ func (l *Login) oidcProvider(ctx context.Context, identityProvider *query.IDPTem
l.baseURL(ctx)+EndpointExternalLoginCallback,
identityProvider.OIDCIDPTemplate.Scopes,
openid.DefaultMapper,
l.httpClient,
opts...,
)
}
@@ -1092,6 +1094,7 @@ func (l *Login) jwtProvider(identityProvider *query.IDPTemplate) (*jwt.Provider,
identityProvider.JWTIDPTemplate.HeaderName,
identityProvider.JWTIDPTemplate.Audience,
l.idpConfigAlg,
l.httpClient,
)
}
@@ -1123,6 +1126,7 @@ func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTe
func() idp.User {
return oauth.NewUserMapper(identityProvider.OAuthIDPTemplate.IDAttribute)
},
l.httpClient,
opts...,
)
}
@@ -1176,6 +1180,7 @@ func (l *Login) samlProvider(ctx context.Context, identityProvider *query.IDPTem
identityProvider.Metadata,
identityProvider.Certificate,
key,
l.httpClient,
opts...,
)
}
@@ -1198,6 +1203,7 @@ func (l *Login) azureProvider(ctx context.Context, identityProvider *query.IDPTe
secret,
l.baseURL(ctx)+EndpointExternalLoginCallback,
identityProvider.AzureADIDPTemplate.Scopes,
l.httpClient,
opts...,
)
}
@@ -1212,6 +1218,7 @@ func (l *Login) githubProvider(ctx context.Context, identityProvider *query.IDPT
secret,
l.baseURL(ctx)+EndpointExternalLoginCallback,
identityProvider.GitHubIDPTemplate.Scopes,
l.httpClient,
)
}
@@ -1229,6 +1236,7 @@ func (l *Login) githubEnterpriseProvider(ctx context.Context, identityProvider *
identityProvider.GitHubEnterpriseIDPTemplate.TokenEndpoint,
identityProvider.GitHubEnterpriseIDPTemplate.UserEndpoint,
identityProvider.GitHubIDPTemplate.Scopes,
l.httpClient,
)
}
@@ -1242,6 +1250,7 @@ func (l *Login) gitlabProvider(ctx context.Context, identityProvider *query.IDPT
secret,
l.baseURL(ctx)+EndpointExternalLoginCallback,
identityProvider.GitLabIDPTemplate.Scopes,
l.httpClient,
)
}
@@ -1257,6 +1266,7 @@ func (l *Login) gitlabSelfHostedProvider(ctx context.Context, identityProvider *
secret,
l.baseURL(ctx)+EndpointExternalLoginCallback,
identityProvider.GitLabSelfHostedIDPTemplate.Scopes,
l.httpClient,
)
}
@@ -1272,6 +1282,7 @@ func (l *Login) appleProvider(ctx context.Context, identityProvider *query.IDPTe
l.baseURL(ctx)+EndpointExternalLoginCallbackFormPost,
privateKey,
identityProvider.AppleIDPTemplate.Scopes,
l.httpClient,
)
}
+3
View File
@@ -44,6 +44,7 @@ type Login struct {
idpConfigAlg crypto.EncryptionAlgorithm
userCodeAlg crypto.EncryptionAlgorithm
caches *Caches
httpClient *http.Client
}
type Config struct {
@@ -198,6 +199,7 @@ func CreateLogin(
csrfCookieKey []byte,
cacheConnectors connector.Connectors,
federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
httpClient *http.Client,
) (*Login, error) {
login := &Login{
oidcAuthCallbackURL: oidcAuthCallbackURL,
@@ -210,6 +212,7 @@ func CreateLogin(
authRepo: authRepo,
idpConfigAlg: idpConfigAlg,
userCodeAlg: userCodeAlg,
httpClient: httpClient,
}
csrfInterceptor := createCSRFInterceptor(config.CSRFCookieName, csrfCookieKey, externalSecure, login.csrfErrorHandler())
cacheInterceptor := createCacheInterceptor(config.Cache.MaxAge, config.Cache.SharedMaxAge, assetCache)
+4 -4
View File
@@ -41,7 +41,7 @@ func (a *AddTarget) isValid(inputDenyList []denylist.AddressChecker, lookupFunc
return zerrors.ThrowInvalidArgument(err, "COMMAND-1r2k6qo6wg", "Errors.Target.InvalidURL")
}
if err := denylist.IsHostBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
if err := denylist.IsURLBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-NcJUKo", "Errors.Target.DeniedURL")
}
@@ -53,7 +53,7 @@ func (c *Commands) AddTarget(ctx context.Context, add *AddTarget, resourceOwner
return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-brml926e2d", "Errors.IDMissing")
}
if err := add.isValid(c.ActionsV2DenyList, c.IPLookupFunction); err != nil {
if err := add.isValid(c.denyList, c.ipLookupFunction); err != nil {
return time.Time{}, err
}
@@ -124,7 +124,7 @@ func (a *ChangeTarget) isValid(inputDenyList []denylist.AddressChecker, lookupFu
if err != nil || *a.Endpoint == "" {
return zerrors.ThrowInvalidArgument(err, "COMMAND-jsbaera7b6", "Errors.Target.InvalidURL")
}
if err := denylist.IsHostBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
if err := denylist.IsURLBlocked(inputDenyList, parsedURL, lookupFunc); err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-jKbbu2", "Errors.Target.DeniedURL")
}
}
@@ -136,7 +136,7 @@ func (c *Commands) ChangeTarget(ctx context.Context, change *ChangeTarget, resou
if resourceOwner == "" {
return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-zqibgg0wwh", "Errors.IDMissing")
}
if err := change.isValid(c.ActionsV2DenyList, c.IPLookupFunction); err != nil {
if err := change.isValid(c.denyList, c.ipLookupFunction); err != nil {
return time.Time{}, err
}
existing, err := c.getTargetWriteModelByID(ctx, change.AggregateID, resourceOwner)
+5 -8
View File
@@ -8,7 +8,6 @@ import (
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
@@ -23,8 +22,7 @@ import (
)
func TestCommands_AddTarget(t *testing.T) {
localhostAddrChecker, err := denylist.NewHostChecker("localhost")
require.NoError(t, err)
localhostAddrChecker := denylist.NewHostChecker("localhost")
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
@@ -305,7 +303,7 @@ func TestCommands_AddTarget(t *testing.T) {
idGenerator: tt.fields.idGenerator,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
ActionsV2DenyList: tt.fields.denyList,
denyList: tt.fields.denyList,
}
_, err := c.AddTarget(tt.args.ctx, tt.args.add, tt.args.resourceOwner)
if tt.res.err == nil {
@@ -322,8 +320,7 @@ func TestCommands_AddTarget(t *testing.T) {
}
func TestCommands_ChangeTarget(t *testing.T) {
localhostAddrChecker, err := denylist.NewHostChecker("localhost")
require.NoError(t, err)
localhostAddrChecker := denylist.NewHostChecker("localhost")
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
@@ -647,8 +644,8 @@ func TestCommands_ChangeTarget(t *testing.T) {
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
ActionsV2DenyList: tt.fields.denyList,
IPLookupFunction: tt.fields.lookupFunc,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.lookupFunc,
}
_, err := c.ChangeTarget(tt.args.ctx, tt.args.change, tt.args.resourceOwner)
if tt.res.err == nil {
+10 -9
View File
@@ -106,9 +106,9 @@ type Commands struct {
// so the query and cache overhead can be completely eliminated.
milestonesCompleted sync.Map
loginPaths LoginPaths
ActionsV2DenyList []denylist.AddressChecker
IPLookupFunction internal_net.IPLookupFunc
loginPaths LoginPaths
ipLookupFunction internal_net.IPLookupFunc
denyList []denylist.AddressChecker
}
//go:generate mockgen -package command -destination ./mock_login_paths.go . LoginPaths
@@ -138,7 +138,7 @@ func StartCommands(
defaultAccessTokenLifetime, defaultRefreshTokenLifetime, defaultRefreshTokenIdleLifetime time.Duration,
defaultSecretGenerators *SecretGenerators,
loginPaths LoginPaths,
actionsDeniedHostList []denylist.AddressChecker,
denyList []denylist.AddressChecker,
) (repo *Commands, err error) {
if externalDomain == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified")
@@ -162,6 +162,7 @@ func StartCommands(
if err != nil {
return nil, fmt.Errorf("caches: %w", err)
}
ipLookupFunction := net.LookupIP
repo = &Commands{
eventstore: es,
static: staticStore,
@@ -228,11 +229,11 @@ func StartCommands(
WithHyphen: defaults.Multifactors.RecoveryCodes.WithHyphen,
},
},
GenerateDomain: domain.NewGeneratedInstanceDomain,
caches: caches,
loginPaths: loginPaths,
ActionsV2DenyList: actionsDeniedHostList,
IPLookupFunction: net.LookupIP,
GenerateDomain: domain.NewGeneratedInstanceDomain,
caches: caches,
loginPaths: loginPaths,
ipLookupFunction: ipLookupFunction,
denyList: denyList,
}
if defaultSecretGenerators != nil && defaultSecretGenerators.ClientSecret != nil {
+2 -1
View File
@@ -95,11 +95,12 @@ func (c *Commands) GetProvider(ctx context.Context, idpID string, idpCallback st
return nil, err
}
if writeModel.IDPType != domain.IDPTypeSAML {
return writeModel.ToProvider(idpCallback, c.idpConfigEncryption)
return writeModel.ToProvider(idpCallback, c.idpConfigEncryption, c.httpClient)
}
return writeModel.ToSAMLProvider(
samlRootURL,
c.idpConfigEncryption,
c.httpClient,
func(ctx context.Context, intentID string) (*samlsp.TrackedRequest, error) {
intent, err := c.GetActiveIntent(ctx, intentID)
if err != nil {
+51 -18
View File
@@ -164,7 +164,7 @@ func (wm *OAuthIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -202,6 +202,7 @@ func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encry
func() providers.User {
return oauth.NewUserMapper(wm.IDAttribute)
},
httpClient,
opts...,
)
}
@@ -380,7 +381,7 @@ func (wm *OIDCIDPWriteModel) reduceOIDCConfigChangedEvent(e *idpconfig.OIDCConfi
}
}
func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -413,6 +414,7 @@ func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp
callbackURL,
wm.Scopes,
oidc.DefaultMapper,
httpClient,
opts...,
)
}
@@ -572,7 +574,7 @@ func (wm *JWTIDPWriteModel) reduceJWTConfigChangedEvent(e *idpconfig.JWTConfigCh
}
}
func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
opts := make([]jwt.ProviderOpts, 0)
if wm.IsCreationAllowed {
opts = append(opts, jwt.WithCreationAllowed())
@@ -594,6 +596,7 @@ func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encrypt
wm.HeaderName,
wm.Audience,
idpAlg,
httpClient,
opts...,
)
}
@@ -708,7 +711,7 @@ func (wm *AzureADIDPWriteModel) NewChanges(
}
return changes, nil
}
func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -742,6 +745,7 @@ func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Enc
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -836,7 +840,7 @@ func (wm *GitHubIDPWriteModel) NewChanges(
}
return changes, nil
}
func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -859,6 +863,7 @@ func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encr
secret,
callbackURL,
wm.Scopes,
httpClient,
oauthOpts...,
)
}
@@ -980,7 +985,7 @@ func (wm *GitHubEnterpriseIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -1007,6 +1012,7 @@ func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg c
wm.TokenEndpoint,
wm.UserEndpoint,
wm.Scopes,
httpClient,
oauthOpts...,
)
}
@@ -1102,7 +1108,7 @@ func (wm *GitLabIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -1125,6 +1131,7 @@ func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encr
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1228,7 +1235,7 @@ func (wm *GitLabSelfHostedIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
@@ -1253,6 +1260,7 @@ func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg c
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1350,7 +1358,7 @@ func (wm *GoogleIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state)
rp.DefaultErrorHandler(w, r, errorType, errorDesc, state)
@@ -1378,6 +1386,7 @@ func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encr
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1552,7 +1561,7 @@ func (wm *LDAPIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
password, err := crypto.DecryptString(wm.BindPassword, idpAlg)
if err != nil {
return nil, err
@@ -1731,7 +1740,7 @@ func (wm *AppleIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
privateKey, err := crypto.Decrypt(wm.PrivateKey, idpAlg)
if err != nil {
return nil, err
@@ -1756,6 +1765,7 @@ func (wm *AppleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encry
callbackURL,
privateKey,
wm.Scopes,
httpClient,
opts...,
)
}
@@ -1902,7 +1912,13 @@ func (wm *SAMLIDPWriteModel) NewChanges(
return changes, nil
}
func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) {
func (wm *SAMLIDPWriteModel) ToProvider(
callbackURL string,
idpAlg crypto.EncryptionAlgorithm,
httpClient *http.Client,
getRequest requesttracker.GetRequest,
addRequest requesttracker.AddRequest,
) (providers.Provider, error) {
key, err := crypto.Decrypt(wm.Key, idpAlg)
if err != nil {
return nil, err
@@ -1949,6 +1965,7 @@ func (wm *SAMLIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.Encryp
wm.Metadata,
wm.Certificate,
key,
httpClient,
opts...,
)
}
@@ -2202,13 +2219,13 @@ func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
type IDP interface {
eventstore.QueryReducer
ToProvider(string, crypto.EncryptionAlgorithm) (providers.Provider, error)
ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error)
GetProviderOptions() idp.Options
}
type SAMLIDP interface {
eventstore.QueryReducer
ToProvider(string, crypto.EncryptionAlgorithm, requesttracker.GetRequest, requesttracker.AddRequest) (providers.Provider, error)
ToProvider(string, crypto.EncryptionAlgorithm, *http.Client, requesttracker.GetRequest, requesttracker.AddRequest) (providers.Provider, error)
GetProviderOptions() idp.Options
}
@@ -2318,11 +2335,15 @@ func (wm *AllIDPWriteModel) AppendEvents(events ...eventstore.Event) {
wm.samlModel.AppendEvents(events...)
}
func (wm *AllIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
func (wm *AllIDPWriteModel) ToProvider(
callbackURL string,
idpAlg crypto.EncryptionAlgorithm,
httpClient *http.Client,
) (providers.Provider, error) {
if wm.model == nil {
return nil, zerrors.ThrowInternal(nil, "COMMAND-afvf0gc9sa", "ErrorsIDPConfig.NotExisting")
}
return wm.model.ToProvider(callbackURL, idpAlg)
return wm.model.ToProvider(callbackURL, idpAlg, httpClient)
}
func (wm *AllIDPWriteModel) GetProviderOptions() idp.Options {
@@ -2332,11 +2353,23 @@ func (wm *AllIDPWriteModel) GetProviderOptions() idp.Options {
return wm.samlModel.GetProviderOptions()
}
func (wm *AllIDPWriteModel) ToSAMLProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, getRequest requesttracker.GetRequest, addRequest requesttracker.AddRequest) (providers.Provider, error) {
func (wm *AllIDPWriteModel) ToSAMLProvider(
callbackURL string,
idpAlg crypto.EncryptionAlgorithm,
httpClient *http.Client,
getRequest requesttracker.GetRequest,
addRequest requesttracker.AddRequest,
) (providers.Provider, error) {
if wm.samlModel == nil {
return nil, zerrors.ThrowInternal(nil, "COMMAND-csi30hdscv", "ErrorsIDPConfig.NotExisting")
}
return wm.samlModel.ToProvider(callbackURL, idpAlg, getRequest, addRequest)
return wm.samlModel.ToProvider(
callbackURL,
idpAlg,
httpClient,
getRequest,
addRequest,
)
}
type ZitadelIDPWriteModel struct {
+2 -2
View File
@@ -377,7 +377,7 @@ func TestOAuthIDPWriteModel_ToProvider_WithPKCE(t *testing.T) {
UsePKCE: true,
}
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{})
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{}, http.DefaultClient)
require.NoError(t, err)
assertProviderUsesPKCE(t, provider)
@@ -417,7 +417,7 @@ func TestOIDCIDPWriteModel_ToProvider_WithPKCE(t *testing.T) {
UsePKCE: true,
}
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{})
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{}, http.DefaultClient)
require.NoError(t, err)
require.True(t, discoveryRequested.Load(), "expected OIDC discovery to be requested")
+28 -2
View File
@@ -2,6 +2,7 @@ package command
import (
"context"
"net/url"
"strings"
"time"
@@ -9,6 +10,7 @@ import (
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
project_repo "github.com/zitadel/zitadel/internal/repository/project"
@@ -207,6 +209,12 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
if err != nil {
return nil, err
}
backchannelLogoutURI, err := c.validateBackchannelLogoutURI(oidcApp)
if err != nil {
return nil, err
}
events = append(events, project_repo.NewOIDCConfigAddedEvent(ctx,
projectAgg,
gu.Value(oidcApp.OIDCVersion),
@@ -227,7 +235,7 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
gu.Value(oidcApp.ClockSkew),
trimStringSliceWhiteSpaces(oidcApp.AdditionalOrigins),
gu.Value(oidcApp.SkipNativeAppSuccessPage),
strings.TrimSpace(gu.Value(oidcApp.BackChannelLogoutURI)),
backchannelLogoutURI,
gu.Value(oidcApp.LoginVersion),
strings.TrimSpace(gu.Value(oidcApp.LoginBaseURI)),
))
@@ -252,6 +260,20 @@ func (c *Commands) addOIDCApplicationWithID(ctx context.Context, oidcApp *domain
return result, nil
}
func (c *Commands) validateBackchannelLogoutURI(oidcApp *domain.OIDCApp) (string, error) {
backchannelLogoutURL := strings.TrimSpace(gu.Value(oidcApp.BackChannelLogoutURI))
if backchannelLogoutURL != "" {
url, err := url.Parse(backchannelLogoutURL)
if err != nil {
return "", zerrors.ThrowInvalidArgument(err, "PROJECT-iZL2Ac", "Errors.Project.App.Invalid.BackchannelLogoutURL")
}
if err := denylist.IsURLBlocked(c.denyList, url, c.ipLookupFunction); err != nil {
return "", zerrors.ThrowInvalidArgument(err, "PROJECT-msNebo", "Errors.Project.App.Blocked.BackchannelLogoutURL")
}
}
return backchannelLogoutURL, nil
}
func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCApp, resourceOwner string) (*domain.OIDCApp, error) {
if !oidc.IsValid() || oidc.AppID == "" || oidc.AggregateID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-5m9fs", "Errors.Project.App.OIDCConfigInvalid")
@@ -277,7 +299,11 @@ func (c *Commands) UpdateOIDCApplication(ctx context.Context, oidc *domain.OIDCA
projectAgg := ProjectAggregateFromWriteModel(&existingOIDC.WriteModel)
var backChannelLogout, loginBaseURI *string
if oidc.BackChannelLogoutURI != nil {
backChannelLogout = gu.Ptr(strings.TrimSpace(*oidc.BackChannelLogoutURI))
bcl, err := c.validateBackchannelLogoutURI(oidc)
if err != nil {
return nil, err
}
backChannelLogout = gu.Ptr(bcl)
}
if oidc.LoginBaseURI != nil {
@@ -2,6 +2,7 @@ package command
import (
"context"
"net"
"testing"
"time"
@@ -10,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
@@ -407,6 +409,7 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
idGenerator id.Generator
denyList []denylist.AddressChecker
}
type args struct {
ctx context.Context
@@ -816,6 +819,83 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
},
},
},
{
name: "backchannel logout URI invalid, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
BackChannelLogoutURI: gu.Ptr("http://example.com\x00"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "backchannel logout URI on denylist, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewProjectAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"project", true, true, true,
domain.PrivateLabelingSettingUnspecified),
),
),
expectFilter(),
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "app1", "client1"),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
},
args: args{
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppName: "app",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
OIDCVersion: gu.Ptr(domain.OIDCVersionV1),
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ApplicationType: gu.Ptr(domain.OIDCApplicationTypeWeb),
AccessTokenType: gu.Ptr(domain.OIDCTokenTypeBearer),
BackChannelLogoutURI: gu.Ptr("https://blocked.example.com/logout"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -828,6 +908,10 @@ func TestCommandSide_AddOIDCApplication(t *testing.T) {
ClientSecret: emptyConfig,
},
checkPermission: newMockPermissionCheckAllowed(),
ipLookupFunction: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("1.2.3.4")}, nil
},
denyList: tt.fields.denyList,
}
c.setMilestonesCompletedForTest("instanceID")
got, err := c.AddOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
@@ -848,6 +932,7 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
t.Parallel()
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
denyList []denylist.AddressChecker
}
type args struct {
ctx context.Context
@@ -1195,13 +1280,138 @@ func TestCommandSide_ChangeOIDCApplication(t *testing.T) {
},
},
},
{
name: "backchannel logout URI invalid, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewOIDCConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
domain.OIDCVersionV1,
"app1",
"client1",
"",
nil,
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeNone,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
expectFilter(),
),
},
args: args{
ctx: context.Background(),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
BackChannelLogoutURI: gu.Ptr("http://example.com\x00"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
{
name: "backchannel logout URI on denylist, invalid argument error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
project.NewApplicationAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
"app1",
"app",
),
),
eventFromEventPusher(
project.NewOIDCConfigAddedEvent(context.Background(),
&project.NewAggregate("project1", "org1").Aggregate,
domain.OIDCVersionV1,
"app1",
"client1",
"",
nil,
[]domain.OIDCResponseType{domain.OIDCResponseTypeCode},
[]domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
domain.OIDCApplicationTypeWeb,
domain.OIDCAuthMethodTypeNone,
nil,
false,
domain.OIDCTokenTypeBearer,
false,
false,
false,
0,
nil,
false,
"",
domain.LoginVersionUnspecified,
"",
),
),
),
expectFilter(),
),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
},
args: args{
ctx: context.Background(),
oidcApp: &domain.OIDCApp{
ObjectRoot: models.ObjectRoot{
AggregateID: "project1",
},
AppID: "app1",
AuthMethodType: gu.Ptr(domain.OIDCAuthMethodTypeNone),
GrantTypes: []domain.OIDCGrantType{domain.OIDCGrantTypeAuthorizationCode},
ResponseTypes: []domain.OIDCResponseType{domain.OIDCResponseTypeCode},
BackChannelLogoutURI: gu.Ptr("https://blocked.example.com/logout"),
},
resourceOwner: "org1",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// t.Parallel()
t.Parallel()
r := &Commands{
eventstore: tt.fields.eventstore(t),
checkPermission: newMockPermissionCheckAllowed(),
ipLookupFunction: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("1.2.3.4")}, nil
},
denyList: tt.fields.denyList,
}
got, err := r.UpdateOIDCApplication(tt.args.ctx, tt.args.oidcApp, tt.args.resourceOwner)
if tt.res.err == nil {
+12
View File
@@ -170,6 +170,11 @@ func (c *Commands) AddSMSConfigHTTP(ctx context.Context, config *AddSMSHTTP) (er
return err
}
}
if err := c.validateNotificationWebhookEndpoint(config.Endpoint); err != nil {
return err
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return err
@@ -217,6 +222,13 @@ func (c *Commands) ChangeSMSConfigHTTP(ctx context.Context, config *ChangeSMSHTT
if config.ID == "" {
return zerrors.ThrowInvalidArgument(nil, "COMMAND-phyb2e4Kll", "Errors.IDMissing")
}
if config.Endpoint != nil {
if err := c.validateNotificationWebhookEndpoint(*config.Endpoint); err != nil {
return err
}
}
smsConfigWriteModel, err := c.getSMSConfig(ctx, config.ResourceOwner, config.ID)
if err != nil {
return err
+66
View File
@@ -3,6 +3,7 @@ package command
import (
"context"
"errors"
"net"
"testing"
"time"
@@ -11,6 +12,7 @@ import (
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
@@ -312,6 +314,8 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
alg crypto.EncryptionAlgorithm
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
ctx context.Context
@@ -366,6 +370,9 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
@@ -381,6 +388,30 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
},
},
},
{
name: "add sms config http, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "providerid"),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
http: &AddSMSHTTP{
ResourceOwner: "INSTANCE",
Description: "description",
Endpoint: "https://blocked.example.com/hook",
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -390,6 +421,8 @@ func TestCommandSide_AddSMSConfigHTTP(t *testing.T) {
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
smsEncryption: tt.fields.alg,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.AddSMSConfigHTTP(tt.args.ctx, tt.args.http)
if tt.res.err == nil {
@@ -410,6 +443,8 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
eventstore func(*testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
ctx context.Context
@@ -499,6 +534,9 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
),
),
),
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
@@ -552,6 +590,9 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
@@ -569,6 +610,29 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
},
},
},
{
name: "change sms config, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
ctx: context.Background(),
http: &ChangeSMSHTTP{
ResourceOwner: "INSTANCE",
ID: "providerid",
Endpoint: gu.Ptr("https://blocked.example.com/hook"),
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -576,6 +640,8 @@ func TestCommandSide_ChangeSMSConfigHTTP(t *testing.T) {
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.ChangeSMSConfigHTTP(tt.args.ctx, tt.args.http)
if tt.res.err == nil {
+24
View File
@@ -3,11 +3,13 @@ package command
import (
"context"
"net"
"net/url"
"strings"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
@@ -304,6 +306,10 @@ func (c *Commands) AddSMTPConfigHTTP(ctx context.Context, config *AddSMTPConfigH
}
}
if err := c.validateNotificationWebhookEndpoint(config.Endpoint); err != nil {
return err
}
smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, "")
if err != nil {
return err
@@ -349,6 +355,10 @@ func (c *Commands) ChangeSMTPConfigHTTP(ctx context.Context, config *ChangeSMTPC
return zerrors.ThrowInvalidArgument(nil, "COMMAND-2MHkV8ObWo", "Errors.IDMissing")
}
if err := c.validateNotificationWebhookEndpoint(config.Endpoint); err != nil {
return err
}
smtpConfigWriteModel, err := c.getSMTPConfig(ctx, config.ResourceOwner, config.ID, "")
if err != nil {
return err
@@ -698,6 +708,20 @@ func (c *Commands) prepareAddAndActivateSMTPConfig(a *instance.Aggregate, descri
}
}
func (c *Commands) validateNotificationWebhookEndpoint(endpoint string) error {
if endpoint == "" {
return nil
}
uri, err := url.Parse(endpoint)
if err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-Sz84nFo3aK", "Errors.Invalid.Argument")
}
if err := denylist.IsURLBlocked(c.denyList, uri, c.ipLookupFunction); err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-Uf0294nKls", "Errors.NotificationProvider.Blocked.Endpoint")
}
return nil
}
func getSMTPConfigWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, id, domain string) (_ *IAMSMTPConfigWriteModel, err error) {
writeModel := NewIAMSMTPConfigWriteModel(authz.GetInstance(ctx).InstanceID(), id, domain)
events, err := filter(ctx, writeModel.Query())
+127
View File
@@ -3,6 +3,7 @@ package command
import (
"context"
"errors"
"net"
"testing"
"time"
@@ -11,6 +12,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
@@ -1335,6 +1337,8 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
idGenerator id.Generator
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
http *AddSMTPConfigHTTP
@@ -1387,6 +1391,9 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("12345678", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &AddSMTPConfigHTTP{
@@ -1401,6 +1408,29 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
},
},
},
{
name: "add smtp config, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "configid"),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &AddSMTPConfigHTTP{
ResourceOwner: "INSTANCE",
Description: "test",
Endpoint: "https://blocked.example.com/hook",
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1409,6 +1439,8 @@ func TestCommandSide_AddSMTPConfigHTTP(t *testing.T) {
idGenerator: tt.fields.idGenerator,
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.AddSMTPConfigHTTP(context.Background(), tt.args.http)
if tt.res.err == nil {
@@ -1430,6 +1462,8 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
eventstore func(t *testing.T) *eventstore.Eventstore
newEncryptedCodeWithDefault encryptedCodeWithDefaultFunc
defaultSecretGenerators *SecretGenerators
denyList []denylist.AddressChecker
ipLookupFunc func(string) ([]net.IP, error)
}
type args struct {
http *ChangeSMTPConfigHTTP
@@ -1479,6 +1513,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
eventstore: expectEventstore(
expectFilter(),
),
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
@@ -1516,6 +1553,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
),
),
),
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
@@ -1569,6 +1609,9 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
),
newEncryptedCodeWithDefault: mockEncryptedCodeWithDefault("87654321", time.Hour),
defaultSecretGenerators: &SecretGenerators{},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
@@ -1585,6 +1628,28 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
},
},
},
{
name: "change smtp config, blocked endpoint",
fields: fields{
eventstore: expectEventstore(),
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
ipLookupFunc: func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
},
},
args: args{
http: &ChangeSMTPConfigHTTP{
ResourceOwner: "INSTANCE",
ID: "configid",
Endpoint: "https://blocked.example.com/hook",
},
},
res: res{
err: func(err error) bool {
return errors.Is(err, zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", ""))
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -1592,6 +1657,8 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
eventstore: tt.fields.eventstore(t),
newEncryptedCodeWithDefault: tt.fields.newEncryptedCodeWithDefault,
defaultSecretGenerators: tt.fields.defaultSecretGenerators,
denyList: tt.fields.denyList,
ipLookupFunction: tt.fields.ipLookupFunc,
}
err := r.ChangeSMTPConfigHTTP(context.Background(), tt.args.http)
if tt.res.err == nil {
@@ -1607,6 +1674,66 @@ func TestCommandSide_ChangeSMTPConfigHTTP(t *testing.T) {
}
}
func TestCommands_validateNotificationWebhookEndpoint(t *testing.T) {
t.Parallel()
blockedLookup := func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("10.0.0.1")}, nil
}
wantBlockedErr := zerrors.ThrowInvalidArgument(nil, "COMMAND-Uf0294nKls", "")
tests := []struct {
name string
denyList []denylist.AddressChecker
endpoint string
wantErr error
}{
{
name: "empty endpoint skips denylist check",
endpoint: "",
},
{
name: "endpoint not on denylist, ok",
endpoint: "https://external.example.com/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
},
{
name: "no denylist configured, ok",
endpoint: "https://anything.example.com/hook",
},
{
name: "endpoint domain on denylist, blocked",
endpoint: "https://blocked.example.com/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("blocked.example.com")},
wantErr: wantBlockedErr,
},
{
name: "endpoint resolves to blocked IP range, blocked",
endpoint: "https://internal.host/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("10.0.0.0/8")},
wantErr: wantBlockedErr,
},
{
name: "localhost IP directly in URL is blocked",
endpoint: "http://127.0.0.1/hook",
denyList: []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.0/8")},
wantErr: wantBlockedErr,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
r := &Commands{
denyList: tt.denyList,
ipLookupFunction: blockedLookup,
}
err := r.validateNotificationWebhookEndpoint(tt.endpoint)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCommandSide_ActivateSMTPConfig(t *testing.T) {
type fields struct {
eventstore func(t *testing.T) *eventstore.Eventstore
+1 -4
View File
@@ -14,10 +14,7 @@ type AddressChecker interface {
func ParseDenyList(inputList []string) ([]AddressChecker, error) {
var toReturn []AddressChecker
for _, input := range inputList {
parsed, parseErr := NewHostChecker(input)
if parseErr != nil {
return nil, parseErr
}
parsed := NewHostChecker(input)
if parsed != nil {
toReturn = append(toReturn, parsed)
}
+45
View File
@@ -0,0 +1,45 @@
package denylist
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseDenyList(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputIPList []string
expectedAddressCheckers int
expectedError error
}{
{
testName: "when empty list should return no error and no checker list",
},
{
testName: "list with a non-ip should return no error and no checker list",
inputIPList: []string{"127.0.0.1", "localhost", "not an ip"},
expectedAddressCheckers: 3,
},
{
testName: "parseable IP list should return no error, non-zero hash and non-empty checker list",
inputIPList: []string{"127.0.0.1", "localhost", "1.1.1.1"},
expectedAddressCheckers: 3,
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
// Test
checkers, err := ParseDenyList(tc.inputIPList)
// Verify
assert.Len(t, checkers, tc.expectedAddressCheckers)
assert.ErrorIs(t, err, tc.expectedError)
})
}
}
+40 -20
View File
@@ -3,6 +3,7 @@ package denylist
import (
"net"
"net/url"
"strings"
internal_net "github.com/zitadel/zitadel/internal/net"
)
@@ -15,23 +16,30 @@ type HostChecker struct {
Domain string
}
func NewHostChecker(entry string) (AddressChecker, error) {
// NewHostChecker returns an [AddressChecker].
//
// If the input string is neither a network, nor an IP address, it is considered a domain and saved as such
func NewHostChecker(entry string) AddressChecker {
if entry == "" {
return nil, nil
return nil
}
_, network, err := net.ParseCIDR(entry)
if err == nil {
return &HostChecker{Net: network}, nil
return &HostChecker{Net: network}
}
if ip := net.ParseIP(entry); ip != nil {
return &HostChecker{IP: ip}, nil
return &HostChecker{IP: ip}
}
return &HostChecker{Domain: entry}, nil
return &HostChecker{Domain: entry}
}
// IsDenied checks if the address or one of the IPs is blocked by the [HostChecker].
// The address is checked using string comparison.
// If either is blocked, an [AddressDeniedError] will be returned.
// If either is not blocked, it will return nil.
func (c *HostChecker) IsDenied(ips []net.IP, address string) error {
// if the address matches the domain, no additional checks as needed
if c.Domain == address {
if c.Domain != "" && strings.EqualFold(c.Domain, address) {
return NewAddressDeniedError(c.Domain)
}
// otherwise we need to check on ips (incl. the resolved ips of the host)
@@ -46,20 +54,9 @@ func (c *HostChecker) IsDenied(ips []net.IP, address string) error {
return nil
}
// IsHostBlocked checks address against denyList. If a match is found, an [AddressDeniedError] will be returned
// Takes an input lookupFunc to convert address to a list of IP addresses.
// If lookupFunc is nil, it defaults to [net.LookupIP]
func IsHostBlocked(denyList []AddressChecker, address *url.URL, lookupFunc internal_net.IPLookupFunc) error {
if lookupFunc == nil {
lookupFunc = net.LookupIP
}
host := address.Hostname()
ips, err := internal_net.HostnameToIPList(host, lookupFunc)
if err != nil {
return err
}
// IsHostBlocked checks address against denyList. If a match is found, an [AddressDeniedError] will be returned.
// If no match is found, it will return nil.
func IsHostBlocked(denyList []AddressChecker, host string, ips ...net.IP) error {
for _, denied := range denyList {
if err := denied.IsDenied(ips, host); err != nil {
return err
@@ -67,3 +64,26 @@ func IsHostBlocked(denyList []AddressChecker, address *url.URL, lookupFunc inter
}
return nil
}
// IsHostnameBlocked checks a hostname against the denyList. If a match is found, an [AddressDeniedError] will be returned
// It takes an input lookupFunc to convert the hostname to a list of IP addresses.
// If lookupFunc is nil, it defaults to [net.LookupIP]
func IsHostnameBlocked(denyList []AddressChecker, hostname string, lookupFunc internal_net.IPLookupFunc) error {
if lookupFunc == nil {
lookupFunc = net.LookupIP
}
ips, err := internal_net.HostnameToIPList(hostname, lookupFunc)
if err != nil {
return err
}
return IsHostBlocked(denyList, hostname, ips...)
}
// IsURLBlocked checks a URL against denyList. If a match is found, an [AddressDeniedError] will be returned
// It takes an input lookupFunc to convert the URL's hostname to a list of IP addresses.
// If lookupFunc is nil, it defaults to [net.LookupIP]
func IsURLBlocked(denyList []AddressChecker, address *url.URL, lookupFunc internal_net.IPLookupFunc) error {
return IsHostnameBlocked(denyList, address.Hostname(), lookupFunc)
}
+57 -20
View File
@@ -7,23 +7,25 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestIsHostBlocked(t *testing.T) {
func TestIsURLBlocked(t *testing.T) {
t.Parallel()
var denyList = []AddressChecker{
mustNewHostChecker(t, "192.168.5.0/24"),
mustNewHostChecker(t, "127.0.0.1"),
mustNewHostChecker(t, "test.com"),
denyList := []AddressChecker{
NewHostChecker("192.168.5.0/24"),
NewHostChecker("127.0.0.1"),
NewHostChecker("test.com"),
}
type fields struct {
lookup func(host string) ([]net.IP, error)
}
type args struct {
address *url.URL
}
tests := []struct {
name string
fields fields
@@ -31,21 +33,31 @@ func TestIsHostBlocked(t *testing.T) {
want error
}{
{
name: "in range",
name: "in range (CIDR match)",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("192.168.5.4")}, nil
},
},
args: args{
address: mustNewURL(t, "https://192.168.5.4/hodor"),
},
want: NewAddressDeniedError("192.168.5.0/24"),
},
{
name: "exact ip",
name: "exact ip literal match",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("127.0.0.1")}, nil
},
},
args: args{
address: mustNewURL(t, "http://127.0.0.1:8080/hodor"),
},
want: NewAddressDeniedError("127.0.0.1"),
},
{
name: "address match",
name: "domain string match",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("194.264.52.4")}, nil
@@ -57,7 +69,7 @@ func TestIsHostBlocked(t *testing.T) {
want: NewAddressDeniedError("test.com"),
},
{
name: "address not match",
name: "allowed domain string pass",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("194.264.52.4")}, nil
@@ -69,7 +81,7 @@ func TestIsHostBlocked(t *testing.T) {
want: nil,
},
{
name: "looked up ip matches",
name: "unmatched domain resolves to blocked IP (SSRF pivot attempt)",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("127.0.0.1")}, nil
@@ -81,7 +93,7 @@ func TestIsHostBlocked(t *testing.T) {
want: NewAddressDeniedError("127.0.0.1"),
},
{
name: "lookup failure",
name: "dns resolver failures are isolated",
fields: fields{
lookup: func(host string) ([]net.IP, error) {
return nil, io.EOF
@@ -93,23 +105,48 @@ func TestIsHostBlocked(t *testing.T) {
want: zerrors.ThrowInternal(io.EOF, "NET-4m9s2", "lookup failed"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := IsHostBlocked(denyList, tt.args.address, tt.fields.lookup)
got := IsURLBlocked(denyList, tt.args.address, tt.fields.lookup)
assert.ErrorIs(t, got, tt.want)
})
}
}
func mustNewHostChecker(t *testing.T, ip string) AddressChecker {
t.Helper()
checker, err := NewHostChecker(ip)
if err != nil {
t.Errorf("unable to parse cidr of %q because: %v", ip, err)
t.FailNow()
func TestIsHostnameBlocked_EdgeCases(t *testing.T) {
t.Parallel()
denyList := []AddressChecker{
NewHostChecker("127.0.0.1"),
NewHostChecker("blocked.example.com"),
}
return checker
t.Run("blocked by host lookup via hostname method", func(t *testing.T) {
t.Parallel()
err := IsHostnameBlocked(denyList, "safe.example.com", func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("127.0.0.1")}, nil
})
assert.ErrorIs(t, err, NewAddressDeniedError("127.0.0.1"))
})
t.Run("blocked by exact domain string mapping", func(t *testing.T) {
t.Parallel()
err := IsHostnameBlocked(denyList, "blocked.example.com", func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("192.0.2.1")}, nil
})
assert.ErrorIs(t, err, NewAddressDeniedError("blocked.example.com"))
})
t.Run("ipv6 target check compatibility", func(t *testing.T) {
t.Parallel()
// URL parsing or raw string handling yields plain IPv6 targets, make sure checker behaves cleanly
err := IsHostnameBlocked(denyList, "::1", func(_ string) ([]net.IP, error) {
return []net.IP{net.ParseIP("::1")}, nil
})
assert.NoError(t, err)
})
}
func mustNewURL(t *testing.T, raw string) *url.URL {
+37
View File
@@ -0,0 +1,37 @@
package denylist
import (
"net"
"net/http"
"syscall"
"time"
)
// NewHTTPTransport returns a cloned default transport that enforces denylist checks
// right before each TCP dial to avoid DNS rebinding TOCTOU gaps.
func NewHTTPTransport(denyList []AddressChecker) http.RoundTripper {
base := http.DefaultTransport.(*http.Transport).Clone()
if len(denyList) == 0 {
return base
}
dialer := &net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
Control: func(network, address string, c syscall.RawConn) error {
host, _, err := net.SplitHostPort(address)
if err != nil {
return err
}
parsedIP := net.ParseIP(host)
if parsedIP == nil { // at this point, it must be an IP so it should never happen
return &net.DNSError{Err: "invalid IP address", Name: host}
}
return IsHostBlocked(denyList, host, parsedIP)
},
}
base.DialContext = dialer.DialContext
return base
}
+111
View File
@@ -0,0 +1,111 @@
package denylist
import (
"context"
"errors"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewHTTPTransport_Allowed(t *testing.T) {
t.Parallel()
// Create a denylist that does NOT block our local test server
denyList := []AddressChecker{
NewHostChecker("192.168.1.0/24"),
NewHostChecker("10.0.0.1"),
}
// Spin up a dummy local test server. It will listen on a loopback address like 127.0.0.1 or ::1
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("success"))
}))
defer server.Close()
// Instantiate the transport with the denylist rules
transport := NewHTTPTransport(denyList)
client := &http.Client{Transport: transport}
// Execute the request to the allowed local server target
resp, err := client.Get(server.URL)
assert.NoError(t, err)
if resp != nil {
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
}
func TestNewHTTPTransport_BlockedByControl(t *testing.T) {
t.Parallel()
// Spin up a test server so we can dynamically extract its specific local listening loopback IP
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Parse out the target host/ip text from the server URL
u, err := net.Dial("tcp", server.Listener.Addr().String())
assert.NoError(t, err)
localIP, _, err := net.SplitHostPort(u.RemoteAddr().String())
assert.NoError(t, err)
_ = u.Close()
// Add the explicitly discovered test server IP to the denylist
denyList := []AddressChecker{
NewHostChecker(localIP),
}
transport := NewHTTPTransport(denyList)
client := &http.Client{Transport: transport}
// This must fail because the Control hook interceptor throws an address denied error
// right before the low-level TCP handshake begins.
_, err = client.Get(server.URL) //nolint:bodyclose
assert.Error(t, err)
// Because Go wraps dialer context execution errors inside a generic url.Error,
// we verify that the true root cause matches our address denial definition.
var addressDeniedErr *AddressDeniedError
assert.ErrorAs(t, err, &addressDeniedErr)
}
func TestNewHTTPTransport_EmptyDenylistReturnsDefaultClone(t *testing.T) {
t.Parallel()
// An empty list should bypass adding a dialer wrapper entirely for performance
transport := NewHTTPTransport([]AddressChecker{})
assert.NotNil(t, transport)
httpTransport, ok := transport.(*http.Transport)
assert.True(t, ok)
// Ensure that DialContext is still populated (inherited cleanly from http.DefaultTransport Clone)
assert.NotNil(t, httpTransport.DialContext)
}
func TestNewHTTPTransport_MalformedAddressHandling(t *testing.T) {
t.Parallel()
denyList := []AddressChecker{
NewHostChecker("127.0.0.1"),
}
transport := NewHTTPTransport(denyList)
httpTransport, ok := transport.(*http.Transport)
assert.True(t, ok)
// Explicitly invoke DialContext with an unparseable address format (missing a valid port boundary)
// to test how the underlying Control code deals with structural string parsing failure exceptions.
_, err := httpTransport.DialContext(context.Background(), "tcp", "malformed-address-without-port")
assert.Error(t, err)
// Ensure it didn't crash and returns a clean, standard connection framework error context
var netErr net.Error
assert.True(t, errors.As(err, &netErr))
}
+9 -23
View File
@@ -9,9 +9,7 @@ import (
"encoding/json"
"encoding/pem"
"io"
"net"
"net/http"
"net/url"
"sync"
"time"
@@ -22,7 +20,6 @@ import (
zhttp "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/oidc/sign"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/denylist"
"github.com/zitadel/zitadel/internal/domain"
target_domain "github.com/zitadel/zitadel/internal/execution/target"
"github.com/zitadel/zitadel/internal/repository/execution"
@@ -46,7 +43,7 @@ func CallTargets(
info ContextInfo,
alg crypto.EncryptionAlgorithm,
activeSigningKey GetActiveSigningWebKey,
deniedIPList []denylist.AddressChecker,
client *http.Client,
) (_ any, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -58,7 +55,7 @@ func CallTargets(
for _, target := range targets {
// call the type of target
resp, err := CallTarget(ctx, target, info, alg, signerOnce, encrypters, deniedIPList)
resp, err := CallTarget(ctx, target, info, alg, signerOnce, encrypters, client)
// handle error if interrupt is set
logging.WithFields("instanceID", authz.GetInstance(ctx).InstanceID(), "target", target.GetTargetID()).OnError(err).Error("error calling target")
if err != nil && target.IsInterruptOnError() {
@@ -86,7 +83,7 @@ func CallTarget(
alg crypto.EncryptionAlgorithm,
signerOnce sign.SignerFunc,
encrypters *sync.Map,
deniedIPList []denylist.AddressChecker,
client *http.Client,
) (res []byte, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -96,16 +93,6 @@ func CallTarget(
return nil, zerrors.ThrowInternal(err, "EXEC-thiiCh5b", "Errors.Internal")
}
if target.GetEndpoint() != "" {
endpointURL, err := url.Parse(target.GetEndpoint())
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "EXEC-N5lu09", "Errors.Endpoint.Invalid")
}
if err := denylist.IsHostBlocked(deniedIPList, endpointURL, net.LookupIP); err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "EXEC-N5lu09", "Errors.Endpoint.Denied")
}
}
body, err := payload(ctx, info.GetHTTPRequestBody(), target, signerOnce, encrypters)
if err != nil {
return nil, err
@@ -114,13 +101,13 @@ func CallTarget(
switch target.GetTargetType() {
// get request, ignore response and return request and error for handling in list of targets
case target_domain.TargetTypeWebhook:
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey)
return nil, webhook(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey, client)
// get request, return response and error
case target_domain.TargetTypeCall:
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey)
return Call(ctx, target.GetEndpoint(), target.GetTimeout(), body, signingKey, client)
case target_domain.TargetTypeAsync:
go func(ctx context.Context, target target_domain.Target, info []byte) {
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, signingKey); err != nil {
if _, err := Call(ctx, target.GetEndpoint(), target.GetTimeout(), info, signingKey, client); err != nil {
logging.WithFields("target", target.GetTargetID()).OnError(err).Info(err)
}
}(context.WithoutCancel(ctx), target, body)
@@ -238,13 +225,13 @@ func publicKeyFromBytes(data []byte) (any, jose.KeyAlgorithm, error) {
}
// webhook call a webhook, ignore the response but return the errror
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) error {
_, err := Call(ctx, url, timeout, body, signingKey)
func webhook(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string, client *http.Client) error {
_, err := Call(ctx, url, timeout, body, signingKey, client)
return err
}
// Call function to do a post HTTP request to a desired url with timeout
func Call(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string) (_ []byte, err error) {
func Call(ctx context.Context, url string, timeout time.Duration, body []byte, signingKey string, client *http.Client) (_ []byte, err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, span := tracing.NewSpan(ctx)
defer func() {
@@ -261,7 +248,6 @@ func Call(ctx context.Context, url string, timeout time.Duration, body []byte, s
req.Header.Set(actions.SigningHeader, actions.ComputeSignatureHeader(time.Now(), body, signingKey))
}
client := http.DefaultClient
resp, err := client.Do(req)
if err != nil {
return nil, err
+4 -6
View File
@@ -423,9 +423,7 @@ func Test_CallTarget(t *testing.T) {
}
func Test_CallTargets(t *testing.T) {
deniedLocalhost, err := denylist.NewHostChecker("127.0.0.1")
require.NoError(t, err)
deniedIPs := []denylist.AddressChecker{deniedLocalhost}
deniedIPs := []denylist.AddressChecker{denylist.NewHostChecker("127.0.0.1")}
type args struct {
ctx context.Context
info *middleware.ContextInfoRequest
@@ -720,7 +718,7 @@ func checkRequest(t *testing.T, sent *http.Request, method string, checkExpected
func testCall(ctx context.Context, timeout time.Duration, body []byte, signingKey string) func(string) ([]byte, error) {
return func(url string) ([]byte, error) {
return execution.Call(ctx, url, timeout, body, signingKey)
return execution.Call(ctx, url, timeout, body, signingKey, http.DefaultClient)
}
}
@@ -734,7 +732,7 @@ func testCallTarget(ctx context.Context,
) func(string) ([]byte, error) {
return func(url string) (r []byte, err error) {
target.Endpoint = url
return execution.CallTarget(ctx, target, info, alg, signerOnce, encrypters, actionsDenyList)
return execution.CallTarget(ctx, target, info, alg, signerOnce, encrypters, &http.Client{Transport: denylist.NewHTTPTransport(actionsDenyList)})
}
}
@@ -751,7 +749,7 @@ func testCallTargets(ctx context.Context,
t.Endpoint = urls[i]
targets[i] = t
}
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, actionsDenyList)
return execution.CallTargets(ctx, targets, info, alg, activeSigningKey, &http.Client{Transport: denylist.NewHTTPTransport(actionsDenyList)})
}
}
+3 -1
View File
@@ -2,6 +2,7 @@ package execution
import (
"context"
"net/http"
"time"
"github.com/zitadel/zitadel/internal/crypto"
@@ -16,12 +17,13 @@ var (
func Register(
ctx context.Context,
workerConfig WorkerConfig,
httpClient *http.Client,
queue *queue.Queue,
targetEncAlg crypto.EncryptionAlgorithm,
activeSigningKey GetActiveSigningWebKey,
) {
queue.ShouldStart()
queue.AddWorkers(ctx, NewWorker(workerConfig, targetEncAlg, activeSigningKey, time.Now))
queue.AddWorkers(ctx, NewWorker(workerConfig, targetEncAlg, activeSigningKey, time.Now, httpClient))
}
func Start(ctx context.Context) {
+7 -3
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/riverqueue/river"
@@ -18,8 +19,9 @@ import (
type Worker struct {
river.WorkerDefaults[*exec_repo.Request]
config WorkerConfig
now NowFunc
config WorkerConfig
httpClient *http.Client
now NowFunc
targetEncAlg crypto.EncryptionAlgorithm
activeSigningKey GetActiveSigningWebKey
@@ -47,7 +49,7 @@ func (w *Worker) Work(ctx context.Context, job *river.Job[*exec_repo.Request]) e
return river.JobCancel(fmt.Errorf("unable to unmarshal targets because %w", err))
}
_, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args), w.targetEncAlg, w.activeSigningKey, w.config.DenyList)
_, err = CallTargets(ctx, targets, exec_repo.ContextInfoFromRequest(job.Args), w.targetEncAlg, w.activeSigningKey, w.httpClient)
if err != nil {
// If there is an error returned from the targets, it means that the execution was interrupted
return river.JobCancel(fmt.Errorf("interruption during call of targets because %w", err))
@@ -70,9 +72,11 @@ func NewWorker(
targetEncAlg crypto.EncryptionAlgorithm,
activeSigningKey GetActiveSigningWebKey,
now NowFunc,
httpClient *http.Client,
) *Worker {
return &Worker{
config: config,
httpClient: httpClient,
now: now,
targetEncAlg: targetEncAlg,
activeSigningKey: activeSigningKey,
+1
View File
@@ -61,6 +61,7 @@ func newExecutionWorker(f fieldsWorker) *execution.Worker {
nil,
mockGetActiveSigningWebKey,
f.now,
http.DefaultClient,
)
}
+3 -2
View File
@@ -2,6 +2,7 @@ package apple
import (
"crypto/ecdsa"
"net/http"
"time"
"github.com/go-jose/go-jose/v4"
@@ -25,13 +26,13 @@ type Provider struct {
*oidc.Provider
}
func New(clientID, teamID, keyID, callbackURL string, key []byte, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
func New(clientID, teamID, keyID, callbackURL string, key []byte, scopes []string, httpClient *http.Client, options ...oidc.ProviderOpts) (*Provider, error) {
secret, err := clientSecretFromPrivateKey(key, teamID, clientID, keyID)
if err != nil {
return nil, err
}
options = append(options, oidc.WithResponseMode("form_post"))
rp, err := oidc.New(name, issuer, clientID, secret, callbackURL, scopes, oidc.DefaultMapper, options...)
rp, err := oidc.New(name, issuer, clientID, secret, callbackURL, scopes, oidc.DefaultMapper, httpClient, options...)
if err != nil {
return nil, err
}
+2 -1
View File
@@ -2,6 +2,7 @@ package apple
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -57,7 +58,7 @@ func TestProvider_BeginAuth(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes)
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes, http.DefaultClient)
r.NoError(err)
ctx := context.Background()
session, err := provider.BeginAuth(ctx, "testState")
+2 -1
View File
@@ -3,6 +3,7 @@ package apple
import (
"context"
"errors"
"net/http"
"testing"
"time"
@@ -156,7 +157,7 @@ func TestSession_FetchUser(t *testing.T) {
// call the real discovery endpoint
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes)
provider, err := New(tt.fields.clientID, tt.fields.teamID, tt.fields.keyID, tt.fields.redirectURI, tt.fields.privateKey, tt.fields.scopes, http.DefaultClient)
require.NoError(t, err)
session := &Session{
+3 -1
View File
@@ -2,6 +2,7 @@ package azuread
import (
"fmt"
"net/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
"golang.org/x/oauth2"
@@ -88,7 +89,7 @@ func WithOAuthOptions(opts ...oauth.ProviderOpts) ProviderOptions {
// New creates an AzureAD provider using the [oauth.Provider] (OAuth 2.0 generic provider).
// By default, it uses the [CommonTenant] and unverified emails.
func New(name, clientID, clientSecret, redirectURI string, scopes []string, opts ...ProviderOptions) (*Provider, error) {
func New(name, clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, opts ...ProviderOptions) (*Provider, error) {
provider := &Provider{
tenant: CommonTenant,
options: make([]oauth.ProviderOpts, 0),
@@ -104,6 +105,7 @@ func New(name, clientID, clientSecret, redirectURI string, scopes []string, opts
func() idp.User {
return &User{isEmailVerified: provider.emailVerified}
},
httpClient,
provider.options...,
)
if err != nil {
@@ -2,6 +2,7 @@ package azuread
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -74,7 +75,7 @@ func TestProvider_BeginAuth(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
r.NoError(err)
ctx := context.Background()
@@ -169,7 +170,7 @@ func TestProvider_Options(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
require.NoError(t, err)
a.Equal(tt.want.name, provider.Name())
@@ -244,7 +244,7 @@ func TestSession_FetchUser(t *testing.T) {
tt.fields.httpMock()
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
require.NoError(t, err)
session := &Session{
@@ -395,7 +395,7 @@ func TestSession_RetrievePreviousID(t *testing.T) {
tt.fields.httpMock()
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
provider, err := New(tt.fields.name, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient)
require.NoError(t, err)
session := &Session{
Provider: provider,
+5 -3
View File
@@ -1,6 +1,7 @@
package github
import (
"net/http"
"strconv"
"time"
@@ -23,13 +24,13 @@ const (
var _ idp.Provider = (*Provider)(nil)
// New creates a GitHub.com provider using the [oauth.Provider] (OAuth 2.0 generic provider)
func New(clientID, secret, callbackURL string, scopes []string, options ...oauth.ProviderOpts) (*Provider, error) {
return NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL, scopes, options...)
func New(clientID, secret, callbackURL string, scopes []string, httpClient *http.Client, options ...oauth.ProviderOpts) (*Provider, error) {
return NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL, scopes, httpClient, options...)
}
// NewCustomURL creates a GitHub provider using the [oauth.Provider] (OAuth 2.0 generic provider)
// with custom endpoints, e.g. GitHub Enterprise server
func NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL string, scopes []string, options ...oauth.ProviderOpts) (*Provider, error) {
func NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profileURL string, scopes []string, httpClient *http.Client, options ...oauth.ProviderOpts) (*Provider, error) {
rp, err := oauth.New(
newConfig(clientID, secret, callbackURL, authURL, tokenURL, scopes),
name,
@@ -37,6 +38,7 @@ func NewCustomURL(name, clientID, secret, callbackURL, authURL, tokenURL, profil
func() idp.User {
return new(User)
},
httpClient,
options...,
)
if err != nil {
+2 -1
View File
@@ -2,6 +2,7 @@ package github
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -41,7 +42,7 @@ func TestProvider_BeginAuth(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
r.NoError(err)
ctx := context.Background()
@@ -395,7 +395,7 @@ func TestSession_FetchUser(t *testing.T) {
}
a := assert.New(t)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
require.NoError(t, err)
session := &Session{
+6 -4
View File
@@ -1,6 +1,8 @@
package gitlab
import (
"net/http"
openid "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/zitadel/internal/idp"
@@ -20,13 +22,13 @@ type Provider struct {
}
// New creates a GitLab.com provider using the [oidc.Provider] (OIDC generic provider)
func New(clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, scopes, options...)
func New(clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, options ...oidc.ProviderOpts) (*Provider, error) {
return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, scopes, httpClient, options...)
}
// NewCustomIssuer creates a GitLab provider using the [oidc.Provider] (OIDC generic provider)
// with a custom issuer for self-managed instances
func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, options ...oidc.ProviderOpts) (*Provider, error) {
if len(scopes) == 0 {
// the OIDC provider would set `openid profile email phone` as default scope,
// but since gitlab does not handle unknown scopes correctly (phone) and returns an error,
@@ -35,7 +37,7 @@ func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, s
}
// gitlab is currently not able to handle the prompt `select_account`:
// https://gitlab.com/gitlab-org/gitlab/-/issues/377368
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, oidc.DefaultMapper, options...)
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, oidc.DefaultMapper, httpClient, options...)
if err != nil {
return nil, err
}
+2 -1
View File
@@ -2,6 +2,7 @@ package gitlab
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -53,7 +54,7 @@ func TestProvider_BeginAuth(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.opts...)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.opts...)
r.NoError(err)
ctx := context.Background()
session, err := provider.BeginAuth(ctx, "testState")
@@ -3,6 +3,7 @@ package gitlab
import (
"context"
"errors"
"net/http"
"testing"
"time"
@@ -166,7 +167,7 @@ func TestProvider_FetchUser(t *testing.T) {
// call the real discovery endpoint
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient, tt.fields.options...)
require.NoError(t, err)
session := &oidc.Session{
+4 -2
View File
@@ -1,6 +1,8 @@
package google
import (
"net/http"
openid "github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/zitadel/internal/idp"
@@ -20,8 +22,8 @@ type Provider struct {
}
// New creates a Google provider using the [oidc.Provider] (OIDC generic provider)
func New(clientID, clientSecret, redirectURI string, scopes []string, opts ...oidc.ProviderOpts) (*Provider, error) {
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, userMapper, append(opts, oidc.WithSelectAccount())...)
func New(clientID, clientSecret, redirectURI string, scopes []string, httpClient *http.Client, opts ...oidc.ProviderOpts) (*Provider, error) {
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, userMapper, httpClient, append(opts, oidc.WithSelectAccount())...)
if err != nil {
return nil, err
}
+2 -1
View File
@@ -2,6 +2,7 @@ package google
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -41,7 +42,7 @@ func TestProvider_BeginAuth(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient)
r.NoError(err)
ctx := context.Background()
@@ -3,6 +3,7 @@ package google
import (
"context"
"errors"
"net/http"
"testing"
"time"
@@ -167,7 +168,7 @@ func TestSession_FetchUser(t *testing.T) {
// call the real discovery endpoint
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, http.DefaultClient)
require.NoError(t, err)
session := &oidc.Session{
+4 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"errors"
"net/http"
"net/url"
"github.com/zitadel/zitadel/internal/crypto"
@@ -34,6 +35,7 @@ type Provider struct {
isAutoCreation bool
isAutoUpdate bool
encryptionAlg crypto.EncryptionAlgorithm
httpClient *http.Client
}
type ProviderOpts func(provider *Provider)
@@ -68,7 +70,7 @@ func WithAutoUpdate() ProviderOpts {
}
// New creates a JWT provider
func New(name, issuer, jwtEndpoint, keysEndpoint, headerName, audience string, encryptionAlg crypto.EncryptionAlgorithm, options ...ProviderOpts) (*Provider, error) {
func New(name, issuer, jwtEndpoint, keysEndpoint, headerName, audience string, encryptionAlg crypto.EncryptionAlgorithm, httpClient *http.Client, options ...ProviderOpts) (*Provider, error) {
provider := &Provider{
name: name,
issuer: issuer,
@@ -77,6 +79,7 @@ func New(name, issuer, jwtEndpoint, keysEndpoint, headerName, audience string, e
headerName: headerName,
audience: audience,
encryptionAlg: encryptionAlg,
httpClient: httpClient,
}
for _, option := range options {
option(provider)
+3
View File
@@ -3,6 +3,7 @@ package jwt
import (
"context"
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -114,6 +115,7 @@ func TestProvider_BeginAuth(t *testing.T) {
tt.fields.headerName,
tt.fields.audience,
tt.fields.encryptionAlg(t),
http.DefaultClient,
)
require.NoError(t, err)
@@ -221,6 +223,7 @@ func TestProvider_Options(t *testing.T) {
tt.fields.headerName,
tt.fields.audience,
tt.fields.encryptionAlg(t),
http.DefaultClient,
tt.fields.opts...,
)
require.NoError(t, err)
+1 -1
View File
@@ -87,7 +87,7 @@ func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDToke
}
logging.Debug("begin signature validation")
keySet := rp.NewRemoteKeySet(http.DefaultClient, s.Provider.keysEndpoint)
keySet := rp.NewRemoteKeySet(s.httpClient, s.keysEndpoint)
if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil {
return nil, fmt.Errorf("%w: invalid signature: %v", ErrInvalidToken, err)
}
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"net/http"
"testing"
"time"
@@ -378,6 +379,7 @@ func TestSession_FetchUser(t *testing.T) {
tt.fields.headerName,
tt.fields.audience,
tt.fields.encryptionAlg(t),
http.DefaultClient,
)
require.NoError(t, err)
+3 -1
View File
@@ -2,6 +2,7 @@ package oauth
import (
"context"
"net/http"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
@@ -65,12 +66,13 @@ func WithRelyingPartyOption(option rp.Option) ProviderOpts {
}
// New creates a generic OAuth 2.0 provider
func New(config *oauth2.Config, name, userEndpoint string, user func() idp.User, options ...ProviderOpts) (provider *Provider, err error) {
func New(config *oauth2.Config, name, userEndpoint string, user func() idp.User, httpClient *http.Client, options ...ProviderOpts) (provider *Provider, err error) {
provider = &Provider{
name: name,
userEndpoint: userEndpoint,
user: user,
generateVerifier: oauth2.GenerateVerifier,
options: []rp.Option{rp.WithHTTPClient(httpClient)},
}
for _, option := range options {
option(provider)
+3 -2
View File
@@ -2,6 +2,7 @@ package oauth
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -70,7 +71,7 @@ func TestProvider_BeginAuth(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, tt.fields.options...)
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, http.DefaultClient, tt.fields.options...)
r.NoError(err)
provider.generateVerifier = func() string {
return "pkceOAuthVerifier"
@@ -170,7 +171,7 @@ func TestProvider_Options(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, tt.fields.options...)
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, http.DefaultClient, tt.fields.options...)
require.NoError(t, err)
a.Equal(tt.want.name, provider.Name())
+1 -1
View File
@@ -238,7 +238,7 @@ func TestProvider_FetchUser(t *testing.T) {
tt.fields.httpMock("https://oauth2.com")
a := assert.New(t)
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper)
provider, err := New(tt.fields.config, tt.fields.name, tt.fields.userEndpoint, tt.fields.userMapper, http.DefaultClient)
require.NoError(t, err)
session := &Session{
+3 -1
View File
@@ -2,6 +2,7 @@ package oidc
import (
"context"
"net/http"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
@@ -101,11 +102,12 @@ var DefaultMapper UserInfoMapper = func(info *oidc.UserInfo) idp.User {
}
// New creates a generic OIDC provider
func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, userInfoMapper UserInfoMapper, options ...ProviderOpts) (provider *Provider, err error) {
func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, userInfoMapper UserInfoMapper, client *http.Client, options ...ProviderOpts) (provider *Provider, err error) {
provider = &Provider{
name: name,
userInfoMapper: userInfoMapper,
generateVerifier: oauth2.GenerateVerifier,
options: []rp.Option{rp.WithHTTPClient(client)},
}
for _, option := range options {
option(provider)
+3 -2
View File
@@ -2,6 +2,7 @@ package oidc
import (
"context"
"net/http"
"testing"
"github.com/h2non/gock"
@@ -88,7 +89,7 @@ func TestProvider_BeginAuth(t *testing.T) {
a := assert.New(t)
r := require.New(t)
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...)
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, http.DefaultClient, tt.fields.opts...)
r.NoError(err)
provider.generateVerifier = func() string {
return "pkceOAuthVerifier"
@@ -208,7 +209,7 @@ func TestProvider_Options(t *testing.T) {
tt.fields.httpMock(tt.fields.issuer)
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...)
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, http.DefaultClient, tt.fields.opts...)
require.NoError(t, err)
a.Equal(tt.want.name, provider.Name())
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"net/http"
"testing"
"time"
@@ -325,7 +326,7 @@ func TestSession_FetchUser(t *testing.T) {
tt.fields.httpMock(tt.fields.issuer)
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.opts...)
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, http.DefaultClient, tt.opts...)
require.NoError(t, err)
session := &Session{
+3
View File
@@ -8,6 +8,7 @@ import (
"crypto/x509"
"encoding/xml"
"io"
"net/http"
"net/url"
"time"
@@ -156,6 +157,7 @@ func New(
metadata []byte,
certificate []byte,
key []byte,
httpClient *http.Client,
options ...ProviderOpts,
) (*Provider, error) {
entityDescriptor, err := ParseMetadata(metadata)
@@ -180,6 +182,7 @@ func New(
Certificate: keyPair.Leaf,
IDPMetadata: entityDescriptor,
SignRequest: false,
HTTPClient: httpClient,
}
provider := &Provider{
name: name,
+3 -1
View File
@@ -3,6 +3,7 @@ package saml
import (
"context"
"encoding/xml"
"net/http"
"net/url"
"testing"
"time"
@@ -107,6 +108,7 @@ func TestProvider_BeginAuth(t *testing.T) {
tt.fields.metadata,
tt.fields.certificate,
tt.fields.key,
http.DefaultClient,
tt.fields.options...,
)
require.NoError(t, err)
@@ -285,7 +287,7 @@ func TestProvider_Options(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, tt.fields.options...)
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, http.DefaultClient, tt.fields.options...)
if tt.want.err {
require.Error(t, err)
} else {
+1 -1
View File
@@ -370,7 +370,7 @@ func TestSession_FetchUser(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
a := assert.New(t)
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, tt.fields.options...)
provider, err := New(tt.fields.name, tt.fields.rootURL, tt.fields.metadata, tt.fields.certificate, tt.fields.key, http.DefaultClient, tt.fields.options...)
require.NoError(t, err)
session, err := NewSession(provider, tt.args.requestID, tt.args.request)
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type IPLookupFunc func(string) ([]builtin_net.IP, error)
// HostnameToIPList converts the input URL to a list [net.IP].
// HostnameToIPList converts the input hostname to a list [net.IP].
//
// Returns an internal error if the lookup fails.
// Returns an invalid argument if the lookup function is nil
@@ -38,7 +38,7 @@ func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel,
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
resp, err := cfg.Client.Do(req)
if err != nil {
return err
}
@@ -1,11 +1,13 @@
package set
import (
"net/http"
"net/url"
)
type Config struct {
CallURL string
Client *http.Client
}
func (w *Config) Validate() error {
@@ -48,7 +48,7 @@ func InitChannel(ctx context.Context, cfg Config) (channels.NotificationChannel,
req.Header.Set(SigningHeader, actions.ComputeSignatureHeader(time.Now(), []byte(payload), cfg.SigningKey))
}
resp, err := http.DefaultClient.Do(req)
resp, err := cfg.Client.Do(req)
if err != nil {
return err
}
@@ -10,6 +10,7 @@ type Config struct {
Method string
Headers http.Header
SigningKey string
Client *http.Client
}
func (w *Config) Validate() error {
@@ -3,6 +3,7 @@ package handlers
import (
"context"
"errors"
"net/http"
"time"
"github.com/riverqueue/river"
@@ -30,6 +31,7 @@ type BackChannelLogoutWorker struct {
config *BackChannelLogoutWorkerConfig
now nowFunc
idGenerator id.Generator
httpClient *http.Client
}
// Timeout implements the Timeout-function of [river.Worker].
@@ -97,7 +99,8 @@ func (w *BackChannelLogoutWorker) sendLogoutRequest(ctx context.Context, request
if err != nil {
return err
}
if err = types.SendSecurityTokenEvent(ctx, set.Config{CallURL: request.BackChannelLogoutURI}, w.channels, &LogoutTokenMessage{LogoutToken: token}, request.TriggeringEventType).WithoutTemplate(); err != nil {
backchannelNotifCfg := set.Config{CallURL: request.BackChannelLogoutURI, Client: w.httpClient}
if err = types.SendSecurityTokenEvent(ctx, backchannelNotifCfg, w.channels, &LogoutTokenMessage{LogoutToken: token}, request.TriggeringEventType).WithoutTemplate(); err != nil {
return err
}
return w.commands.BackChannelLogoutSent(ctx, request.SessionID, request.OIDCSessionID, request.Aggregate.InstanceID)
@@ -129,6 +132,7 @@ func NewBackChannelLogoutWorker(
config *BackChannelLogoutWorkerConfig,
idGenerator id.Generator,
httpClient *http.Client,
) *BackChannelLogoutWorker {
return &BackChannelLogoutWorker{
commands: commands,
@@ -139,6 +143,7 @@ func NewBackChannelLogoutWorker(
config: config,
now: time.Now,
idGenerator: idGenerator,
httpClient: httpClient,
}
}
@@ -439,6 +439,7 @@ func newBackChannelLogoutWorker(queries Queries, commands Commands, es *eventsto
nil,
nil,
nil,
nil,
),
eventstore: es,
queue: queue,
@@ -36,6 +36,7 @@ func (n *NotificationQueries) GetActiveEmailConfig(ctx context.Context) (*email.
Method: http.MethodPost,
Headers: nil,
SigningKey: config.HTTPConfig.SigningKey,
Client: n.httpClient,
},
}, nil
}
@@ -1,6 +1,8 @@
package handlers
import (
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -11,33 +13,41 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/notification/channels/email"
"github.com/zitadel/zitadel/internal/notification/channels/smtp"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
"github.com/zitadel/zitadel/internal/notification/handlers/mock"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
t.Parallel()
const instId = "instance-1"
const cryptAlg = "alg1"
const keyId = "key1"
const pwd = "password"
const clientSecret = "client-secret"
encryptedPwd := []byte("password-encrypted")
cryptAlgMock := crypto.NewMockEncryptionAlgorithm(ctrl)
cryptAlgMock.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(pwd, nil)
cryptAlgMock.EXPECT().Algorithm().Return(cryptAlg)
cryptAlgMock.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
encryptedClientSecret := []byte("client-secret-encrypted")
var (
errDecryptionFailed = errors.New("decryption failed")
errDatabaseError = errors.New("database error")
)
tt := []struct {
smtpConfig *query.SMTPConfig
expected *email.Config
name string
smtpConfig *query.SMTPConfig
smtpConfigErr error
setupCrypto func(m *crypto.MockEncryptionAlgorithm)
expected *email.Config
expectedErr error
}{
{
name: "smtp config with plain auth (user and password)",
smtpConfig: &query.SMTPConfig{
ID: "smtp1",
Description: "smtp config with authentication (user and password)",
Description: "smtp config with plain auth (user and password)",
SMTPConfig: &query.SMTP{
TLS: true,
SenderAddress: "[email protected]",
@@ -55,10 +65,15 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(pwd, nil)
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp1",
Description: "smtp config with authentication (user and password)",
Description: "smtp config with plain auth (user and password)",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
@@ -76,9 +91,10 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
},
},
{
name: "smtp config with user but no plain auth config (fallback to plain auth without password)",
smtpConfig: &query.SMTPConfig{
ID: "smtp2",
Description: "smtp config with authentication (user and no password)",
Description: "smtp config with user but no plain auth config (fallback to plain auth without password)",
SMTPConfig: &query.SMTP{
TLS: true,
SenderAddress: "[email protected]",
@@ -91,7 +107,7 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp2",
Description: "smtp config with authentication (user and no password)",
Description: "smtp config with user but no plain auth config (fallback to plain auth without password)",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
@@ -108,6 +124,7 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
},
},
{
name: "smtp config without authentication",
smtpConfig: &query.SMTPConfig{
ID: "smtp3",
Description: "smtp config without authentication",
@@ -135,18 +152,205 @@ func TestNotificationQueries_GetActiveEmailConfig(t *testing.T) {
},
},
},
{
name: "smtp config with xoauth2 auth and client credentials",
smtpConfig: &query.SMTPConfig{
ID: "smtp4",
Description: "smtp config with xoauth2 auth and client credentials",
SMTPConfig: &query.SMTP{
SenderAddress: "[email protected]",
SenderName: "sender",
Host: "mail.com",
User: "mail-user",
XOAuth2Auth: &query.XOAuth2Auth{
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"https://mail.example.com/.default"},
ClientCredentials: &query.XOAuthClientCredentials{
ClientId: "client-id",
ClientSecret: &crypto.CryptoValue{
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedClientSecret,
},
},
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(clientSecret, nil)
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp4",
Description: "smtp config with xoauth2 auth and client credentials",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "mail.com",
XOAuth2Auth: &smtp.XOAuth2AuthConfig{
User: "mail-user",
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"https://mail.example.com/.default"},
ClientCredentialsAuth: &smtp.OAuth2ClientCredentials{
ClientId: "client-id",
ClientSecret: clientSecret,
},
},
},
From: "[email protected]",
FromName: "sender",
},
},
},
{
name: "smtp config with xoauth2 auth without client credentials",
smtpConfig: &query.SMTPConfig{
ID: "smtp5",
Description: "smtp config with xoauth2 auth without client credentials",
SMTPConfig: &query.SMTP{
SenderAddress: "[email protected]",
SenderName: "sender",
Host: "mail.com",
User: "mail-user",
XOAuth2Auth: &query.XOAuth2Auth{
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"scope1"},
},
},
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "smtp5",
Description: "smtp config with xoauth2 auth without client credentials",
},
SMTPConfig: &smtp.Config{
SMTP: smtp.SMTP{
Host: "mail.com",
XOAuth2Auth: &smtp.XOAuth2AuthConfig{
User: "mail-user",
TokenEndpoint: "https://token.example.com/token",
Scopes: []string{"scope1"},
},
},
From: "[email protected]",
FromName: "sender",
},
},
},
{
name: "plain auth password decryption error",
smtpConfig: &query.SMTPConfig{
ID: "smtp6",
Description: "plain auth password decryption error",
SMTPConfig: &query.SMTP{
Host: "mail.com",
User: "mail-user",
PlainAuth: &query.PlainAuth{
Password: &crypto.CryptoValue{
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedPwd,
},
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
},
expectedErr: errDecryptionFailed,
},
{
name: "xoauth2 client credentials decryption error",
smtpConfig: &query.SMTPConfig{
ID: "smtp7",
Description: "xoauth2 client credentials decryption error",
SMTPConfig: &query.SMTP{
Host: "mail.com",
User: "mail-user",
XOAuth2Auth: &query.XOAuth2Auth{
TokenEndpoint: "https://token.example.com/token",
ClientCredentials: &query.XOAuthClientCredentials{
ClientId: "client-id",
ClientSecret: &crypto.CryptoValue{
CryptoType: 0,
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedClientSecret,
},
},
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
},
expectedErr: errDecryptionFailed,
},
{
name: "http webhook config",
smtpConfig: &query.SMTPConfig{
ID: "http1",
Description: "http webhook config",
HTTPConfig: &query.HTTP{
Endpoint: "https://example.com/notify",
SigningKey: "my-signing-key",
},
},
expected: &email.Config{
ProviderConfig: &email.Provider{
ID: "http1",
Description: "http webhook config",
},
WebhookConfig: &webhook.Config{
CallURL: "https://example.com/notify",
Method: http.MethodPost,
SigningKey: "my-signing-key",
},
},
},
{
name: "neither smtp nor http config set",
smtpConfig: &query.SMTPConfig{
ID: "smtp8",
Description: "neither smtp nor http config set",
},
expectedErr: zerrors.ThrowNotFound(nil, "", ""),
},
{
name: "SMTPConfigActive query error",
smtpConfigErr: errDatabaseError,
expectedErr: errDatabaseError,
},
}
for _, tc := range tt {
t.Run(tc.smtpConfig.Description, func(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext(instId, "org-1", "user-1")
queryMock := mock.NewMockQueries(ctrl)
queryMock.EXPECT().SMTPConfigActive(gomock.Any(), instId).Return(tc.smtpConfig, nil)
cryptAlgMock := crypto.NewMockEncryptionAlgorithm(ctrl)
if tc.setupCrypto != nil {
tc.setupCrypto(cryptAlgMock)
}
notificationQueries := NewNotificationQueries(queryMock, &eventstore.Eventstore{}, "ext domain", uint16(1234), false, "filepath", nil, cryptAlgMock, nil)
queryMock := mock.NewMockQueries(ctrl)
queryMock.EXPECT().SMTPConfigActive(gomock.Any(), instId).Return(tc.smtpConfig, tc.smtpConfigErr)
notificationQueries := NewNotificationQueries(queryMock, &eventstore.Eventstore{}, "ext domain", uint16(1234), false, "filepath", nil, cryptAlgMock, nil, nil)
cfg, err := notificationQueries.GetActiveEmailConfig(ctx)
assert.NoError(t, err)
assert.ErrorIs(t, err, tc.expectedErr)
if tc.expectedErr != nil {
return
}
assert.EqualValues(t, tc.expected, cfg)
})
}
@@ -49,6 +49,7 @@ func (n *NotificationQueries) GetActiveSMSConfig(ctx context.Context) (*sms.Conf
Method: http.MethodPost,
Headers: nil,
SigningKey: config.HTTPConfig.SigningKey,
Client: n.httpClient,
},
}, nil
}
@@ -0,0 +1,176 @@
package handlers
import (
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/notification/channels/sms"
"github.com/zitadel/zitadel/internal/notification/channels/twilio"
"github.com/zitadel/zitadel/internal/notification/channels/webhook"
"github.com/zitadel/zitadel/internal/notification/handlers/mock"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestNotificationQueries_GetActiveSMSConfig(t *testing.T) {
t.Parallel()
const instId = "instance-1"
const cryptAlg = "alg1"
const keyId = "key1"
const token = "twilio-token"
encryptedToken := []byte("twilio-token-encrypted")
var (
errDecryptionFailed = errors.New("decryption failed")
errDatabaseError = errors.New("database error")
)
tt := []struct {
name string
smsConfig *query.SMSConfig
smsConfigErr error
setupCrypto func(m *crypto.MockEncryptionAlgorithm)
expected *sms.Config
expectedErr error
}{
{
name: "twilio config with token",
smsConfig: &query.SMSConfig{
ID: "twilio1",
Description: "twilio config with token",
TwilioConfig: &query.Twilio{
SID: "sid-1",
SenderNumber: "+10005550100",
VerifyServiceSID: "verify-sid-1",
Token: &crypto.CryptoValue{
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedToken,
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return(token, nil)
},
expected: &sms.Config{
ProviderConfig: &sms.Provider{
ID: "twilio1",
Description: "twilio config with token",
},
TwilioConfig: &twilio.Config{
SID: "sid-1",
Token: token,
SenderNumber: "+10005550100",
VerifyServiceSID: "verify-sid-1",
},
},
},
{
name: "twilio config with nil token",
smsConfig: &query.SMSConfig{
ID: "twilio2",
Description: "twilio config with nil token",
TwilioConfig: &query.Twilio{
SID: "sid-2",
SenderNumber: "+10005550100",
},
},
expectedErr: zerrors.ThrowNotFound(nil, "", ""),
},
{
name: "twilio token decryption error",
smsConfig: &query.SMSConfig{
ID: "twilio3",
Description: "twilio token decryption error",
TwilioConfig: &query.Twilio{
SID: "sid-3",
SenderNumber: "+10005550100",
Token: &crypto.CryptoValue{
Algorithm: cryptAlg,
KeyID: keyId,
Crypted: encryptedToken,
},
},
},
setupCrypto: func(m *crypto.MockEncryptionAlgorithm) {
m.EXPECT().Algorithm().Return(cryptAlg)
m.EXPECT().DecryptionKeyIDs().Return([]string{keyId})
m.EXPECT().DecryptString(gomock.Any(), gomock.Any()).Return("", errDecryptionFailed)
},
expectedErr: errDecryptionFailed,
},
{
name: "http webhook config",
smsConfig: &query.SMSConfig{
ID: "http1",
Description: "http webhook config",
HTTPConfig: &query.HTTP{
Endpoint: "https://example.com/sms",
SigningKey: "my-signing-key",
},
},
expected: &sms.Config{
ProviderConfig: &sms.Provider{
ID: "http1",
Description: "http webhook config",
},
WebhookConfig: &webhook.Config{
CallURL: "https://example.com/sms",
Method: http.MethodPost,
Headers: nil,
SigningKey: "my-signing-key",
},
},
},
{
name: "neither twilio nor http config set",
smsConfig: &query.SMSConfig{
ID: "sms1",
Description: "neither twilio nor http config set",
},
expectedErr: zerrors.ThrowNotFound(nil, "", ""),
},
{
name: "SMSProviderConfigActive query error",
smsConfigErr: errDatabaseError,
expectedErr: errDatabaseError,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
ctx := authz.NewMockContext(instId, "org-1", "user-1")
cryptAlgMock := crypto.NewMockEncryptionAlgorithm(ctrl)
if tc.setupCrypto != nil {
tc.setupCrypto(cryptAlgMock)
}
queryMock := mock.NewMockQueries(ctrl)
queryMock.EXPECT().SMSProviderConfigActive(gomock.Any(), instId).Return(tc.smsConfig, tc.smsConfigErr)
notificationQueries := NewNotificationQueries(queryMock, &eventstore.Eventstore{}, "ext domain", uint16(1234), false, "filepath", nil, nil, cryptAlgMock, nil)
cfg, err := notificationQueries.GetActiveSMSConfig(ctx)
assert.ErrorIs(t, err, tc.expectedErr)
if tc.expectedErr != nil {
return
}
assert.EqualValues(t, tc.expected, cfg)
})
}
}
@@ -433,6 +433,7 @@ func newNotificationWorker(t *testing.T, ctrl *gomock.Controller, queries *mock.
f.userDataCrypto,
smtpAlg,
f.SMSTokenCrypto,
nil,
),
channels: &notificationChannels{
Chain: *senders.ChainChannels(channel),
@@ -2,6 +2,7 @@ package handlers
import (
"context"
"net/http"
"github.com/go-jose/go-jose/v4"
"golang.org/x/text/language"
@@ -44,6 +45,7 @@ type NotificationQueries struct {
UserDataCrypto crypto.EncryptionAlgorithm
SMTPPasswordCrypto crypto.EncryptionAlgorithm
SMSTokenCrypto crypto.EncryptionAlgorithm
httpClient *http.Client
}
func NewNotificationQueries(
@@ -56,6 +58,7 @@ func NewNotificationQueries(
userDataCrypto crypto.EncryptionAlgorithm,
smtpPasswordCrypto crypto.EncryptionAlgorithm,
smsTokenCrypto crypto.EncryptionAlgorithm,
httpClient *http.Client,
) *NotificationQueries {
return &NotificationQueries{
Queries: baseQueries,
@@ -67,5 +70,6 @@ func NewNotificationQueries(
UserDataCrypto: userDataCrypto,
SMTPPasswordCrypto: smtpPasswordCrypto,
SMSTokenCrypto: smsTokenCrypto,
httpClient: httpClient,
}
}
@@ -71,7 +71,7 @@ func (u *quotaNotifier) reduceNotificationDue(event eventstore.Event) (*handler.
if alreadyHandled {
return nil
}
err = types.SendJSON(ctx, webhook.Config{CallURL: e.CallURL, Method: http.MethodPost}, u.channels, e, e.Type()).WithoutTemplate()
err = types.SendJSON(ctx, webhook.Config{CallURL: e.CallURL, Method: http.MethodPost, Client: u.queries.httpClient}, u.channels, e, e.Type()).WithoutTemplate()
if err != nil {
return err
}
@@ -89,6 +89,7 @@ func (t *telemetryPusher) pushMilestone(ctx context.Context, e *milestone.Reache
CallURL: endpoint,
Method: http.MethodPost,
Headers: t.cfg.Headers,
Client: t.queries.httpClient,
},
t.channels,
&struct {
@@ -1972,6 +1972,7 @@ func newUserNotifierLegacy(t *testing.T, ctrl *gomock.Controller, queries *mock.
f.userDataCrypto,
smtpAlg,
f.SMSTokenCrypto,
nil,
),
otpEmailTmpl: func(origin *url.URL) string {
return origin.String() + defaultOTPEmailTemplate
@@ -1936,6 +1936,7 @@ func newUserNotifier(t *testing.T, ctrl *gomock.Controller, queries *mock.MockQu
f.userDataCrypto,
smtpAlg,
f.SMSTokenCrypto,
nil,
),
otpEmailTmpl: func(origin *url.URL) string {
return origin.String() + defaultOTPEmailTemplate
+13 -2
View File
@@ -3,6 +3,7 @@ package notification
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/zitadel/logging"
@@ -40,6 +41,7 @@ func Register(
fileSystemPath string,
userEncryption, smtpEncryption, smsEncryption crypto.EncryptionAlgorithm,
queue *queue.Queue,
httpClient *http.Client,
) {
if !notificationWorkerConfig.LegacyEnabled {
queue.ShouldStart()
@@ -48,7 +50,7 @@ func Register(
// make sure the slice does not contain old values
projections = nil
q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption)
q := handlers.NewNotificationQueries(queries, es, externalDomain, externalPort, externalSecure, fileSystemPath, userEncryption, smtpEncryption, smsEncryption, httpClient)
c := newChannels(q)
projections = append(projections, handlers.NewUserNotifier(ctx, projection.ApplyCustomConfig(userHandlerCustomConfig), commands, q, c, otpEmailTmpl, notificationWorkerConfig, queue))
projections = append(projections, handlers.NewQuotaNotifier(ctx, projection.ApplyCustomConfig(quotaHandlerCustomConfig), commands, q, c))
@@ -59,7 +61,16 @@ func Register(
queue,
backChannelLogoutWorkerConfig.MaxAttempts,
))
queue.AddWorkers(ctx, handlers.NewBackChannelLogoutWorker(commands, q, es, queue, c, backChannelLogoutWorkerConfig, id.SonyFlakeGenerator()))
queue.AddWorkers(ctx, handlers.NewBackChannelLogoutWorker(
commands,
q,
es,
queue,
c,
backChannelLogoutWorkerConfig,
id.SonyFlakeGenerator(),
httpClient,
))
if telemetryCfg.Enabled {
projections = append(projections, handlers.NewTelemetryPusher(ctx, telemetryCfg, projection.ApplyCustomConfig(telemetryHandlerCustomConfig), commands, q, c))
}