From 75ec73ca4ab429beb75c8fb9d54d3a2ef95276e7 Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Mon, 25 Apr 2022 10:01:17 +0200 Subject: [PATCH] feat: dynamic issuer (#3481) * feat: dynamic issuer * dynamic domain handling * key rotation durations * feat: dynamic issuer * make webauthn displayname dynamic --- cmd/admin/setup/03.go | 4 +- cmd/admin/start/config.go | 6 +- cmd/admin/start/start.go | 31 +-- cmd/defaults.yaml | 2 + go.mod | 4 +- go.sum | 11 +- internal/actions/context.go | 2 +- internal/api/grpc/management/user.go | 2 +- internal/api/http/cookie.go | 28 +-- internal/api/http/middleware/csp.go | 21 +- .../api/http/middleware/security_headers.go | 2 +- .../api/http/middleware/user_agent_cookie.go | 13 +- internal/api/http/origin.go | 6 +- internal/api/oidc/auth_request.go | 4 +- internal/api/oidc/auth_request_converter.go | 4 +- internal/api/oidc/client.go | 6 +- internal/api/oidc/client_converter.go | 4 +- internal/api/oidc/key.go | 215 ++++++++++-------- internal/api/oidc/op.go | 61 +++-- internal/api/ui/console/console.go | 20 +- internal/api/ui/login/custom_action.go | 4 +- .../api/ui/login/external_login_handler.go | 4 +- .../api/ui/login/external_register_handler.go | 4 +- internal/api/ui/login/jwt_handler.go | 4 +- internal/api/ui/login/login.go | 30 +-- .../api/ui/login/login_success_handler.go | 4 +- internal/command/command.go | 10 +- internal/command/instance.go | 12 +- internal/command/instance_domain.go | 33 +-- internal/command/key_pair.go | 13 +- internal/command/key_pair_model.go | 5 +- internal/command/project_application_oidc.go | 14 ++ .../command/user_human_refresh_token_test.go | 2 +- internal/command/user_human_webauthn.go | 8 +- .../config/systemdefaults/system_defaults.go | 8 +- internal/i18n/i18n.go | 12 +- internal/query/instance.go | 49 +++- internal/query/projection/key.go | 9 +- internal/query/projection/projection.go | 4 +- internal/query/query.go | 4 +- internal/webauthn/webauthn.go | 72 +++--- 41 files changed, 403 insertions(+), 348 deletions(-) diff --git a/cmd/admin/setup/03.go b/cmd/admin/setup/03.go index 248f861755..fc8660986f 100644 --- a/cmd/admin/setup/03.go +++ b/cmd/admin/setup/03.go @@ -11,7 +11,6 @@ import ( "github.com/caos/zitadel/internal/crypto" crypto_db "github.com/caos/zitadel/internal/crypto/database" "github.com/caos/zitadel/internal/eventstore" - webauthn_helper "github.com/caos/zitadel/internal/webauthn" ) type DefaultInstance struct { @@ -47,8 +46,7 @@ func (mig *DefaultInstance) Execute(ctx context.Context) error { mig.zitadelRoles, nil, nil, - //TODO: Livio will fix this, but it ZITADEL doesn't run without this - webauthn_helper.Config{DisplayName: "HELLO LIVIO", ID: "RPID"}, + nil, nil, nil, nil, diff --git a/cmd/admin/start/config.go b/cmd/admin/start/config.go index 63e8c6e045..2109984f78 100644 --- a/cmd/admin/start/config.go +++ b/cmd/admin/start/config.go @@ -2,11 +2,12 @@ package start import ( "github.com/caos/logging" - "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/config/hook" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" + "github.com/caos/zitadel/internal/command" + "github.com/caos/zitadel/internal/config/hook" + admin_es "github.com/caos/zitadel/internal/admin/repository/eventsourcing" internal_authz "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/http/middleware" @@ -31,6 +32,7 @@ type Config struct { ExternalSecure bool HTTP2HostHeader string HTTP1HostHeader string + WebAuthNName string Database database.Config Projections projection.Config AuthZ authz.Config diff --git a/cmd/admin/start/start.go b/cmd/admin/start/start.go index 8e9baa78be..0f156d03ce 100644 --- a/cmd/admin/start/start.go +++ b/cmd/admin/start/start.go @@ -13,8 +13,7 @@ import ( "time" "github.com/caos/logging" - "github.com/caos/oidc/pkg/op" - "github.com/caos/zitadel/internal/api/grpc/system" + "github.com/caos/oidc/v2/pkg/op" "github.com/gorilla/mux" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -29,6 +28,7 @@ import ( "github.com/caos/zitadel/internal/api/grpc/admin" "github.com/caos/zitadel/internal/api/grpc/auth" "github.com/caos/zitadel/internal/api/grpc/management" + "github.com/caos/zitadel/internal/api/grpc/system" http_util "github.com/caos/zitadel/internal/api/http" "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/api/oidc" @@ -49,10 +49,6 @@ import ( "github.com/caos/zitadel/openapi" ) -const ( - flagMasterKey = "masterkey" -) - func New() *cobra.Command { start := &cobra.Command{ Use: "start", @@ -78,7 +74,6 @@ Requirements: func startZitadel(config *Config, masterKey string) error { ctx := context.Background() - keyChan := make(chan interface{}) dbClient, err := database.Connect(config.Database) if err != nil { @@ -99,7 +94,7 @@ func startZitadel(config *Config, masterKey string) error { return fmt.Errorf("cannot start eventstore for queries: %w", err) } - queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, keys.OIDC, keyChan, config.InternalAuthZ.RolePermissionMappings) + queries, err := query.StartQueries(ctx, eventstoreClient, dbClient, config.Projections, keys.OIDC, config.InternalAuthZ.RolePermissionMappings) if err != nil { return fmt.Errorf("cannot start queries: %w", err) } @@ -113,10 +108,9 @@ func startZitadel(config *Config, masterKey string) error { if err != nil { return fmt.Errorf("cannot start asset storage client: %w", err) } - webAuthNConfig := webauthn.Config{ - ID: config.ExternalDomain, - Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), - DisplayName: "ZITADEL", + webAuthNConfig := &webauthn.Config{ + DisplayName: config.WebAuthNName, + ExternalSecure: config.ExternalSecure, } commands, err := command.StartCommands(eventstoreClient, config.SystemDefaults, config.InternalAuthZ.RolePermissionMappings, storage, authZRepo, webAuthNConfig, keys.IDPConfig, keys.OTP, keys.SMTP, keys.SMS, keys.User, keys.DomainVerification, keys.OIDC) if err != nil { @@ -126,14 +120,14 @@ func startZitadel(config *Config, masterKey string) error { notification.Start(config.Notification, config.SystemDefaults, commands, queries, dbClient, assets.HandlerPrefix, keys.User, keys.SMTP, keys.SMS) router := mux.NewRouter() - err = startAPIs(ctx, router, commands, queries, eventstoreClient, dbClient, keyChan, config, storage, authZRepo, keys) + err = startAPIs(ctx, router, commands, queries, eventstoreClient, dbClient, config, storage, authZRepo, keys) if err != nil { return err } return listen(ctx, router, config.Port) } -func startAPIs(ctx context.Context, router *mux.Router, commands *command.Commands, queries *query.Queries, eventstore *eventstore.Eventstore, dbClient *sql.DB, keyChan chan interface{}, config *Config, store static.Storage, authZRepo authz_repo.Repository, keys *encryptionKeys) error { +func startAPIs(ctx context.Context, router *mux.Router, commands *command.Commands, queries *query.Queries, eventstore *eventstore.Eventstore, dbClient *sql.DB, config *Config, store static.Storage, authZRepo authz_repo.Repository, keys *encryptionKeys) error { repo := struct { authz_repo.Repository *query.Queries @@ -168,13 +162,12 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader) authenticatedAPIs.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries, instanceInterceptor.Handler)) - userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, config.ExternalDomain, id.SonyFlakeGenerator, config.ExternalSecure) + userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator, config.ExternalSecure) if err != nil { return err } - issuer := oidc.Issuer(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) - oidcProvider, err := oidc.NewProvider(ctx, config.OIDC, issuer, login.DefaultLoggedOutPath, commands, queries, authRepo, config.SystemDefaults.KeyConfig, keys.OIDC, keys.OIDCKey, eventstore, dbClient, keyChan, userAgentInterceptor, instanceInterceptor.Handler) + oidcProvider, err := oidc.NewProvider(ctx, config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler) if err != nil { return fmt.Errorf("unable to start oidc provider: %w", err) } @@ -187,13 +180,13 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman authenticatedAPIs.RegisterHandler(openapi.HandlerPrefix, openAPIHandler) baseURL := http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) - c, err := console.Start(config.Console, config.ExternalDomain, baseURL, issuer, instanceInterceptor.Handler) + c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, instanceInterceptor.Handler) if err != nil { return fmt.Errorf("unable to start console: %w", err) } authenticatedAPIs.RegisterHandler(console.HandlerPrefix, c) - l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, config.SystemDefaults, console.HandlerPrefix+"/", config.ExternalDomain, baseURL, op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, instanceInterceptor.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey) + l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, config.SystemDefaults, console.HandlerPrefix+"/", config.ExternalDomain, baseURL, op.AuthCallbackURL(oidcProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, keys.User, keys.IDPConfig, keys.CSRFCookieKey) if err != nil { return fmt.Errorf("unable to start login: %w", err) } diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 00cb1f9802..a76b2eb71f 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -10,6 +10,8 @@ ExternalSecure: true HTTP2HostHeader: ":authority" HTTP1HostHeader: "host" +WebAuthNName: ZITADEL + Database: Host: localhost Port: 26257 diff --git a/go.mod b/go.mod index b8fe605ddb..adcc1f1b3d 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/allegro/bigcache v1.2.1 github.com/boombuler/barcode v1.0.1 github.com/caos/logging v0.3.1 - github.com/caos/oidc v1.2.0 + github.com/caos/oidc/v2 v2.0.0-dynamic-issuer.1 github.com/cockroachdb/cockroach-go/v2 v2.2.4 github.com/dop251/goja v0.0.0-20211129110639-4739a1d10a51 github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d @@ -47,7 +47,7 @@ require ( github.com/sony/sonyflake v1.0.0 github.com/spf13/cobra v1.3.0 github.com/spf13/viper v1.10.1 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.27.0 diff --git a/go.sum b/go.sum index eab0e94df8..166e96decb 100644 --- a/go.sum +++ b/go.sum @@ -123,13 +123,10 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= github.com/caos/logging v0.3.1 h1:892AMeHs09D0e3ZcGB+QDRsZ5+2xtPAsAhOy8eKfztc= github.com/caos/logging v0.3.1/go.mod h1:B8QNS0WDmR2Keac52Fw+XN4ZJkzLDGrcRIPB2Ux4uRo= -github.com/caos/oidc v1.0.1 h1:8UHAPynCObwaqortppDtJFktjqLDLYSLidkNy0Num4o= -github.com/caos/oidc v1.0.1/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU= -github.com/caos/oidc v1.2.0 h1:dTy5bcT2WQbwPgytEZiG8SV1bCgHUXyDdaPDCNtRdEU= -github.com/caos/oidc v1.2.0/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU= +github.com/caos/oidc/v2 v2.0.0-dynamic-issuer.1 h1:xJuhRlhJf9huGX/+c/mEN2QaFiYlSMHd4QfsWkDoCGI= +github.com/caos/oidc/v2 v2.0.0-dynamic-issuer.1/go.mod h1:bdIIx0WCNjbGqpi7o4PifypVoeCAD3yTncL03rogNKw= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -895,8 +892,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 h1:1SWXcTphBQjYGWRRxLFIAR1LVtQEj4eR7xPtyeOVM/c= @@ -1192,7 +1190,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/actions/context.go b/internal/actions/context.go index 52a33cc911..3564bf4d97 100644 --- a/internal/actions/context.go +++ b/internal/actions/context.go @@ -3,7 +3,7 @@ package actions import ( "encoding/json" - "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/v2/pkg/oidc" ) type Context map[string]interface{} diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index e889262247..f2577ba7e0 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -5,7 +5,7 @@ import ( "time" "github.com/caos/logging" - "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/v2/pkg/oidc" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/durationpb" diff --git a/internal/api/http/cookie.go b/internal/api/http/cookie.go index fc645f27a1..134e67bbe6 100644 --- a/internal/api/http/cookie.go +++ b/internal/api/http/cookie.go @@ -2,6 +2,7 @@ package http import ( "net/http" + "strings" "github.com/gorilla/securecookie" @@ -20,7 +21,6 @@ type CookieHandler struct { sameSite http.SameSite path string maxAge int - domain string } func NewCookieHandler(opts ...CookieHandlerOpt) *CookieHandler { @@ -76,12 +76,6 @@ func WithMaxAge(maxAge int) CookieHandlerOpt { } } -func WithDomain(domain string) CookieHandlerOpt { - return func(c *CookieHandler) { - c.domain = domain - } -} - func SetCookiePrefix(name, domain, path string, secureOnly bool) string { if !secureOnly { return name @@ -101,7 +95,7 @@ func (c *CookieHandler) GetCookieValue(r *http.Request, name string) (string, er } func (c *CookieHandler) GetEncryptedCookieValue(r *http.Request, name string, value interface{}) error { - cookie, err := r.Cookie(SetCookiePrefix(name, c.domain, c.path, c.secureOnly)) + cookie, err := r.Cookie(SetCookiePrefix(name, r.Host, c.path, c.secureOnly)) if err != nil { return err } @@ -111,11 +105,11 @@ func (c *CookieHandler) GetEncryptedCookieValue(r *http.Request, name string, va return c.securecookie.Decode(name, cookie.Value, value) } -func (c *CookieHandler) SetCookie(w http.ResponseWriter, name string, value string) { - c.httpSet(w, name, value, c.maxAge) +func (c *CookieHandler) SetCookie(w http.ResponseWriter, name, domain, value string) { + c.httpSet(w, name, domain, value, c.maxAge) } -func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name string, value interface{}) error { +func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name, domain string, value interface{}) error { if c.securecookie == nil { return errors.ThrowInternal(nil, "HTTP-s2HUtx", "securecookie not configured") } @@ -123,19 +117,19 @@ func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name string, v if err != nil { return err } - c.httpSet(w, name, encoded, c.maxAge) + c.httpSet(w, name, domain, encoded, c.maxAge) return nil } -func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) { - c.httpSet(w, name, "", -1) +func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, r *http.Request, name string) { + c.httpSet(w, name, r.Host, "", -1) } -func (c *CookieHandler) httpSet(w http.ResponseWriter, name, value string, maxage int) { +func (c *CookieHandler) httpSet(w http.ResponseWriter, name, domain, value string, maxage int) { http.SetCookie(w, &http.Cookie{ - Name: SetCookiePrefix(name, c.domain, c.path, c.secureOnly), + Name: SetCookiePrefix(name, domain, c.path, c.secureOnly), Value: value, - Domain: c.domain, + Domain: strings.Split(domain, ":")[0], Path: c.path, MaxAge: maxage, HttpOnly: c.httpOnly, diff --git a/internal/api/http/middleware/csp.go b/internal/api/http/middleware/csp.go index 70658edd88..83f69f1a49 100644 --- a/internal/api/http/middleware/csp.go +++ b/internal/api/http/middleware/csp.go @@ -34,7 +34,7 @@ var ( } ) -func (csp *CSP) Value(nonce string) string { +func (csp *CSP) Value(nonce string, host string) string { valuesMap := csp.asMap() values := make([]string, 0, len(valuesMap)) @@ -43,7 +43,7 @@ func (csp *CSP) Value(nonce string) string { continue } - values = append(values, fmt.Sprintf("%v %v", k, v.String(nonce))) + values = append(values, fmt.Sprintf("%v %v", k, v.String(nonce, host))) } return strings.Join(values, ";") @@ -99,24 +99,33 @@ func (srcOpts CSPSourceOptions) AddHost(h ...string) CSPSourceOptions { return append(srcOpts, h...) } +func (srcOpts CSPSourceOptions) AddOwnHost() CSPSourceOptions { + return append(srcOpts, placeHolderHost) +} + func (srcOpts CSPSourceOptions) AddScheme(s ...string) CSPSourceOptions { return srcOpts.add(s, "%v:") } func (srcOpts CSPSourceOptions) AddNonce() CSPSourceOptions { - return append(srcOpts, "'nonce-%v'") + return append(srcOpts, fmt.Sprintf("'nonce-%s'", placeHolderNonce)) } +const ( + placeHolderNonce = "{{nonce}}" + placeHolderHost = "{{host}}" +) + func (srcOpts CSPSourceOptions) AddHash(alg, b64v string) CSPSourceOptions { return append(srcOpts, fmt.Sprintf("'%v-%v'", alg, b64v)) } -func (srcOpts CSPSourceOptions) String(nonce string) string { +func (srcOpts CSPSourceOptions) String(nonce, host string) string { value := strings.Join(srcOpts, " ") - if !strings.Contains(value, "%v") { + if !strings.Contains(value, placeHolderNonce) && !strings.Contains(value, placeHolderHost) { return value } - return fmt.Sprintf(value, nonce) + return strings.ReplaceAll(strings.ReplaceAll(value, placeHolderHost, host), placeHolderNonce, nonce) } func (srcOpts CSPSourceOptions) add(values []string, format string) CSPSourceOptions { diff --git a/internal/api/http/middleware/security_headers.go b/internal/api/http/middleware/security_headers.go index 268861ced6..c4f221fb65 100644 --- a/internal/api/http/middleware/security_headers.go +++ b/internal/api/http/middleware/security_headers.go @@ -63,7 +63,7 @@ func (h *headers) ServeHTTP(w http.ResponseWriter, r *http.Request) { r = saveContext(r, nonceKey, nonce) } headers := w.Header() - headers.Set(http_utils.ContentSecurityPolicy, h.csp.Value(nonce)) + headers.Set(http_utils.ContentSecurityPolicy, h.csp.Value(nonce, r.Host)) headers.Set(http_utils.XXSSProtection, "1; mode=block") headers.Set(http_utils.StrictTransportSecurity, "max-age=31536000; includeSubDomains") headers.Set(http_utils.XFrameOptions, "DENY") diff --git a/internal/api/http/middleware/user_agent_cookie.go b/internal/api/http/middleware/user_agent_cookie.go index 176de252df..2aa736fb4b 100644 --- a/internal/api/http/middleware/user_agent_cookie.go +++ b/internal/api/http/middleware/user_agent_cookie.go @@ -21,10 +21,6 @@ func UserAgentIDFromCtx(ctx context.Context) (string, bool) { return userAgentID, ok } -func InstanceIDFromCtx(ctx context.Context) string { - return "" //TODO: implement -} - type UserAgent struct { ID string } @@ -41,10 +37,9 @@ type UserAgentCookieConfig struct { MaxAge time.Duration } -func NewUserAgentHandler(config *UserAgentCookieConfig, cookieKey []byte, domain string, idGenerator id.Generator, externalSecure bool) (func(http.Handler) http.Handler, error) { +func NewUserAgentHandler(config *UserAgentCookieConfig, cookieKey []byte, idGenerator id.Generator, externalSecure bool) (func(http.Handler) http.Handler, error) { opts := []http_utils.CookieHandlerOpt{ http_utils.WithEncryption(cookieKey, cookieKey), - http_utils.WithDomain(domain), http_utils.WithMaxAge(int(config.MaxAge.Seconds())), } if !externalSecure { @@ -68,7 +63,7 @@ func (ua *userAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err == nil { ctx := context.WithValue(r.Context(), userAgentKey, agent.ID) r = r.WithContext(ctx) - ua.setUserAgent(w, agent) + ua.setUserAgent(w, r.Host, agent) } ua.nextHandler.ServeHTTP(w, r) } @@ -90,8 +85,8 @@ func (ua *userAgentHandler) getUserAgent(r *http.Request) (*UserAgent, error) { return userAgent, nil } -func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, agent *UserAgent) error { - err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, agent) +func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, host string, agent *UserAgent) error { + err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, host, agent) if err != nil { return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie") } diff --git a/internal/api/http/origin.go b/internal/api/http/origin.go index d0a5168f73..8b7130970e 100644 --- a/internal/api/http/origin.go +++ b/internal/api/http/origin.go @@ -32,9 +32,13 @@ func IsOrigin(rawOrigin string) bool { } func BuildHTTP(hostname string, externalPort uint16, secure bool) string { + return BuildOrigin(fmt.Sprintf("%s:%d", hostname, externalPort), secure) +} + +func BuildOrigin(host string, secure bool) string { schema := "https" if !secure { schema = "http" } - return fmt.Sprintf("%s://%s:%d", schema, hostname, externalPort) + return fmt.Sprintf("%s://%s", schema, host) } diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 53ed9860c4..aff1241f9c 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -6,8 +6,8 @@ import ( "time" "github.com/caos/logging" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/caos/oidc/v2/pkg/oidc" + "github.com/caos/oidc/v2/pkg/op" "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/http/middleware" diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 868f288b1e..ac19c454ea 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/caos/oidc/v2/pkg/oidc" + "github.com/caos/oidc/v2/pkg/op" "golang.org/x/text/language" "github.com/caos/zitadel/internal/api/authz" diff --git a/internal/api/oidc/client.go b/internal/api/oidc/client.go index 089e660e99..f471881f08 100644 --- a/internal/api/oidc/client.go +++ b/internal/api/oidc/client.go @@ -5,8 +5,8 @@ import ( "encoding/base64" "strings" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/caos/oidc/v2/pkg/oidc" + "github.com/caos/oidc/v2/pkg/op" "gopkg.in/square/go-jose.v2" "github.com/caos/zitadel/internal/api/authz" @@ -43,7 +43,7 @@ func (o *OPStorage) GetClientByClientID(ctx context.Context, id string) (_ op.Cl if err != nil { return nil, errors.ThrowInternal(err, "OIDC-mPxqP", "Errors.Internal") } - projectRoles, err := o.query.SearchProjectRoles(context.TODO(), &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) + projectRoles, err := o.query.SearchProjectRoles(ctx, &query.ProjectRoleSearchQueries{Queries: []query.SearchQuery{projectIDQuery}}) if err != nil { return nil, err } diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index 759d482b27..eb6d67c909 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -4,8 +4,8 @@ import ( "strings" "time" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/oidc/pkg/op" + "github.com/caos/oidc/v2/pkg/oidc" + "github.com/caos/oidc/v2/pkg/op" "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 3ed2a875dd..2d0e175efb 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -6,6 +6,7 @@ import ( "time" "github.com/caos/logging" + "github.com/caos/oidc/v2/pkg/op" "gopkg.in/square/go-jose.v2" "github.com/caos/zitadel/internal/api/authz" @@ -20,99 +21,121 @@ import ( const ( locksTable = "projections.locks" signingKey = "signing_key" + oidcUser = "OIDC" + + retryBackoff = 500 * time.Millisecond + retryCount = 3 + lockDuration = retryCount * retryBackoff * 5 + gracefulPeriod = 10 * time.Minute ) -func (o *OPStorage) GetKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err error) { +//SigningKey wraps the query.PrivateKey to implement the op.SigningKey interface +type SigningKey struct { + algorithm jose.SignatureAlgorithm + id string + key interface{} +} + +func (s *SigningKey) SignatureAlgorithm() jose.SignatureAlgorithm { + return s.algorithm +} + +func (s *SigningKey) Key() interface{} { + return s.key +} + +func (s *SigningKey) ID() string { + return s.id +} + +//PublicKey wraps the query.PublicKey to implement the op.Key interface +type PublicKey struct { + key query.PublicKey +} + +func (s *PublicKey) Algorithm() jose.SignatureAlgorithm { + return jose.SignatureAlgorithm(s.key.Algorithm()) +} + +func (s *PublicKey) Use() string { + return s.key.Use().String() +} + +func (s *PublicKey) Key() interface{} { + return s.key.Key() +} + +func (s *PublicKey) ID() string { + return s.key.ID() +} + +//KeySet implements the op.Storage interface +func (o *OPStorage) KeySet(ctx context.Context) (keys []op.Key, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - keys, err := o.query.ActivePublicKeys(ctx, time.Now()) + err = retry(func() error { + publicKeys, err := o.query.ActivePublicKeys(ctx, time.Now()) + if err != nil { + return err + } + keys = make([]op.Key, len(publicKeys.Keys)) + for i, key := range publicKeys.Keys { + keys[i] = &PublicKey{key} + } + return nil + }) + return keys, err +} + +//SignatureAlgorithms implements the op.Storage interface +func (o *OPStorage) SignatureAlgorithms(ctx context.Context) ([]jose.SignatureAlgorithm, error) { + key, err := o.SigningKey(ctx) if err != nil { return nil, err } - webKeys := make([]jose.JSONWebKey, len(keys.Keys)) - for i, key := range keys.Keys { - webKeys[i] = jose.JSONWebKey{ - KeyID: key.ID(), - Algorithm: key.Algorithm(), - Use: key.Use().String(), - Key: key.Key(), - } - } - return &jose.JSONWebKeySet{Keys: webKeys}, nil + return []jose.SignatureAlgorithm{key.SignatureAlgorithm()}, nil } -func (o *OPStorage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { - renewTimer := time.NewTimer(0) - go func() { - for { - select { - case <-ctx.Done(): - return - case <-o.keyChan: - if !renewTimer.Stop() { - <-renewTimer.C - } - checkAfter := o.resetTimer(renewTimer, true) - logging.Infof("requested next signing key check in %s", checkAfter) - case <-renewTimer.C: - o.getSigningKey(ctx, renewTimer, keyCh) - } +//SigningKey implements the op.Storage interface +func (o *OPStorage) SigningKey(ctx context.Context) (key op.SigningKey, err error) { + err = retry(func() error { + key, err = o.getSigningKey(ctx) + if err != nil { + return err } - }() + if key == nil { + return errors.ThrowInternal(nil, "test", "test") + } + return nil + }) + return key, err } -func (o *OPStorage) getSigningKey(ctx context.Context, renewTimer *time.Timer, keyCh chan<- jose.SigningKey) { - keys, err := o.query.ActivePrivateSigningKey(ctx, time.Now().Add(o.signingKeyGracefulPeriod)) +func (o *OPStorage) getSigningKey(ctx context.Context) (op.SigningKey, error) { + keys, err := o.query.ActivePrivateSigningKey(ctx, time.Now().Add(gracefulPeriod)) if err != nil { - checkAfter := o.resetTimer(renewTimer, true) - logging.Infof("next signing key check in %s", checkAfter) - return + return nil, err } - if len(keys.Keys) == 0 { - var sequence uint64 - if keys.LatestSequence != nil { - sequence = keys.LatestSequence.Sequence - } - o.refreshSigningKey(ctx, keyCh, o.signingKeyAlgorithm, sequence) - checkAfter := o.resetTimer(renewTimer, true) - logging.Infof("next signing key check in %s", checkAfter) - return + if len(keys.Keys) > 0 { + return o.privateKeyToSigningKey(selectSigningKey(keys.Keys)) } - err = o.exchangeSigningKey(selectSigningKey(keys.Keys), keyCh) - logging.OnError(err).Error("could not exchange signing key") - checkAfter := o.resetTimer(renewTimer, err != nil) - logging.Infof("next signing key check in %s", checkAfter) + var sequence uint64 + if keys.LatestSequence != nil { + sequence = keys.LatestSequence.Sequence + } + return nil, o.refreshSigningKey(ctx, o.signingKeyAlgorithm, sequence) } -func (o *OPStorage) resetTimer(timer *time.Timer, shortRefresh bool) (nextCheck time.Duration) { - nextCheck = o.signingKeyRotationCheck - defer func() { timer.Reset(nextCheck) }() - if shortRefresh || o.currentKey == nil { - return nextCheck - } - maxLifetime := time.Until(o.currentKey.Expiry()) - if maxLifetime < o.signingKeyGracefulPeriod+2*o.signingKeyRotationCheck { - return nextCheck - } - return maxLifetime - o.signingKeyGracefulPeriod - o.signingKeyRotationCheck -} - -func (o *OPStorage) refreshSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, algorithm string, sequence uint64) { - if o.currentKey != nil && o.currentKey.Expiry().Before(time.Now().UTC()) { - logging.Info("unset current signing key") - keyCh <- jose.SigningKey{} - } +func (o *OPStorage) refreshSigningKey(ctx context.Context, algorithm string, sequence uint64) error { ok, err := o.ensureIsLatestKey(ctx, sequence) - if err != nil { - logging.New().WithError(err).Error("could not ensure latest key") - return - } - if !ok { - logging.Warn("view not up to date, retrying later") - return + if err != nil || !ok { + return errors.ThrowInternal(err, "OIDC-ASfh3", "cannot ensure that projection is up to date") } err = o.lockAndGenerateSigningKeyPair(ctx, algorithm) - logging.OnError(err).Warn("could not create signing key") + if err != nil { + return errors.ThrowInternal(err, "OIDC-ADh31", "could not create signing key") + } + return errors.ThrowInternal(nil, "OIDC-Df1bh", "") } func (o *OPStorage) ensureIsLatestKey(ctx context.Context, sequence uint64) (bool, error) { @@ -123,29 +146,20 @@ func (o *OPStorage) ensureIsLatestKey(ctx context.Context, sequence uint64) (boo return sequence == maxSequence, nil } -func (o *OPStorage) exchangeSigningKey(key query.PrivateKey, keyCh chan<- jose.SigningKey) (err error) { - if o.currentKey != nil && o.currentKey.ID() == key.ID() { - logging.Info("no new signing key") - return nil - } +func (o *OPStorage) privateKeyToSigningKey(key query.PrivateKey) (_ op.SigningKey, err error) { keyData, err := crypto.Decrypt(key.Key(), o.encAlg) if err != nil { - return err + return nil, err } privateKey, err := crypto.BytesToPrivateKey(keyData) if err != nil { - return err + return nil, err } - keyCh <- jose.SigningKey{ - Algorithm: jose.SignatureAlgorithm(key.Algorithm()), - Key: jose.JSONWebKey{ - KeyID: key.ID(), - Key: privateKey, - }, - } - o.currentKey = key - logging.WithFields("keyID", key.ID()).Info("exchanged signing key") - return nil + return &SigningKey{ + algorithm: jose.SignatureAlgorithm(key.Algorithm()), + key: privateKey, + id: key.ID(), + }, nil } func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm string) error { @@ -154,7 +168,7 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm ctx, cancel := context.WithCancel(ctx) defer cancel() - errs := o.locker.Lock(ctx, o.signingKeyRotationCheck*2, authz.GetInstance(ctx).InstanceID()) + errs := o.locker.Lock(ctx, lockDuration, authz.GetInstance(ctx).InstanceID()) err, ok := <-errs if err != nil || !ok { if errors.IsErrorAlreadyExists(err) { @@ -164,13 +178,13 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm return err } - return o.command.GenerateSigningKeyPair(ctx, algorithm) + return o.command.GenerateSigningKeyPair(setOIDCCtx(ctx), algorithm) } func (o *OPStorage) getMaxKeySequence(ctx context.Context) (uint64, error) { return o.eventstore.LatestSequence(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsMaxSequence). - ResourceOwner("system"). //TODO: change with multi issuer + ResourceOwner(authz.GetInstance(ctx).InstanceID()). AddQuery(). AggregateTypes(keypair.AggregateType). Builder(), @@ -180,3 +194,18 @@ func (o *OPStorage) getMaxKeySequence(ctx context.Context) (uint64, error) { func selectSigningKey(keys []query.PrivateKey) query.PrivateKey { return keys[len(keys)-1] } + +func setOIDCCtx(ctx context.Context) context.Context { + return authz.SetCtxData(ctx, authz.CtxData{UserID: oidcUser, OrgID: authz.GetInstance(ctx).InstanceID()}) +} + +func retry(retryable func() error) (err error) { + for i := 0; i < retryCount; i++ { + time.Sleep(retryBackoff) + err = retryable() + if err == nil { + return nil + } + } + return err +} diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index c032177cb6..5e27ae3198 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -7,7 +7,7 @@ import ( "net/http" "time" - "github.com/caos/oidc/pkg/op" + "github.com/caos/oidc/v2/pkg/op" "github.com/rakyll/statik/fs" "golang.org/x/text/language" @@ -17,7 +17,6 @@ import ( "github.com/caos/zitadel/internal/api/ui/login" "github.com/caos/zitadel/internal/auth/repository" "github.com/caos/zitadel/internal/command" - "github.com/caos/zitadel/internal/config/systemdefaults" "github.com/caos/zitadel/internal/crypto" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -74,26 +73,23 @@ type OPStorage struct { defaultRefreshTokenIdleExpiration time.Duration defaultRefreshTokenExpiration time.Duration encAlg crypto.EncryptionAlgorithm - keyChan <-chan interface{} - currentKey query.PrivateKey - signingKeyRotationCheck time.Duration - signingKeyGracefulPeriod time.Duration locker crdb.Locker assetAPIPrefix string } -func NewProvider(ctx context.Context, config Config, issuer, defaultLogoutRedirectURI string, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, encryptionAlg crypto.EncryptionAlgorithm, cryptoKey []byte, es *eventstore.Eventstore, projections *sql.DB, keyChan <-chan interface{}, userAgentCookie, instanceHandler func(http.Handler) http.Handler) (op.OpenIDProvider, error) { - opConfig, err := createOPConfig(config, issuer, defaultLogoutRedirectURI, cryptoKey) +func NewProvider(ctx context.Context, config Config, defaultLogoutRedirectURI string, externalSecure bool, command *command.Commands, query *query.Queries, repo repository.Repository, encryptionAlg crypto.EncryptionAlgorithm, cryptoKey []byte, es *eventstore.Eventstore, projections *sql.DB, userAgentCookie, instanceHandler func(http.Handler) http.Handler) (op.OpenIDProvider, error) { + opConfig, err := createOPConfig(config, defaultLogoutRedirectURI, cryptoKey) if err != nil { return nil, caos_errs.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w") } - storage := newStorage(config, command, query, repo, keyConfig, encryptionAlg, es, projections, keyChan) - options, err := createOptions(config, userAgentCookie, instanceHandler) + storage := newStorage(config, command, query, repo, encryptionAlg, es, projections) + options, err := createOptions(config, externalSecure, userAgentCookie, instanceHandler) if err != nil { return nil, caos_errs.ThrowInternal(err, "OIDC-D3gq1", "cannot create options: %w") } - provider, err := op.NewOpenIDProvider( + provider, err := op.NewDynamicOpenIDProvider( ctx, + HandlerPrefix, opConfig, storage, options..., @@ -104,17 +100,12 @@ func NewProvider(ctx context.Context, config Config, issuer, defaultLogoutRedire return provider, nil } -func Issuer(domain string, port uint16, externalSecure bool) string { - return http_utils.BuildHTTP(domain, port, externalSecure) + HandlerPrefix -} - -func createOPConfig(config Config, issuer, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) { +func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) { supportedLanguages, err := getSupportedLanguages() if err != nil { return nil, err } opConfig := &op.Config{ - Issuer: issuer, DefaultLogoutRedirectURI: defaultLogoutRedirectURI, CodeMethodS256: config.CodeMethodS256, AuthMethodPost: config.AuthMethodPost, @@ -130,21 +121,26 @@ func createOPConfig(config Config, issuer, defaultLogoutRedirectURI string, cryp return opConfig, nil } -func createOptions(config Config, userAgentCookie, instanceHandler func(http.Handler) http.Handler) ([]op.Option, error) { +func createOptions(config Config, externalSecure bool, userAgentCookie, instanceHandler func(http.Handler) http.Handler) ([]op.Option, error) { metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount} - interceptor := op.WithHttpInterceptors( - middleware.MetricsHandler(metricTypes), - middleware.TelemetryHandler(), - middleware.NoCacheInterceptor, - instanceHandler, - userAgentCookie, - http_utils.CopyHeadersToContext, - ) - endpoints := customEndpoints(config.CustomEndpoints) - if len(endpoints) == 0 { - return []op.Option{interceptor}, nil + options := []op.Option{ + op.WithHttpInterceptors( + middleware.MetricsHandler(metricTypes), + middleware.TelemetryHandler(), + middleware.NoCacheInterceptor, + instanceHandler, + userAgentCookie, + http_utils.CopyHeadersToContext, + ), } - return append(endpoints, interceptor), nil + if !externalSecure { + options = append(options, op.WithAllowInsecure()) + } + endpoints := customEndpoints(config.CustomEndpoints) + if len(endpoints) != 0 { + options = append(options, endpoints...) + } + return options, nil } func customEndpoints(endpointConfig *EndpointConfig) []op.Option { @@ -176,7 +172,7 @@ func customEndpoints(endpointConfig *EndpointConfig) []op.Option { return options } -func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, projections *sql.DB, keyChan <-chan interface{}) *OPStorage { +func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, encAlg crypto.EncryptionAlgorithm, es *eventstore.Eventstore, projections *sql.DB) *OPStorage { return &OPStorage{ repo: repo, command: command, @@ -189,10 +185,7 @@ func newStorage(config Config, command *command.Commands, query *query.Queries, defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration, defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration, encAlg: encAlg, - signingKeyGracefulPeriod: keyConfig.SigningKeyGracefulPeriod, - signingKeyRotationCheck: keyConfig.SigningKeyRotationCheck, locker: crdb.NewLocker(projections, locksTable, signingKey), - keyChan: keyChan, assetAPIPrefix: assets.HandlerPrefix, } } diff --git a/internal/api/ui/console/console.go b/internal/api/ui/console/console.go index 0cc408e32a..5a59fae8fb 100644 --- a/internal/api/ui/console/console.go +++ b/internal/api/ui/console/console.go @@ -8,13 +8,13 @@ import ( "net/http" "os" "path" - "strings" "time" "github.com/caos/logging" + "github.com/caos/oidc/v2/pkg/op" "github.com/caos/zitadel/internal/api/authz" - + http_util "github.com/caos/zitadel/internal/api/http" "github.com/caos/zitadel/internal/api/http/middleware" ) @@ -59,7 +59,7 @@ func (i *spaHandler) Open(name string) (http.File, error) { return i.fileSystem.Open("/index.html") } -func Start(config Config, domain, url, issuer string, instanceHandler func(http.Handler) http.Handler) (http.Handler, error) { +func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, instanceHandler func(http.Handler) http.Handler) (http.Handler, error) { fSys, err := fs.Sub(static, "static") if err != nil { return nil, err @@ -70,7 +70,7 @@ func Start(config Config, domain, url, issuer string, instanceHandler func(http. config.LongCache.MaxAge, config.LongCache.SharedMaxAge, ) - security := middleware.SecurityHeaders(csp(domain), nil) + security := middleware.SecurityHeaders(csp(), nil) handler := &http.ServeMux{} handler.Handle("/", cache(security(http.FileServer(&spaHandler{http.FS(fSys)})))) @@ -80,7 +80,8 @@ func Start(config Config, domain, url, issuer string, instanceHandler func(http. http.Error(w, "empty instanceID", http.StatusInternalServerError) return } - environmentJSON, err := createEnvironmentJSON(url, issuer, instance.ConsoleClientID()) + url := http_util.BuildOrigin(r.Host, externalSecure) + environmentJSON, err := createEnvironmentJSON(url, issuer(r), instance.ConsoleClientID()) if err != nil { http.Error(w, fmt.Sprintf("unable to marshal env for console: %v", err), http.StatusInternalServerError) return @@ -91,15 +92,12 @@ func Start(config Config, domain, url, issuer string, instanceHandler func(http. return handler, nil } -func csp(zitadelDomain string) *middleware.CSP { - if !strings.HasPrefix(zitadelDomain, "*.") { - zitadelDomain = "*." + zitadelDomain - } +func csp() *middleware.CSP { csp := middleware.DefaultSCP csp.StyleSrc = csp.StyleSrc.AddInline() csp.ScriptSrc = csp.ScriptSrc.AddEval() - csp.ConnectSrc = csp.ConnectSrc.AddHost(zitadelDomain) - csp.ImgSrc = csp.ImgSrc.AddHost(zitadelDomain).AddScheme("blob") + csp.ConnectSrc = csp.ConnectSrc.AddOwnHost() + csp.ImgSrc = csp.ImgSrc.AddOwnHost().AddScheme("blob") return &csp } diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index de9a5e9485..93360a4003 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -3,10 +3,10 @@ package login import ( "context" - "github.com/caos/oidc/pkg/oidc" - "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/oidc/v2/pkg/oidc" "github.com/caos/zitadel/internal/actions" + "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/domain" iam_model "github.com/caos/zitadel/internal/iam/model" ) diff --git a/internal/api/ui/login/external_login_handler.go b/internal/api/ui/login/external_login_handler.go index 98d691a1e1..cef37b16ed 100644 --- a/internal/api/ui/login/external_login_handler.go +++ b/internal/api/ui/login/external_login_handler.go @@ -7,8 +7,8 @@ import ( "strings" "time" - "github.com/caos/oidc/pkg/client/rp" - "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/v2/pkg/client/rp" + "github.com/caos/oidc/v2/pkg/oidc" "golang.org/x/oauth2" http_mw "github.com/caos/zitadel/internal/api/http/middleware" diff --git a/internal/api/ui/login/external_register_handler.go b/internal/api/ui/login/external_register_handler.go index 06450ed7f3..50523e0eae 100644 --- a/internal/api/ui/login/external_register_handler.go +++ b/internal/api/ui/login/external_register_handler.go @@ -4,8 +4,8 @@ import ( "net/http" "strings" - "github.com/caos/oidc/pkg/client/rp" - "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/v2/pkg/client/rp" + "github.com/caos/oidc/v2/pkg/oidc" "golang.org/x/text/language" http_mw "github.com/caos/zitadel/internal/api/http/middleware" diff --git a/internal/api/ui/login/jwt_handler.go b/internal/api/ui/login/jwt_handler.go index d8acaa363e..a33db9a0b9 100644 --- a/internal/api/ui/login/jwt_handler.go +++ b/internal/api/ui/login/jwt_handler.go @@ -9,8 +9,8 @@ import ( "time" "github.com/caos/logging" - "github.com/caos/oidc/pkg/client/rp" - "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/v2/pkg/client/rp" + "github.com/caos/oidc/v2/pkg/oidc" http_util "github.com/caos/zitadel/internal/api/http" "github.com/caos/zitadel/internal/domain" diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go index 1ec5a72e3a..afba27591b 100644 --- a/internal/api/ui/login/login.go +++ b/internal/api/ui/login/login.go @@ -25,18 +25,17 @@ import ( ) type Login struct { - endpoint string - router http.Handler - renderer *Renderer - parser *form.Parser - command *command.Commands - query *query.Queries - staticStorage static.Storage - //staticCache cache.Cache //TODO: enable when storage is implemented again + endpoint string + router http.Handler + renderer *Renderer + parser *form.Parser + command *command.Commands + query *query.Queries + staticStorage static.Storage authRepo auth_repository.Repository baseURL string consolePath string - oidcAuthCallbackURL func(string) string + oidcAuthCallbackURL func(context.Context, string) string idpConfigAlg crypto.EncryptionAlgorithm userCodeAlg crypto.EncryptionAlgorithm iamDomain string @@ -46,7 +45,6 @@ type Config struct { LanguageCookieName string CSRFCookieName string Cache middleware.CacheConfig - //StaticCache cache_config.CacheConfig //TODO: enable when storage is implemented again } const ( @@ -64,9 +62,10 @@ func CreateLogin(config Config, consolePath, domain, baseURL string, - oidcAuthCallbackURL func(string) string, + oidcAuthCallbackURL func(context.Context, string) string, externalSecure bool, userAgentCookie, + issuerInterceptor, instanceHandler mux.MiddlewareFunc, userCodeAlg crypto.EncryptionAlgorithm, idpConfigAlg crypto.EncryptionAlgorithm, @@ -85,12 +84,6 @@ func CreateLogin(config Config, idpConfigAlg: idpConfigAlg, userCodeAlg: userCodeAlg, } - //TODO: enable when storage is implemented again - //login.staticCache, err = config.StaticCache.Config.NewCache() - //if err != nil { - // return nil, fmt.Errorf("unable to create storage cache: %w", err) - //} - statikFS, err := fs.NewWithNamespace("login") if err != nil { return nil, fmt.Errorf("unable to create filesystem: %w", err) @@ -105,7 +98,8 @@ func CreateLogin(config Config, return nil, fmt.Errorf("unable to create cacheInterceptor: %w", err) } security := middleware.SecurityHeaders(csp(), login.cspErrorHandler) - login.router = CreateRouter(login, statikFS, instanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, middleware.TelemetryHandler(EndpointResources)) + + login.router = CreateRouter(login, statikFS, instanceHandler, csrfInterceptor, cacheInterceptor, security, userAgentCookie, middleware.TelemetryHandler(EndpointResources), issuerInterceptor) login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName, systemDefaults.DefaultLanguage) login.parser = form.NewParser() return login, nil diff --git a/internal/api/ui/login/login_success_handler.go b/internal/api/ui/login/login_success_handler.go index d6bb858a9c..5e8e7f64a9 100644 --- a/internal/api/ui/login/login_success_handler.go +++ b/internal/api/ui/login/login_success_handler.go @@ -43,11 +43,11 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request, userData: l.getUserData(r, authReq, "Login Successful", errID, errMessage), } if authReq != nil { - data.RedirectURI = l.oidcAuthCallbackURL("") //the id will be set via the html (maybe change this with the login refactoring) + data.RedirectURI = l.oidcAuthCallbackURL(r.Context(), "") //the id will be set via the html (maybe change this with the login refactoring) } l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplLoginSuccess], data, nil) } func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { - http.Redirect(w, r, l.oidcAuthCallbackURL(authReq.ID), http.StatusFound) + http.Redirect(w, r, l.oidcAuthCallbackURL(r.Context(), authReq.ID), http.StatusFound) } diff --git a/internal/command/command.go b/internal/command/command.go index 26e3264008..3e16892109 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -42,7 +42,7 @@ type Commands struct { domainVerificationValidator func(domain, token, verifier string, checkType http.CheckType) error multifactors domain.MultifactorConfigs - webauthn *webauthn_helper.WebAuthN + webauthnConfig *webauthn_helper.Config keySize int keyAlgorithm crypto.EncryptionAlgorithm privateKeyLifetime time.Duration @@ -60,7 +60,7 @@ func StartCommands(es *eventstore.Eventstore, zitadelRoles []authz.RoleMapping, staticStore static.Storage, authZRepo authz_repo.Repository, - webAuthN webauthn_helper.Config, + webAuthN *webauthn_helper.Config, idpConfigEncryption, otpEncryption, smtpEncryption, @@ -84,6 +84,7 @@ func StartCommands(es *eventstore.Eventstore, userEncryption: userEncryption, domainVerificationAlg: domainVerificationEncryption, keyAlgorithm: oidcEncryption, + webauthnConfig: webAuthN, } instance_repo.RegisterEventMappers(repo.eventstore) @@ -107,11 +108,6 @@ func StartCommands(es *eventstore.Eventstore, repo.domainVerificationGenerator = crypto.NewEncryptionGenerator(defaults.DomainVerification.VerificationGenerator, repo.domainVerificationAlg) repo.domainVerificationValidator = http.ValidateDomain - web, err := webauthn_helper.StartServer(webAuthN) - if err != nil { - return nil, err - } - repo.webauthn = web repo.tokenVerifier = authZRepo return repo, nil diff --git a/internal/command/instance.go b/internal/command/instance.go index 3037e1dc65..6d27e4673f 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -28,16 +28,6 @@ const ( consolePostLogoutPath = console.HandlerPrefix + "/signedout" ) -type AddInstance struct { - InstanceName string - CustomDomain string - FirstOrgName string - OwnerEmail string - OwnerUsername string - OwnerFirstName string - OwnerLastName string -} - type InstanceSetup struct { zitadel ZitadelConfig InstanceName string @@ -301,7 +291,7 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup, exte } if setup.CustomDomain != "" { - validations = append(validations, c.addInstanceDomain(instanceAgg, setup.CustomDomain, false)) + validations = append(validations, addInstanceDomain(instanceAgg, setup.CustomDomain, false)) } console := &addOIDCApp{ diff --git a/internal/command/instance_domain.go b/internal/command/instance_domain.go index 686a0b2527..c562051d47 100644 --- a/internal/command/instance_domain.go +++ b/internal/command/instance_domain.go @@ -15,7 +15,7 @@ import ( func (c *Commands) AddInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - validation := c.addInstanceDomain(instanceAgg, instanceDomain, false) + validation := addInstanceDomain(instanceAgg, instanceDomain, false) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err @@ -33,7 +33,7 @@ func (c *Commands) AddInstanceDomain(ctx context.Context, instanceDomain string) func (c *Commands) SetPrimaryInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - validation := c.setPrimaryInstanceDomain(instanceAgg, instanceDomain) + validation := setPrimaryInstanceDomain(instanceAgg, instanceDomain) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err @@ -51,7 +51,7 @@ func (c *Commands) SetPrimaryInstanceDomain(ctx context.Context, instanceDomain func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain string) (*domain.ObjectDetails, error) { instanceAgg := instance.NewAggregate(authz.GetInstance(ctx).InstanceID()) - validation := c.removeInstanceDomain(instanceAgg, instanceDomain) + validation := removeInstanceDomain(instanceAgg, instanceDomain) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err @@ -69,16 +69,16 @@ func (c *Commands) RemoveInstanceDomain(ctx context.Context, instanceDomain stri func (c *Commands) addGeneratedInstanceDomain(a *instance.Aggregate, instanceName string) preparation.Validation { domain := domain.NewGeneratedInstanceDomain(instanceName, c.iamDomain) - return c.addInstanceDomain(a, domain, true) + return addInstanceDomain(a, domain, true) } -func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain string, generated bool) preparation.Validation { +func addInstanceDomain(a *instance.Aggregate, instanceDomain string, generated bool) preparation.Validation { return func() (preparation.CreateCommands, error) { if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" { return nil, errors.ThrowInvalidArgument(nil, "INST-28nlD", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain) + domainWriteModel, err := getInstanceDomainWriteModel(ctx, filter, instanceDomain) if err != nil { return nil, err } @@ -88,7 +88,7 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin events := []eventstore.Command{ instance.NewDomainAddedEvent(ctx, &a.Aggregate, instanceDomain, generated), } - appWriteModel, err := c.getOIDCAppWriteModel(ctx, authz.GetInstance(ctx).ProjectID(), authz.GetInstance(ctx).ConsoleApplicationID(), "") + appWriteModel, err := getOIDCAppWriteModel(ctx, filter, authz.GetInstance(ctx).ProjectID(), authz.GetInstance(ctx).ConsoleApplicationID(), "") if err != nil { return nil, err } @@ -115,13 +115,13 @@ func (c *Commands) addInstanceDomain(a *instance.Aggregate, instanceDomain strin } } -func (c *Commands) setPrimaryInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation { +func setPrimaryInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation { return func() (preparation.CreateCommands, error) { if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" { return nil, errors.ThrowInvalidArgument(nil, "INST-9mWjf", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain) + domainWriteModel, err := getInstanceDomainWriteModel(ctx, filter, instanceDomain) if err != nil { return nil, err } @@ -133,13 +133,13 @@ func (c *Commands) setPrimaryInstanceDomain(a *instance.Aggregate, instanceDomai } } -func (c *Commands) removeInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation { +func removeInstanceDomain(a *instance.Aggregate, instanceDomain string) preparation.Validation { return func() (preparation.CreateCommands, error) { if instanceDomain = strings.TrimSpace(instanceDomain); instanceDomain == "" { return nil, errors.ThrowInvalidArgument(nil, "INST-39nls", "Errors.Invalid.Argument") } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { - domainWriteModel, err := c.getInstanceDomainWriteModel(ctx, instanceDomain) + domainWriteModel, err := getInstanceDomainWriteModel(ctx, filter, instanceDomain) if err != nil { return nil, err } @@ -154,11 +154,16 @@ func (c *Commands) removeInstanceDomain(a *instance.Aggregate, instanceDomain st } } -func (c *Commands) getInstanceDomainWriteModel(ctx context.Context, domain string) (*InstanceDomainWriteModel, error) { +func getInstanceDomainWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, domain string) (*InstanceDomainWriteModel, error) { domainWriteModel := NewInstanceDomainWriteModel(ctx, domain) - err := c.eventstore.FilterToQueryReducer(ctx, domainWriteModel) + events, err := filter(ctx, domainWriteModel.Query()) if err != nil { return nil, err } - return domainWriteModel, nil + if len(events) == 0 { + return domainWriteModel, nil + } + domainWriteModel.AppendEvents(events...) + err = domainWriteModel.Reduce() + return domainWriteModel, err } diff --git a/internal/command/key_pair.go b/internal/command/key_pair.go index c41f3b467a..d253ffe4e6 100644 --- a/internal/command/key_pair.go +++ b/internal/command/key_pair.go @@ -10,12 +10,7 @@ import ( "github.com/caos/zitadel/internal/repository/keypair" ) -const ( - oidcUser = "OIDC" -) - func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string) error { - ctx = setOIDCCtx(ctx) privateCrypto, publicCrypto, err := crypto.GenerateEncryptedKeyPair(c.keySize, c.keyAlgorithm) if err != nil { return err @@ -28,8 +23,7 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string) privateKeyExp := time.Now().UTC().Add(c.privateKeyLifetime) publicKeyExp := time.Now().UTC().Add(c.publicKeyLifetime) - //TODO: InstanceID not available here? - keyPairWriteModel := NewKeyPairWriteModel(keyID, "system") //TODO: change with multi issuer + keyPairWriteModel := NewKeyPairWriteModel(keyID, authz.GetInstance(ctx).InstanceID()) keyAgg := KeyPairAggregateFromWriteModel(&keyPairWriteModel.WriteModel) _, err = c.eventstore.Push(ctx, keypair.NewAddedEvent( ctx, @@ -40,8 +34,3 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string) privateKeyExp, publicKeyExp)) return err } - -func setOIDCCtx(ctx context.Context) context.Context { - //TODO: InstanceID not available here? - return authz.SetCtxData(ctx, authz.CtxData{UserID: oidcUser, OrgID: authz.GetInstance(ctx).InstanceID()}) -} diff --git a/internal/command/key_pair_model.go b/internal/command/key_pair_model.go index d3abf22e8c..766069d1e6 100644 --- a/internal/command/key_pair_model.go +++ b/internal/command/key_pair_model.go @@ -3,8 +3,7 @@ package command import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/eventstore" - keypair "github.com/caos/zitadel/internal/repository/keypair" - "github.com/caos/zitadel/internal/repository/project" + "github.com/caos/zitadel/internal/repository/keypair" ) type KeyPairWriteModel struct { @@ -52,7 +51,7 @@ func (wm *KeyPairWriteModel) Query() *eventstore.SearchQueryBuilder { return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). ResourceOwner(wm.ResourceOwner). AddQuery(). - AggregateTypes(project.AggregateType). + AggregateTypes(keypair.AggregateType). AggregateIDs(wm.AggregateID). EventTypes(keypair.AddedEventType). Builder() diff --git a/internal/command/project_application_oidc.go b/internal/command/project_application_oidc.go index eada5bfcef..e8bb97a607 100644 --- a/internal/command/project_application_oidc.go +++ b/internal/command/project_application_oidc.go @@ -321,3 +321,17 @@ func (c *Commands) getOIDCAppWriteModel(ctx context.Context, projectID, appID, r } return appWriteModel, nil } + +func getOIDCAppWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, projectID, appID, resourceOwner string) (*OIDCApplicationWriteModel, error) { + appWriteModel := NewOIDCApplicationWriteModelWithAppID(projectID, appID, resourceOwner) + events, err := filter(ctx, appWriteModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return appWriteModel, nil + } + appWriteModel.AppendEvents(events...) + err = appWriteModel.Reduce() + return appWriteModel, err +} diff --git a/internal/command/user_human_refresh_token_test.go b/internal/command/user_human_refresh_token_test.go index c41b2af57e..9170246e90 100644 --- a/internal/command/user_human_refresh_token_test.go +++ b/internal/command/user_human_refresh_token_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/v2/pkg/oidc" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" diff --git a/internal/command/user_human_webauthn.go b/internal/command/user_human_webauthn.go index 827ef60842..659df9fe9d 100644 --- a/internal/command/user_human_webauthn.go +++ b/internal/command/user_human_webauthn.go @@ -157,7 +157,7 @@ func (c *Commands) addHumanWebAuthN(ctx context.Context, userID, resourceowner s if accountName == "" { accountName = user.EmailAddress } - webAuthN, err := c.webauthn.BeginRegistration(user, accountName, authenticatorPlatform, domain.UserVerificationRequirementDiscouraged, isLoginUI, tokens...) + webAuthN, err := c.webauthnConfig.BeginRegistration(ctx, user, accountName, authenticatorPlatform, domain.UserVerificationRequirementDiscouraged, isLoginUI, tokens...) if err != nil { return nil, nil, nil, err } @@ -272,7 +272,7 @@ func (c *Commands) verifyHumanWebAuthN(ctx context.Context, userID, resourceowne return nil, nil, nil, err } _, token := domain.GetTokenToVerify(tokens) - webAuthN, err := c.webauthn.FinishRegistration(user, token, tokenName, credentialData, userAgentID != "") + webAuthN, err := c.webauthnConfig.FinishRegistration(ctx, user, token, tokenName, credentialData, userAgentID != "") if err != nil { return nil, nil, nil, err } @@ -343,7 +343,7 @@ func (c *Commands) beginWebAuthNLogin(ctx context.Context, userID, resourceOwner if err != nil { return nil, nil, err } - webAuthNLogin, err := c.webauthn.BeginLogin(human, domain.UserVerificationRequirementDiscouraged, isLoginUI, tokens...) + webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, human, domain.UserVerificationRequirementDiscouraged, isLoginUI, tokens...) if err != nil { return nil, nil, err } @@ -454,7 +454,7 @@ func (c *Commands) finishWebAuthNLogin(ctx context.Context, userID, resourceOwne if err != nil { return nil, nil, 0, err } - keyID, signCount, err := c.webauthn.FinishLogin(human, webAuthN, credentialData, isLoginUI, tokens...) + keyID, signCount, err := c.webauthnConfig.FinishLogin(ctx, human, webAuthN, credentialData, isLoginUI, tokens...) if err != nil && keyID == nil { return nil, nil, 0, err } diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index 003aacd2c4..0edb425c2b 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -56,9 +56,7 @@ type Endpoints struct { } type KeyConfig struct { - Size int - PrivateKeyLifetime time.Duration - PublicKeyLifetime time.Duration - SigningKeyRotationCheck time.Duration - SigningKeyGracefulPeriod time.Duration + Size int + PrivateKeyLifetime time.Duration + PublicKeyLifetime time.Duration } diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index 0281322c27..6e6b72a9e1 100644 --- a/internal/i18n/i18n.go +++ b/internal/i18n/i18n.go @@ -145,8 +145,8 @@ func (t *Translator) Lang(r *http.Request) language.Tag { return tag } -func (t *Translator) SetLangCookie(w http.ResponseWriter, lang language.Tag) { - t.cookieHandler.SetCookie(w, t.cookieName, lang.String()) +func (t *Translator) SetLangCookie(w http.ResponseWriter, r *http.Request, lang language.Tag) { + t.cookieHandler.SetCookie(w, t.cookieName, r.Host, lang.String()) } func (t *Translator) localizerFromRequest(r *http.Request) *i18n.Localizer { @@ -177,7 +177,7 @@ func (t *Translator) langsFromCtx(ctx context.Context) []string { langs := t.preferredLanguages if ctx != nil { ctxData := authz.GetCtxData(ctx) - if ctxData.PreferredLanguage != "" { + if ctxData.PreferredLanguage != language.Und.String() { langs = append(langs, authz.GetCtxData(ctx).PreferredLanguage) } langs = append(langs, getAcceptLanguageHeader(ctx)) @@ -190,6 +190,10 @@ func (t *Translator) SetPreferredLanguages(langs ...string) { } func getAcceptLanguageHeader(ctx context.Context) string { + acceptLanguage := metautils.ExtractIncoming(ctx).Get("accept-language") + if acceptLanguage != "" { + return acceptLanguage + } return metautils.ExtractIncoming(ctx).Get("grpcgateway-accept-language") } @@ -199,7 +203,7 @@ func localize(localizer *i18n.Localizer, id string, args map[string]interface{}) TemplateData: args, }) if err != nil { - logging.LogWithFields("I18N-MsF5sx", "id", id, "args", args).WithError(err).Warnf("missing translation") + logging.WithFields("id", id, "args", args).WithError(err).Warnf("missing translation") return id } return s diff --git a/internal/query/instance.go b/internal/query/instance.go index 8e256a9814..52515144a8 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" errs "errors" + "strings" "time" sq "github.com/Masterminds/squirrel" @@ -163,9 +164,9 @@ func (q *Queries) Instance(ctx context.Context) (*Instance, error) { } func (q *Queries) InstanceByHost(ctx context.Context, host string) (authz.Instance, error) { - stmt, scan := prepareInstanceQuery(host) + stmt, scan := prepareInstanceDomainQuery(host) query, args, err := stmt.Where(sq.Eq{ - InstanceColumnID.identifier(): "system", //TODO: change column to domain when available + InstanceDomainDomainCol.identifier(): strings.Split(host, ":")[0], }).ToSql() if err != nil { return nil, errors.ThrowInternal(err, "QUERY-SAfg2", "Errors.Query.SQLStatement") @@ -279,3 +280,47 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instances, err }, nil } } + +func prepareInstanceDomainQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { + return sq.Select( + InstanceColumnID.identifier(), + InstanceColumnCreationDate.identifier(), + InstanceColumnChangeDate.identifier(), + InstanceColumnSequence.identifier(), + InstanceColumnGlobalOrgID.identifier(), + InstanceColumnProjectID.identifier(), + InstanceColumnConsoleID.identifier(), + InstanceColumnConsoleAppID.identifier(), + InstanceColumnSetupStarted.identifier(), + InstanceColumnSetupDone.identifier(), + InstanceColumnDefaultLanguage.identifier(), + ). + From(instanceTable.identifier()). + LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)). + PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*Instance, error) { + instance := &Instance{Host: host} + lang := "" + err := row.Scan( + &instance.ID, + &instance.CreationDate, + &instance.ChangeDate, + &instance.Sequence, + &instance.GlobalOrgID, + &instance.IAMProjectID, + &instance.ConsoleID, + &instance.ConsoleAppID, + &instance.SetupStarted, + &instance.SetupDone, + &lang, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-n0wng", "Errors.IAM.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-d9nw", "Errors.Internal") + } + instance.DefaultLanguage = language.Make(lang) + return instance, nil + } +} diff --git a/internal/query/projection/key.go b/internal/query/projection/key.go index 7d1e6e7c89..c1959ce35e 100644 --- a/internal/query/projection/key.go +++ b/internal/query/projection/key.go @@ -40,10 +40,9 @@ const ( type KeyProjection struct { crdb.StatementHandler encryptionAlgorithm crypto.EncryptionAlgorithm - keyChan chan<- interface{} } -func NewKeyProjection(ctx context.Context, config crdb.StatementHandlerConfig, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, keyChan chan<- interface{}) *KeyProjection { +func NewKeyProjection(ctx context.Context, config crdb.StatementHandlerConfig, keyEncryptionAlgorithm crypto.EncryptionAlgorithm) *KeyProjection { p := new(KeyProjection) config.ProjectionName = KeyProjectionTable config.Reducers = p.reducers() @@ -56,7 +55,7 @@ func NewKeyProjection(ctx context.Context, config crdb.StatementHandlerConfig, k crdb.NewColumn(KeyColumnInstanceID, crdb.ColumnTypeText), crdb.NewColumn(KeyColumnSequence, crdb.ColumnTypeInt64), crdb.NewColumn(KeyColumnAlgorithm, crdb.ColumnTypeText, crdb.Default("")), - crdb.NewColumn(KeyColumnUse, crdb.ColumnTypeText, crdb.Default("")), + crdb.NewColumn(KeyColumnUse, crdb.ColumnTypeEnum, crdb.Default(0)), }, crdb.NewPrimaryKey(KeyColumnInstanceID, KeyColumnID), crdb.WithConstraint(crdb.NewConstraint("id_unique", []string{KeyColumnID})), @@ -79,7 +78,6 @@ func NewKeyProjection(ctx context.Context, config crdb.StatementHandlerConfig, k ), ) p.StatementHandler = crdb.NewStatementHandler(ctx, config) - p.keyChan = keyChan p.encryptionAlgorithm = keyEncryptionAlgorithm return p @@ -130,9 +128,6 @@ func (p *KeyProjection) reduceKeyPairAdded(event eventstore.Event) (*handler.Sta }, crdb.WithTableSuffix(privateKeyTableSuffix), )) - if p.keyChan != nil { - p.keyChan <- true - } } if e.PublicKey.Expiry.After(time.Now()) { publicKey, err := crypto.Decrypt(e.PublicKey.Key, p.encryptionAlgorithm) diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index e5da1736e8..87e84cdb1e 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -17,7 +17,7 @@ const ( FailedEventsTable = "projections.failed_events" ) -func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, config Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, keyChan chan<- interface{}) error { +func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, config Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm) error { projectionConfig := crdb.StatementHandlerConfig{ ProjectionHandlerConfig: handler.ProjectionHandlerConfig{ HandlerConfig: handler.HandlerConfig{ @@ -74,7 +74,7 @@ func Start(ctx context.Context, sqlClient *sql.DB, es *eventstore.Eventstore, co NewSMSConfigProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["sms_config"])) NewOIDCSettingsProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["oidc_settings"])) NewDebugNotificationProviderProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["debug_notification_provider"])) - NewKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), keyEncryptionAlgorithm, keyChan) + NewKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), keyEncryptionAlgorithm) return nil } diff --git a/internal/query/query.go b/internal/query/query.go index 4ed9c2e1d4..597a8299bf 100644 --- a/internal/query/query.go +++ b/internal/query/query.go @@ -37,7 +37,7 @@ type Queries struct { zitadelRoles []authz.RoleMapping } -func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *sql.DB, projections projection.Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, keyChan chan<- interface{}, zitadelRoles []authz.RoleMapping) (repo *Queries, err error) { +func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *sql.DB, projections projection.Config, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, zitadelRoles []authz.RoleMapping) (repo *Queries, err error) { statikLoginFS, err := fs.NewWithNamespace("login") if err != nil { return nil, fmt.Errorf("unable to start login statik dir") @@ -66,7 +66,7 @@ func StartQueries(ctx context.Context, es *eventstore.Eventstore, sqlClient *sql keypair.RegisterEventMappers(repo.eventstore) usergrant.RegisterEventMappers(repo.eventstore) - err = projection.Start(ctx, sqlClient, es, projections, keyEncryptionAlgorithm, keyChan) + err = projection.Start(ctx, sqlClient, es, projections, keyEncryptionAlgorithm) if err != nil { return nil, err } diff --git a/internal/webauthn/webauthn.go b/internal/webauthn/webauthn.go index be70d8a719..3d28109785 100644 --- a/internal/webauthn/webauthn.go +++ b/internal/webauthn/webauthn.go @@ -2,38 +2,22 @@ package webauthn import ( "bytes" + "context" "encoding/json" "github.com/caos/logging" "github.com/duo-labs/webauthn/protocol" "github.com/duo-labs/webauthn/webauthn" + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/api/http" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" ) -type WebAuthN struct { - webAuthN *webauthn.WebAuthn -} - type Config struct { - ID string - Origin string - DisplayName string -} - -func StartServer(config Config) (*WebAuthN, error) { - webAuthN, err := webauthn.New(&webauthn.Config{ - RPDisplayName: config.DisplayName, - RPID: config.ID, - RPOrigin: config.Origin, - }) - if err != nil { - return nil, err - } - return &WebAuthN{ - webAuthN: webAuthN, - }, err + DisplayName string + ExternalSecure bool } type webUser struct { @@ -54,7 +38,10 @@ func (u *webUser) WebAuthnName() string { } func (u *webUser) WebAuthnDisplayName() string { - return u.DisplayName + if u.DisplayName != "" { + return u.DisplayName + } + return u.GetUsername() } func (u *webUser) WebAuthnIcon() string { @@ -65,7 +52,11 @@ func (u *webUser) WebAuthnCredentials() []webauthn.Credential { return u.credentials } -func (w *WebAuthN) BeginRegistration(user *domain.Human, accountName string, authType domain.AuthenticatorAttachment, userVerification domain.UserVerificationRequirement, isLoginUI bool, webAuthNs ...*domain.WebAuthNToken) (*domain.WebAuthNToken, error) { +func (w *Config) BeginRegistration(ctx context.Context, user *domain.Human, accountName string, authType domain.AuthenticatorAttachment, userVerification domain.UserVerificationRequirement, isLoginUI bool, webAuthNs ...*domain.WebAuthNToken) (*domain.WebAuthNToken, error) { + webAuthNServer, err := w.serverFromContext(ctx) + if err != nil { + return nil, err + } creds := WebAuthNsToCredentials(webAuthNs) existing := make([]protocol.CredentialDescriptor, len(creds)) for i, cred := range creds { @@ -74,7 +65,7 @@ func (w *WebAuthN) BeginRegistration(user *domain.Human, accountName string, aut CredentialID: cred.ID, } } - credentialOptions, sessionData, err := w.webAuthN.BeginRegistration( + credentialOptions, sessionData, err := webAuthNServer.BeginRegistration( &webUser{ Human: user, accountName: accountName, @@ -102,7 +93,7 @@ func (w *WebAuthN) BeginRegistration(user *domain.Human, accountName string, aut }, nil } -func (w *WebAuthN) FinishRegistration(user *domain.Human, webAuthN *domain.WebAuthNToken, tokenName string, credData []byte, isLoginUI bool) (*domain.WebAuthNToken, error) { +func (w *Config) FinishRegistration(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNToken, tokenName string, credData []byte, isLoginUI bool) (*domain.WebAuthNToken, error) { if webAuthN == nil { return nil, caos_errs.ThrowInternal(nil, "WEBAU-5M9so", "Errors.User.WebAuthN.NotFound") } @@ -113,7 +104,11 @@ func (w *WebAuthN) FinishRegistration(user *domain.Human, webAuthN *domain.WebAu return nil, caos_errs.ThrowInternal(err, "WEBAU-sEr8c", "Errors.User.WebAuthN.ErrorOnParseCredential") } sessionData := WebAuthNToSessionData(webAuthN) - credential, err := w.webAuthN.CreateCredential( + webAuthNServer, err := w.serverFromContext(ctx) + if err != nil { + return nil, err + } + credential, err := webAuthNServer.CreateCredential( &webUser{ Human: user, }, @@ -132,8 +127,12 @@ func (w *WebAuthN) FinishRegistration(user *domain.Human, webAuthN *domain.WebAu return webAuthN, nil } -func (w *WebAuthN) BeginLogin(user *domain.Human, userVerification domain.UserVerificationRequirement, isLoginUI bool, webAuthNs ...*domain.WebAuthNToken) (*domain.WebAuthNLogin, error) { - assertion, sessionData, err := w.webAuthN.BeginLogin(&webUser{ +func (w *Config) BeginLogin(ctx context.Context, user *domain.Human, userVerification domain.UserVerificationRequirement, isLoginUI bool, webAuthNs ...*domain.WebAuthNToken) (*domain.WebAuthNLogin, error) { + webAuthNServer, err := w.serverFromContext(ctx) + if err != nil { + return nil, err + } + assertion, sessionData, err := webAuthNServer.BeginLogin(&webUser{ Human: user, credentials: WebAuthNsToCredentials(webAuthNs), }, webauthn.WithUserVerification(UserVerificationFromDomain(userVerification))) @@ -152,7 +151,7 @@ func (w *WebAuthN) BeginLogin(user *domain.Human, userVerification domain.UserVe }, nil } -func (w *WebAuthN) FinishLogin(user *domain.Human, webAuthN *domain.WebAuthNLogin, credData []byte, isLoginUI bool, webAuthNs ...*domain.WebAuthNToken) ([]byte, uint32, error) { +func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNLogin, credData []byte, isLoginUI bool, webAuthNs ...*domain.WebAuthNToken) ([]byte, uint32, error) { assertionData, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(credData)) if err != nil { return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-ADgv4", "Errors.User.WebAuthN.ValidateLoginFailed") @@ -161,7 +160,11 @@ func (w *WebAuthN) FinishLogin(user *domain.Human, webAuthN *domain.WebAuthNLogi Human: user, credentials: WebAuthNsToCredentials(webAuthNs), } - credential, err := w.webAuthN.ValidateLogin(webUser, WebAuthNLoginToSessionData(webAuthN), assertionData) + webAuthNServer, err := w.serverFromContext(ctx) + if err != nil { + return nil, 0, err + } + credential, err := webAuthNServer.ValidateLogin(webUser, WebAuthNLoginToSessionData(webAuthN), assertionData) if err != nil { return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-3M9si", "Errors.User.WebAuthN.ValidateLoginFailed") } @@ -171,3 +174,12 @@ func (w *WebAuthN) FinishLogin(user *domain.Human, webAuthN *domain.WebAuthNLogi } return credential.ID, credential.Authenticator.SignCount, nil } + +func (w *Config) serverFromContext(ctx context.Context) (*webauthn.WebAuthn, error) { + host := authz.GetInstance(ctx).RequestedDomain() + return webauthn.New(&webauthn.Config{ + RPDisplayName: w.DisplayName, + RPID: host, + RPOrigin: http.BuildOrigin(host, w.ExternalSecure), + }) +}