Files
zitadel/internal/api/saml/provider.go
T
8e82ec1cb9 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]>
2026-06-15 15:36:14 +02:00

166 lines
4.7 KiB
Go

package saml
import (
"context"
"fmt"
"net/http"
"github.com/zitadel/saml/pkg/provider"
"github.com/zitadel/zitadel/backend/v3/instrumentation/metrics"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/auth/repository"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/crdb"
"github.com/zitadel/zitadel/internal/query"
)
const (
HandlerPrefix = "/saml/v2"
)
type Config struct {
ProviderConfig *provider.Config
DefaultLoginURLV2 string
}
type Provider struct {
*provider.Provider
command *command.Commands
}
func NewProvider(
conf Config,
externalSecure bool,
command *command.Commands,
query *query.Queries,
repo repository.Repository,
encAlg crypto.EncryptionAlgorithm,
certEncAlg crypto.EncryptionAlgorithm,
targetEncAlg crypto.EncryptionAlgorithm,
es *eventstore.Eventstore,
projections *database.DB,
instanceHandler,
userAgentCookie func(http.Handler) http.Handler,
accessHandler *middleware.AccessInterceptor,
httpClient *http.Client,
) (*Provider, error) {
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
provStorage, err := newStorage(
command,
query,
repo,
encAlg,
certEncAlg,
targetEncAlg,
es,
projections,
fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
conf.DefaultLoginURLV2,
ContextToIssuer,
httpClient,
)
if err != nil {
return nil, err
}
options := []provider.Option{
provider.WithHttpInterceptors(
middleware.CallDurationHandler,
middleware.RequestDetailsHandler(),
middleware.MetricsHandler(metricTypes),
middleware.TraceHandler(),
middleware.LogHandler("saml"),
middleware.NoCacheInterceptor().Handler,
instanceHandler,
userAgentCookie,
accessHandler.HandleWithPublicAuthPathPrefixes(publicAuthPathPrefixes(conf.ProviderConfig)),
http_utils.CopyHeadersToContext,
middleware.ActivityHandler,
),
provider.WithCustomTimeFormat("2006-01-02T15:04:05.999Z"),
}
if !externalSecure {
options = append(options, provider.WithAllowInsecure())
}
p, err := provider.NewProvider(
provStorage,
IssuerFromContext,
conf.ProviderConfig,
options...,
)
if err != nil {
return nil, err
}
return &Provider{
p,
command,
}, nil
}
func ContextToIssuer(ctx context.Context) string {
return http_utils.DomainContext(ctx).Origin() + HandlerPrefix
}
func IssuerFromContext(_ bool) (provider.IssuerFromRequest, error) {
return func(r *http.Request) string {
return ContextToIssuer(r.Context())
}, nil
}
func newStorage(
command *command.Commands,
query *query.Queries,
repo repository.Repository,
encAlg crypto.EncryptionAlgorithm,
certEncAlg crypto.EncryptionAlgorithm,
targetEncAlg crypto.EncryptionAlgorithm,
es *eventstore.Eventstore,
db *database.DB,
defaultLoginURL string,
defaultLoginURLV2 string,
contextToIssuer func(context.Context) string,
actionHttpClient *http.Client,
) (*Storage, error) {
return &Storage{
encAlg: encAlg,
certEncAlg: certEncAlg,
targetEncAlg: targetEncAlg,
locker: crdb.NewLocker(db.DB, locksTable, signingKey),
eventstore: es,
repo: repo,
command: command,
query: query,
defaultLoginURL: defaultLoginURL,
defaultLoginURLv2: defaultLoginURLV2,
contextToIssuer: contextToIssuer,
httpClient: actionHttpClient,
}, nil
}
func publicAuthPathPrefixes(config *provider.Config) []string {
metadataEndpoint := HandlerPrefix + provider.DefaultMetadataEndpoint
certificateEndpoint := HandlerPrefix + provider.DefaultCertificateEndpoint
ssoEndpoint := HandlerPrefix + provider.DefaultSingleSignOnEndpoint
if config.MetadataConfig != nil && config.MetadataConfig.Path != "" {
metadataEndpoint = HandlerPrefix + config.MetadataConfig.Path
}
if config.IDPConfig == nil || config.IDPConfig.Endpoints == nil {
return []string{metadataEndpoint, certificateEndpoint, ssoEndpoint}
}
if config.IDPConfig.Endpoints.Certificate != nil && config.IDPConfig.Endpoints.Certificate.Relative() != "" {
certificateEndpoint = HandlerPrefix + config.IDPConfig.Endpoints.Certificate.Relative()
}
if config.IDPConfig.Endpoints.SingleSignOn != nil && config.IDPConfig.Endpoints.SingleSignOn.Relative() != "" {
ssoEndpoint = HandlerPrefix + config.IDPConfig.Endpoints.SingleSignOn.Relative()
}
return []string{metadataEndpoint, certificateEndpoint, ssoEndpoint}
}