diff --git a/cmd/zitadel/main.go b/cmd/zitadel/main.go index 2b14809e0c..d43caf6362 100644 --- a/cmd/zitadel/main.go +++ b/cmd/zitadel/main.go @@ -114,16 +114,13 @@ func startZitadel(configPaths []string) { func startUI(ctx context.Context, conf *Config, authRepo *auth_es.EsRepository) { uis := ui.Create(conf.UI) if *loginEnabled { - prefix := "" - if *localDevMode { - prefix = ui.LoginHandler - } - uis.RegisterHandler(ui.LoginHandler, login.Start(conf.UI.Login, authRepo, prefix).Handler()) + login, prefix := login.Start(conf.UI.Login, authRepo, *localDevMode) + uis.RegisterHandler(prefix, login.Handler()) } if *consoleEnabled { - consoleHandler, err := console.Start(conf.UI.Console) + consoleHandler, prefix, err := console.Start(conf.UI.Console) logging.Log("API-AGD1f").OnError(err).Fatal("error starting console") - uis.RegisterHandler(ui.ConsoleHandler, consoleHandler) + uis.RegisterHandler(prefix, consoleHandler) } uis.Start(ctx) } @@ -148,7 +145,7 @@ func startAPI(ctx context.Context, conf *Config, authZRepo *authz_repo.EsReposit apis.RegisterServer(ctx, auth.CreateServer(authRepo)) } if *oidcEnabled { - op := oidc.NewProvider(ctx, conf.API.OIDC, authRepo) + op := oidc.NewProvider(ctx, conf.API.OIDC, authRepo, *localDevMode) apis.RegisterHandler("/oauth/v2", op.HttpHandler()) } apis.Start(ctx) diff --git a/cmd/zitadel/startup.yaml b/cmd/zitadel/startup.yaml index 14c42a351f..f6dbe66553 100644 --- a/cmd/zitadel/startup.yaml +++ b/cmd/zitadel/startup.yaml @@ -195,6 +195,7 @@ API: UserAgentCookieConfig: Name: caos.zitadel.useragent Domain: $ZITADEL_COOKIE_DOMAIN + MaxAge: 8760h #365*24h (1 year) Key: EncryptionKeyID: $ZITADEL_COOKIE_KEY Cache: @@ -230,6 +231,12 @@ UI: Key: EncryptionKeyID: $ZITADEL_CSRF_KEY Development: $ZITADEL_CSRF_DEV + UserAgentCookieConfig: + Name: caos.zitadel.useragent + Domain: $ZITADEL_COOKIE_DOMAIN + MaxAge: 8760h #365*24h (1 year) + Key: + EncryptionKeyID: $ZITADEL_COOKIE_KEY Cache: MaxAge: $ZITADEL_CACHE_MAXAGE SharedMaxAge: $ZITADEL_CACHE_SHARED_MAXAGE diff --git a/go.mod b/go.mod index 757bdbe206..0055655ec5 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/aws/aws-sdk-go v1.33.13 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc github.com/caos/logging v0.0.2 - github.com/caos/oidc v0.7.3 + github.com/caos/oidc v0.7.4 github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect github.com/cockroachdb/cockroach-go/v2 v2.0.5 github.com/envoyproxy/protoc-gen-validate v0.4.0 diff --git a/go.sum b/go.sum index fa4ab60e59..f5c1790c06 100644 --- a/go.sum +++ b/go.sum @@ -71,8 +71,8 @@ github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBW github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo= github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= -github.com/caos/oidc v0.7.3 h1:QWxzCJLv7tZ4HfZRrgM3qzlmc+sB7dD9+x2VIvyc7Co= -github.com/caos/oidc v0.7.3/go.mod h1:mnuSyFmv+WSuk2C/zps445xiMU9dW384/pV4WnIS8b0= +github.com/caos/oidc v0.7.4 h1:m98Cb+wL6aPVveNaJDgkFJGmEyyamOtO0AyOKLxXWXI= +github.com/caos/oidc v0.7.4/go.mod h1:mnuSyFmv+WSuk2C/zps445xiMU9dW384/pV4WnIS8b0= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/internal/api/http/cookie.go b/internal/api/http/cookie.go index f2e28b5294..fc645f27a1 100644 --- a/internal/api/http/cookie.go +++ b/internal/api/http/cookie.go @@ -8,9 +8,15 @@ import ( "github.com/caos/zitadel/internal/errors" ) +const ( + prefixSecure = "__Secure-" + prefixHost = "__Host-" +) + type CookieHandler struct { securecookie *securecookie.SecureCookie secureOnly bool + httpOnly bool sameSite http.SameSite path string maxAge int @@ -20,6 +26,7 @@ type CookieHandler struct { func NewCookieHandler(opts ...CookieHandlerOpt) *CookieHandler { c := &CookieHandler{ secureOnly: true, + httpOnly: true, sameSite: http.SameSiteLaxMode, path: "/", } @@ -44,6 +51,12 @@ func WithUnsecure() CookieHandlerOpt { } } +func WithNonHttpOnly() CookieHandlerOpt { + return func(c *CookieHandler) { + c.httpOnly = false + } +} + func WithSameSite(sameSite http.SameSite) CookieHandlerOpt { return func(c *CookieHandler) { c.sameSite = sameSite @@ -69,6 +82,16 @@ func WithDomain(domain string) CookieHandlerOpt { } } +func SetCookiePrefix(name, domain, path string, secureOnly bool) string { + if !secureOnly { + return name + } + if domain != "" || path != "/" { + return prefixSecure + name + } + return prefixHost + name +} + func (c *CookieHandler) GetCookieValue(r *http.Request, name string) (string, error) { cookie, err := r.Cookie(name) if err != nil { @@ -78,7 +101,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(name) + cookie, err := r.Cookie(SetCookiePrefix(name, c.domain, c.path, c.secureOnly)) if err != nil { return err } @@ -110,12 +133,12 @@ func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) { func (c *CookieHandler) httpSet(w http.ResponseWriter, name, value string, maxage int) { http.SetCookie(w, &http.Cookie{ - Name: name, + Name: SetCookiePrefix(name, c.domain, c.path, c.secureOnly), Value: value, Domain: c.domain, Path: c.path, MaxAge: maxage, - HttpOnly: true, + HttpOnly: c.httpOnly, Secure: c.secureOnly, SameSite: c.sameSite, }) diff --git a/internal/api/http/header.go b/internal/api/http/header.go index a0676a3094..abd6a36793 100644 --- a/internal/api/http/header.go +++ b/internal/api/http/header.go @@ -41,13 +41,13 @@ var ( remoteAddr key ) -func CopyHeadersToContext(h http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func CopyHeadersToContext(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), httpHeaders, r.Header) ctx = context.WithValue(ctx, remoteAddr, r.RemoteAddr) r = r.WithContext(ctx) - h(w, r) - } + h.ServeHTTP(w, r) + }) } func HeadersFromCtx(ctx context.Context) (http.Header, bool) { diff --git a/internal/api/http/middleware/security_headers.go b/internal/api/http/middleware/security_headers.go index 045e41d35e..ded18e7ae0 100644 --- a/internal/api/http/middleware/security_headers.go +++ b/internal/api/http/middleware/security_headers.go @@ -9,10 +9,10 @@ import ( http_utils "github.com/caos/zitadel/internal/api/http" ) -type key int +type securityKey int const ( - nonceKey key = 0 + nonceKey securityKey = 0 DefaultNonceLength = uint(32) ) diff --git a/internal/api/http/middleware/user_agent_cookie.go b/internal/api/http/middleware/user_agent_cookie.go new file mode 100644 index 0000000000..385f8ab49e --- /dev/null +++ b/internal/api/http/middleware/user_agent_cookie.go @@ -0,0 +1,103 @@ +package middleware + +import ( + "context" + "net/http" + + http_utils "github.com/caos/zitadel/internal/api/http" + "github.com/caos/zitadel/internal/config/types" + "github.com/caos/zitadel/internal/crypto" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/id" +) + +type cookieKey int + +var ( + userAgentKey cookieKey = 0 +) + +func UserAgentIDFromCtx(ctx context.Context) (string, bool) { + userAgentID, ok := ctx.Value(userAgentKey).(string) + return userAgentID, ok +} + +type UserAgent struct { + ID string +} + +type userAgentHandler struct { + cookieHandler *http_utils.CookieHandler + cookieName string + idGenerator id.Generator + nextHandler http.Handler +} + +type UserAgentCookieConfig struct { + Name string + Domain string + Key *crypto.KeyConfig + MaxAge types.Duration +} + +func NewUserAgentHandler(config *UserAgentCookieConfig, idGenerator id.Generator, localDevMode bool) (func(http.Handler) http.Handler, error) { + key, err := crypto.LoadKey(config.Key, config.Key.EncryptionKeyID) + if err != nil { + return nil, err + } + cookieKey := []byte(key) + opts := []http_utils.CookieHandlerOpt{ + http_utils.WithEncryption(cookieKey, cookieKey), + http_utils.WithDomain(config.Domain), + http_utils.WithMaxAge(int(config.MaxAge.Seconds())), + } + if localDevMode { + opts = append(opts, http_utils.WithUnsecure()) + } + return func(handler http.Handler) http.Handler { + return &userAgentHandler{ + nextHandler: handler, + cookieName: config.Name, + cookieHandler: http_utils.NewCookieHandler(opts...), + idGenerator: idGenerator, + } + }, nil +} + +func (ua *userAgentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + agent, err := ua.getUserAgent(r) + if err != nil { + agent, err = ua.newUserAgent() + } + if err == nil { + ctx := context.WithValue(r.Context(), userAgentKey, agent.ID) + r = r.WithContext(ctx) + ua.setUserAgent(w, agent) + } + ua.nextHandler.ServeHTTP(w, r) +} + +func (ua *userAgentHandler) newUserAgent() (*UserAgent, error) { + agentID, err := ua.idGenerator.Next() + if err != nil { + return nil, err + } + return &UserAgent{ID: agentID}, nil +} + +func (ua *userAgentHandler) getUserAgent(r *http.Request) (*UserAgent, error) { + userAgent := new(UserAgent) + err := ua.cookieHandler.GetEncryptedCookieValue(r, ua.cookieName, userAgent) + if err != nil { + return nil, errors.ThrowPermissionDenied(err, "HTTP-YULqH4", "cannot read user agent cookie") + } + return userAgent, nil +} + +func (ua *userAgentHandler) setUserAgent(w http.ResponseWriter, agent *UserAgent) error { + err := ua.cookieHandler.SetEncryptedCookie(w, ua.cookieName, agent) + if err != nil { + return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie") + } + return nil +} diff --git a/internal/api/http/user_agent_cookie.go b/internal/api/http/user_agent_cookie.go deleted file mode 100644 index 22773dc62e..0000000000 --- a/internal/api/http/user_agent_cookie.go +++ /dev/null @@ -1,68 +0,0 @@ -package http - -import ( - "net/http" - - "github.com/caos/zitadel/internal/crypto" - "github.com/caos/zitadel/internal/errors" - "github.com/caos/zitadel/internal/id" -) - -type UserAgent struct { - ID string -} - -type UserAgentHandler struct { - handler *CookieHandler - cookieName string - idGenerator id.Generator -} - -type UserAgentCookieConfig struct { - Name string - Domain string - Key *crypto.KeyConfig -} - -func NewUserAgentHandler(config *UserAgentCookieConfig, idGenerator id.Generator) (*UserAgentHandler, error) { - key, err := crypto.LoadKey(config.Key, config.Key.EncryptionKeyID) - if err != nil { - return nil, err - } - cookieKey := []byte(key) - handler := NewCookieHandler( - WithEncryption(cookieKey, cookieKey), - WithDomain(config.Domain), - WithUnsecure(), - ) - return &UserAgentHandler{ - cookieName: config.Name, - handler: handler, - idGenerator: idGenerator, - }, nil -} - -func (ua *UserAgentHandler) NewUserAgent() (*UserAgent, error) { - agentID, err := ua.idGenerator.Next() - if err != nil { - return nil, err - } - return &UserAgent{ID: agentID}, nil -} - -func (ua *UserAgentHandler) GetUserAgent(r *http.Request) (*UserAgent, error) { - userAgent := new(UserAgent) - err := ua.handler.GetEncryptedCookieValue(r, ua.cookieName, userAgent) - if err != nil { - return nil, errors.ThrowPermissionDenied(err, "HTTP-YULqH4", "cannot read user agent cookie") - } - return userAgent, nil -} - -func (ua *UserAgentHandler) SetUserAgent(w http.ResponseWriter, agent *UserAgent) error { - err := ua.handler.SetEncryptedCookie(w, ua.cookieName, agent) - if err != nil { - return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie") - } - return nil -} diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index f9437a7b8f..b6741e5b41 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -9,13 +9,13 @@ import ( "github.com/caos/oidc/pkg/op" "gopkg.in/square/go-jose.v2" - "github.com/caos/zitadel/internal/auth_request/model" + "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/errors" grant_model "github.com/caos/zitadel/internal/usergrant/model" ) func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest, userID string) (op.AuthRequest, error) { - userAgentID, ok := UserAgentIDFromCtx(ctx) + userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) if !ok { return nil, errors.ThrowPreconditionFailed(nil, "OIDC-sd436", "no user agent id") } @@ -28,7 +28,11 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest } func (o *OPStorage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) { - resp, err := o.repo.AuthRequestByIDCheckLoggedIn(ctx, id) + userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) + if !ok { + return nil, errors.ThrowPreconditionFailed(nil, "OIDC-D3g21", "no user agent id") + } + resp, err := o.repo.AuthRequestByIDCheckLoggedIn(ctx, id, userAgentID) if err != nil { return nil, err } @@ -44,7 +48,11 @@ func (o *OPStorage) AuthRequestByCode(ctx context.Context, code string) (op.Auth } func (o *OPStorage) SaveAuthCode(ctx context.Context, id, code string) error { - return o.repo.SaveAuthCode(ctx, id, code) + userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) + if !ok { + return errors.ThrowPreconditionFailed(nil, "OIDC-Dgus2", "no user agent id") + } + return o.repo.SaveAuthCode(ctx, id, code, userAgentID) } func (o *OPStorage) DeleteAuthRequest(ctx context.Context, id string) error { @@ -52,16 +60,13 @@ func (o *OPStorage) DeleteAuthRequest(ctx context.Context, id string) error { } func (o *OPStorage) CreateToken(ctx context.Context, authReq op.AuthRequest) (string, time.Time, error) { - req, err := o.repo.AuthRequestByID(ctx, authReq.GetID()) + app, err := o.repo.ApplicationByClientID(ctx, authReq.GetClientID()) if err != nil { return "", time.Time{}, err } - app, err := o.repo.ApplicationByClientID(ctx, req.ApplicationID) - if err != nil { - return "", time.Time{}, err - } - grants, err := o.repo.UserGrantsByProjectAndUserID(app.ProjectID, req.UserID) - scopes := append(req.Request.(*model.AuthRequestOIDC).Scopes, grantsToScopes(grants)...) + grants, err := o.repo.UserGrantsByProjectAndUserID(app.ProjectID, authReq.GetSubject()) + scopes := append(authReq.GetScopes(), grantsToScopes(grants)...) + req, _ := authReq.(*AuthRequest) resp, err := o.repo.CreateToken(ctx, req.AgentID, req.ApplicationID, req.UserID, req.Audience, scopes, o.defaultAccessTokenLifetime) //PLANNED: lifetime from client if err != nil { return "", time.Time{}, err @@ -80,7 +85,7 @@ func grantsToScopes(grants []*grant_model.UserGrantView) []string { } func (o *OPStorage) TerminateSession(ctx context.Context, userID, clientID string) error { - userAgentID, ok := UserAgentIDFromCtx(ctx) + userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) if !ok { return errors.ThrowPreconditionFailed(nil, "OIDC-fso7F", "no user agent id") } diff --git a/internal/api/oidc/cookie_interceptor.go b/internal/api/oidc/cookie_interceptor.go deleted file mode 100644 index 9604d72047..0000000000 --- a/internal/api/oidc/cookie_interceptor.go +++ /dev/null @@ -1,37 +0,0 @@ -package oidc - -import ( - "context" - "net/http" - - http_utils "github.com/caos/zitadel/internal/api/http" -) - -type key int - -var ( - userAgentKey key -) - -func UserAgentIDFromCtx(ctx context.Context) (string, bool) { - userAgentID, ok := ctx.Value(userAgentKey).(string) - return userAgentID, ok -} - -func UserAgentCookieHandler(cookieHandler *http_utils.UserAgentHandler, nextHandlerFunc func(http.HandlerFunc) http.HandlerFunc) func(http.HandlerFunc) http.HandlerFunc { - return func(handlerFunc http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - ua, err := cookieHandler.GetUserAgent(r) - if err != nil { - ua, err = cookieHandler.NewUserAgent() - } - if err == nil { - ctx := context.WithValue(r.Context(), userAgentKey, ua.ID) - r = r.WithContext(ctx) - cookieHandler.SetUserAgent(w, ua) - } - handlerFunc(w, r) - nextHandlerFunc(handlerFunc) - } - } -} diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 9b751f9667..61cf526dca 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -2,7 +2,6 @@ package oidc import ( "context" - "net/http" "time" "github.com/caos/logging" @@ -18,7 +17,7 @@ import ( type OPHandlerConfig struct { OPConfig *op.Config StorageConfig StorageConfig - UserAgentCookieConfig *http_utils.UserAgentCookieConfig + UserAgentCookieConfig *middleware.UserAgentCookieConfig Cache *middleware.CacheConfig Endpoints *EndpointConfig } @@ -51,24 +50,18 @@ type OPStorage struct { signingKeyAlgorithm string } -func NewProvider(ctx context.Context, config OPHandlerConfig, repo repository.Repository) op.OpenIDProvider { - cookieHandler, err := http_utils.NewUserAgentHandler(config.UserAgentCookieConfig, id.SonyFlakeGenerator) +func NewProvider(ctx context.Context, config OPHandlerConfig, repo repository.Repository, localDevMode bool) op.OpenIDProvider { + cookieHandler, err := middleware.NewUserAgentHandler(config.UserAgentCookieConfig, id.SonyFlakeGenerator, localDevMode) logging.Log("OIDC-sd4fd").OnError(err).Panic("cannot user agent handler") - nextHandler := func(handlerFunc http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - middleware.NoCacheInterceptor(http_utils.CopyHeadersToContext(handlerFunc)) - } - } config.OPConfig.CodeMethodS256 = true provider, err := op.NewDefaultOP( ctx, config.OPConfig, newStorage(config.StorageConfig, repo), - op.WithHttpInterceptor( - UserAgentCookieHandler( - cookieHandler, - nextHandler, - ), + op.WithHttpInterceptors( + middleware.NoCacheInterceptor, + cookieHandler, + http_utils.CopyHeadersToContext, ), op.WithCustomAuthEndpoint(op.NewEndpointWithURL(config.Endpoints.Auth.Path, config.Endpoints.Auth.URL)), op.WithCustomTokenEndpoint(op.NewEndpointWithURL(config.Endpoints.Token.Path, config.Endpoints.Token.URL)), diff --git a/internal/auth/repository/auth_request.go b/internal/auth/repository/auth_request.go index 296ddef8fc..6e3781ed22 100644 --- a/internal/auth/repository/auth_request.go +++ b/internal/auth/repository/auth_request.go @@ -8,13 +8,13 @@ import ( type AuthRequestRepository interface { CreateAuthRequest(ctx context.Context, request *model.AuthRequest) (*model.AuthRequest, error) - AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) - AuthRequestByIDCheckLoggedIn(ctx context.Context, id string) (*model.AuthRequest, error) + AuthRequestByID(ctx context.Context, id, userAgentID string) (*model.AuthRequest, error) + AuthRequestByIDCheckLoggedIn(ctx context.Context, id, userAgentID string) (*model.AuthRequest, error) AuthRequestByCode(ctx context.Context, code string) (*model.AuthRequest, error) - SaveAuthCode(ctx context.Context, id, code string) error + SaveAuthCode(ctx context.Context, id, code, userAgentID string) error DeleteAuthRequest(ctx context.Context, id string) error - CheckLoginName(ctx context.Context, id, loginName string) error - SelectUser(ctx context.Context, id, userID string) error - VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error - VerifyMfaOTP(ctx context.Context, agentID, authRequestID string, code string, info *model.BrowserInfo) error + CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error + SelectUser(ctx context.Context, id, userID, userAgentID string) error + VerifyPassword(ctx context.Context, id, userID, password, userAgentID string, info *model.BrowserInfo) error + VerifyMfaOTP(ctx context.Context, agentID, authRequestID, code, userAgentID string, info *model.BrowserInfo) error } diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 51646041fa..35b766d6e7 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -83,16 +83,16 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *mod return request, nil } -func (repo *AuthRequestRepo) AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) { - return repo.getAuthRequest(ctx, id, false) +func (repo *AuthRequestRepo) AuthRequestByID(ctx context.Context, id, userAgentID string) (*model.AuthRequest, error) { + return repo.getAuthRequestNextSteps(ctx, id, userAgentID, false) } -func (repo *AuthRequestRepo) AuthRequestByIDCheckLoggedIn(ctx context.Context, id string) (*model.AuthRequest, error) { - return repo.getAuthRequest(ctx, id, true) +func (repo *AuthRequestRepo) AuthRequestByIDCheckLoggedIn(ctx context.Context, id, userAgentID string) (*model.AuthRequest, error) { + return repo.getAuthRequestNextSteps(ctx, id, userAgentID, true) } -func (repo *AuthRequestRepo) SaveAuthCode(ctx context.Context, id, code string) error { - request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) +func (repo *AuthRequestRepo) SaveAuthCode(ctx context.Context, id, code, userAgentID string) error { + request, err := repo.getAuthRequest(ctx, id, userAgentID) if err != nil { return err } @@ -117,8 +117,8 @@ func (repo *AuthRequestRepo) DeleteAuthRequest(ctx context.Context, id string) e return repo.AuthRequests.DeleteAuthRequest(ctx, id) } -func (repo *AuthRequestRepo) CheckLoginName(ctx context.Context, id, loginName string) error { - request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) +func (repo *AuthRequestRepo) CheckLoginName(ctx context.Context, id, loginName, userAgentID string) error { + request, err := repo.getAuthRequest(ctx, id, userAgentID) if err != nil { return err } @@ -129,8 +129,8 @@ func (repo *AuthRequestRepo) CheckLoginName(ctx context.Context, id, loginName s return repo.AuthRequests.UpdateAuthRequest(ctx, request) } -func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID string) error { - request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) +func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID, userAgentID string) error { + request, err := repo.getAuthRequest(ctx, id, userAgentID) if err != nil { return err } @@ -142,8 +142,8 @@ func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID string) return repo.AuthRequests.UpdateAuthRequest(ctx, request) } -func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error { - request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) +func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, password, userAgentID string, info *model.BrowserInfo) error { + request, err := repo.getAuthRequest(ctx, id, userAgentID) if err != nil { return err } @@ -153,8 +153,8 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, pas return repo.UserEvents.CheckPassword(ctx, userID, password, request.WithCurrentInfo(info)) } -func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, userID string, code string, info *model.BrowserInfo) error { - request, err := repo.AuthRequests.GetAuthRequestByID(ctx, authRequestID) +func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, userID, code, userAgentID string, info *model.BrowserInfo) error { + request, err := repo.getAuthRequest(ctx, authRequestID, userAgentID) if err != nil { return err } @@ -164,8 +164,8 @@ func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, us return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info)) } -func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id string, checkLoggedIn bool) (*model.AuthRequest, error) { - request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) +func (repo *AuthRequestRepo) getAuthRequestNextSteps(ctx context.Context, id, userAgentID string, checkLoggedIn bool) (*model.AuthRequest, error) { + request, err := repo.getAuthRequest(ctx, id, userAgentID) if err != nil { return nil, err } @@ -177,6 +177,17 @@ func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id string, chec return request, nil } +func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id, userAgentID string) (*model.AuthRequest, error) { + request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) + if err != nil { + return nil, err + } + if request.AgentID != userAgentID { + return nil, errors.ThrowPermissionDenied(nil, "EVENT-adk13", "Errors.AuthRequest.UserAgentNotCorresponding") + } + return request, nil +} + func (repo *AuthRequestRepo) checkLoginName(request *model.AuthRequest, loginName string) error { user, err := repo.View.UserByLoginName(loginName) if err != nil { diff --git a/internal/ui/console/console.go b/internal/ui/console/console.go index 6752ce956f..d1b415ac7a 100644 --- a/internal/ui/console/console.go +++ b/internal/ui/console/console.go @@ -28,6 +28,7 @@ type spaHandler struct { const ( envRequestPath = "/assets/environment.json" envDefaultDir = "/console/" + handlerPrefix = "/console" ) var ( @@ -50,10 +51,10 @@ func (i *spaHandler) Open(name string) (http.File, error) { return i.fileSystem.Open("/index.html") } -func Start(config Config) (http.Handler, error) { +func Start(config Config) (http.Handler, string, error) { statikFS, err := fs.NewWithNamespace("console") if err != nil { - return nil, err + return nil, "", err } envDir := envDefaultDir if config.EnvOverwriteDir != "" { @@ -69,7 +70,7 @@ func Start(config Config) (http.Handler, error) { handler := &http.ServeMux{} handler.Handle("/", cache(security(http.FileServer(&spaHandler{statikFS})))) handler.Handle(envRequestPath, cache(security(http.StripPrefix("/assets", http.FileServer(http.Dir(envDir)))))) - return handler, nil + return handler, handlerPrefix, nil } func csp(zitadelDomain string) *middleware.CSP { diff --git a/internal/ui/login/handler/auth_request.go b/internal/ui/login/handler/auth_request.go index 82fad0cb0e..0fd9da68cd 100644 --- a/internal/ui/login/handler/auth_request.go +++ b/internal/ui/login/handler/auth_request.go @@ -3,6 +3,7 @@ package handler import ( "net/http" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" ) @@ -15,7 +16,8 @@ func (l *Login) getAuthRequest(r *http.Request) (*model.AuthRequest, error) { if authRequestID == "" { return nil, nil } - return l.authRepo.AuthRequestByID(r.Context(), authRequestID) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + return l.authRepo.AuthRequestByID(r.Context(), authRequestID, userAgentID) } func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*model.AuthRequest, error) { diff --git a/internal/ui/login/handler/login.go b/internal/ui/login/handler/login.go index 0e738fd9ae..6be0da6141 100644 --- a/internal/ui/login/handler/login.go +++ b/internal/ui/login/handler/login.go @@ -11,11 +11,14 @@ import ( "golang.org/x/text/language" "github.com/caos/zitadel/internal/api/authz" + http_utils "github.com/caos/zitadel/internal/api/http" "github.com/caos/zitadel/internal/api/http/middleware" auth_repository "github.com/caos/zitadel/internal/auth/repository" "github.com/caos/zitadel/internal/auth/repository/eventsourcing" "github.com/caos/zitadel/internal/crypto" "github.com/caos/zitadel/internal/form" + "github.com/caos/zitadel/internal/id" + _ "github.com/caos/zitadel/internal/ui/login/statik" ) @@ -30,12 +33,13 @@ type Login struct { } type Config struct { - OidcAuthCallbackURL string - ZitadelURL string - LanguageCookieName string - DefaultLanguage language.Tag - CSRF CSRF - Cache middleware.CacheConfig + OidcAuthCallbackURL string + ZitadelURL string + LanguageCookieName string + DefaultLanguage language.Tag + CSRF CSRF + UserAgentCookieConfig *middleware.UserAgentCookieConfig + Cache middleware.CacheConfig } type CSRF struct { @@ -45,15 +49,20 @@ type CSRF struct { } const ( - login = "LOGIN" + login = "LOGIN" + handlerPrefix = "/login" ) -func CreateLogin(config Config, authRepo *eventsourcing.EsRepository, prefix string) *Login { +func CreateLogin(config Config, authRepo *eventsourcing.EsRepository, localDevMode bool) (*Login, string) { login := &Login{ oidcAuthCallbackURL: config.OidcAuthCallbackURL, zitadelURL: config.ZitadelURL, authRepo: authRepo, } + prefix := "" + if localDevMode { + prefix = handlerPrefix + } statikFS, err := fs.NewWithNamespace("login") logging.Log("CONFI-Ga21f").OnError(err).Panic("unable to create filesystem") @@ -62,10 +71,12 @@ func CreateLogin(config Config, authRepo *eventsourcing.EsRepository, prefix str cache, err := middleware.DefaultCacheInterceptor(EndpointResources, config.Cache.MaxAge.Duration, config.Cache.SharedMaxAge.Duration) logging.Log("CONFI-BHq2a").OnError(err).Panic("unable to create cacheInterceptor") security := middleware.SecurityHeaders(csp(), login.cspErrorHandler) - login.router = CreateRouter(login, statikFS, csrf, cache, security) + userAgentCookie, err := middleware.NewUserAgentHandler(config.UserAgentCookieConfig, id.SonyFlakeGenerator, localDevMode) + logging.Log("CONFI-Dvwf2").OnError(err).Panic("unable to create userAgentInterceptor") + login.router = CreateRouter(login, statikFS, csrf, cache, security, userAgentCookie) login.renderer = CreateRenderer(prefix, statikFS, config.LanguageCookieName, config.DefaultLanguage) login.parser = form.NewParser() - return login + return login, prefix } func csp() *middleware.CSP { @@ -81,10 +92,11 @@ func csrfInterceptor(config CSRF, errorHandler http.Handler) (func(http.Handler) if err != nil { return nil, err } + path := "/" return csrf.Protect([]byte(csrfKey), csrf.Secure(!config.Development), - csrf.CookieName(config.CookieName), - csrf.Path("/"), + csrf.CookieName(http_utils.SetCookiePrefix(config.CookieName, "", path, !config.Development)), + csrf.Path(path), csrf.ErrorHandler(errorHandler), ), nil } diff --git a/internal/ui/login/handler/login_handler.go b/internal/ui/login/handler/login_handler.go index d9d9f1f924..6fc7930c6e 100644 --- a/internal/ui/login/handler/login_handler.go +++ b/internal/ui/login/handler/login_handler.go @@ -3,6 +3,7 @@ package handler import ( "net/http" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" ) @@ -48,7 +49,8 @@ func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) { l.handleRegister(w, r) return } - err = l.authRepo.CheckLoginName(r.Context(), authReq.ID, data.LoginName) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.CheckLoginName(r.Context(), authReq.ID, data.LoginName, userAgentID) if err != nil { l.renderLogin(w, r, authReq, err) return diff --git a/internal/ui/login/handler/mfa_verify_handler.go b/internal/ui/login/handler/mfa_verify_handler.go index 8ed16f9baa..a0882c4cc7 100644 --- a/internal/ui/login/handler/mfa_verify_handler.go +++ b/internal/ui/login/handler/mfa_verify_handler.go @@ -3,6 +3,7 @@ package handler import ( "net/http" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" ) @@ -23,7 +24,8 @@ func (l *Login) handleMfaVerify(w http.ResponseWriter, r *http.Request) { return } if data.MfaType == model.MfaTypeOTP { - err = l.authRepo.VerifyMfaOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Code, model.BrowserInfoFromRequest(r)) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.VerifyMfaOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Code, userAgentID, model.BrowserInfoFromRequest(r)) } if err != nil { l.renderError(w, r, authReq, err) diff --git a/internal/ui/login/handler/password_handler.go b/internal/ui/login/handler/password_handler.go index 2d1e3afaa5..971f50d18d 100644 --- a/internal/ui/login/handler/password_handler.go +++ b/internal/ui/login/handler/password_handler.go @@ -3,6 +3,7 @@ package handler import ( "net/http" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" ) @@ -30,7 +31,8 @@ func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) { l.renderError(w, r, authReq, err) return } - err = l.authRepo.VerifyPassword(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Password, model.BrowserInfoFromRequest(r)) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.VerifyPassword(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, data.Password, userAgentID, model.BrowserInfoFromRequest(r)) if err != nil { l.renderPassword(w, r, authReq, err) return diff --git a/internal/ui/login/handler/renderer.go b/internal/ui/login/handler/renderer.go index f215c4b1d0..76f19e2872 100644 --- a/internal/ui/login/handler/renderer.go +++ b/internal/ui/login/handler/renderer.go @@ -7,16 +7,15 @@ import ( "net/http" "path" + "github.com/caos/logging" "github.com/gorilla/csrf" + "golang.org/x/text/language" - "github.com/caos/zitadel/internal/api/http/middleware" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/i18n" "github.com/caos/zitadel/internal/renderer" - - "github.com/caos/logging" - "golang.org/x/text/language" ) const ( @@ -135,7 +134,8 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, cookieName str } func (l *Login) renderNextStep(w http.ResponseWriter, r *http.Request, authReq *model.AuthRequest) { - authReq, err := l.authRepo.AuthRequestByID(r.Context(), authReq.ID) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + authReq, err := l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID) if err != nil { l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-sio0W", "could not get authreq")) } @@ -219,7 +219,7 @@ func (l *Login) getBaseData(r *http.Request, authReq *model.AuthRequest, title s ThemeMode: l.getThemeMode(r), AuthReqID: getRequestID(authReq, r), CSRF: csrf.TemplateField(r), - Nonce: middleware.GetNonce(r), + Nonce: http_mw.GetNonce(r), } } diff --git a/internal/ui/login/handler/select_user_handler.go b/internal/ui/login/handler/select_user_handler.go index 3503bd29d6..1327c62298 100644 --- a/internal/ui/login/handler/select_user_handler.go +++ b/internal/ui/login/handler/select_user_handler.go @@ -3,6 +3,7 @@ package handler import ( "net/http" + http_mw "github.com/caos/zitadel/internal/api/http/middleware" "github.com/caos/zitadel/internal/auth_request/model" ) @@ -33,7 +34,8 @@ func (l *Login) handleSelectUser(w http.ResponseWriter, r *http.Request) { l.renderLogin(w, r, authSession, nil) return } - err = l.authRepo.SelectUser(r.Context(), authSession.ID, data.UserID) + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.SelectUser(r.Context(), authSession.ID, data.UserID, userAgentID) if err != nil { l.renderError(w, r, authSession, err) return diff --git a/internal/ui/login/login.go b/internal/ui/login/login.go index 5d928c9e3c..b076b21a79 100644 --- a/internal/ui/login/login.go +++ b/internal/ui/login/login.go @@ -9,6 +9,6 @@ type Config struct { Handler handler.Config } -func Start(config Config, authRepo *eventsourcing.EsRepository, pathPrefix string) *handler.Login { - return handler.CreateLogin(config.Handler, authRepo, pathPrefix) +func Start(config Config, authRepo *eventsourcing.EsRepository, localDevMode bool) (*handler.Login, string) { + return handler.CreateLogin(config.Handler, authRepo, localDevMode) } diff --git a/internal/ui/login/static/i18n/de.yaml b/internal/ui/login/static/i18n/de.yaml index 32a47601f6..120b36c6c8 100644 --- a/internal/ui/login/static/i18n/de.yaml +++ b/internal/ui/login/static/i18n/de.yaml @@ -157,6 +157,7 @@ Errors: Internal: Es ist ein interner Fehler aufgetreten AuthRequest: NotFound: AuthRequest konnte nicht gefunden werden + UserAgentNotCorresponding: User Agent stimmt nicht überein User: NotFound: Benutzer konnte nicht gefunden werden NotMatchingUserID: User stimm nicht mit User in Auth Request überein diff --git a/internal/ui/login/static/i18n/en.yaml b/internal/ui/login/static/i18n/en.yaml index 7fafe12d5c..edc3ebf170 100644 --- a/internal/ui/login/static/i18n/en.yaml +++ b/internal/ui/login/static/i18n/en.yaml @@ -159,6 +159,7 @@ Errors: Internal: An internal error occured AuthRequest: NotFound: Could not find authrequest + UserAgentNotCorresponding: User Agent does not correspond User: NotFound: User could not be found NotMatchingUserID: User and user in authrequest don't match diff --git a/internal/ui/ui.go b/internal/ui/ui.go index d40cf571a3..a13f2b3762 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -10,9 +10,7 @@ import ( ) const ( - LoginHandler = "/login" - ConsoleHandler = "/console" - uiname = "ui" + uiname = "ui" ) type Config struct {