From 48f9815b7c4e956256aa360e936de6b7367fdd3a Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Tue, 28 Feb 2023 21:20:58 +0100 Subject: [PATCH] feat(login): use new IDP templates (#5315) The login uses the new template based IDPs with backwards compatibility for old IDPs --- internal/api/grpc/admin/idp.go | 6 +- internal/api/grpc/idp/converter.go | 8 +- internal/api/grpc/management/idp.go | 6 +- internal/api/ui/login/custom_action.go | 8 +- .../api/ui/login/external_login_handler.go | 538 ------------- .../api/ui/login/external_provider_handler.go | 707 ++++++++++++++++++ .../api/ui/login/external_register_handler.go | 304 -------- internal/api/ui/login/jwt_handler.go | 157 +--- internal/api/ui/login/policy_handler.go | 5 +- .../api/ui/login/register_option_handler.go | 2 +- internal/api/ui/login/renderer.go | 8 +- internal/api/ui/login/router.go | 3 +- internal/api/ui/login/static/i18n/de.yaml | 2 + internal/api/ui/login/static/i18n/en.yaml | 2 + internal/api/ui/login/static/i18n/fr.yaml | 2 + internal/api/ui/login/static/i18n/it.yaml | 2 + internal/api/ui/login/static/i18n/pl.yaml | 2 + internal/api/ui/login/static/i18n/zh.yaml | 2 + .../templates/external_not_found_option.html | 6 +- .../templates/external_register_overview.html | 128 ---- .../api/ui/login/static/templates/login.html | 2 +- .../static/templates/register_option.html | 2 +- .../eventsourcing/eventstore/auth_request.go | 64 +- .../eventsourcing/eventstore/org.go | 8 - .../eventsourcing/handler/handler.go | 8 - .../eventsourcing/handler/idp_config.go | 142 ---- .../eventsourcing/handler/idp_providers.go | 204 ----- .../handler/user_external_idps.go | 194 ----- .../repository/eventsourcing/repository.go | 2 +- .../eventsourcing/view/external_idps.go | 97 --- .../eventsourcing/view/idp_configs.go | 82 -- .../eventsourcing/view/idp_providers.go | 102 --- internal/auth/repository/org.go | 1 - internal/command/idp.go | 39 +- internal/command/instance_idp.go | 17 + internal/command/instance_policy_login.go | 4 +- internal/command/org_idp.go | 17 + internal/command/org_policy_login.go | 7 +- internal/command/user_idp_link.go | 8 +- internal/domain/idp.go | 9 + internal/domain/idp_config.go | 6 + internal/domain/policy_login.go | 8 +- internal/iam/model/idp_provider_view.go | 62 -- internal/idp/providers/gitlab/gitlab.go | 8 +- internal/idp/providers/gitlab/gitlab_test.go | 4 +- internal/idp/providers/gitlab/session_test.go | 6 +- internal/idp/providers/google/google.go | 4 +- internal/idp/providers/google/google_test.go | 4 +- internal/idp/providers/google/session_test.go | 6 +- internal/idp/providers/jwt/jwt.go | 1 - internal/idp/providers/jwt/session.go | 51 ++ internal/idp/providers/jwt/session_test.go | 214 +++++- internal/idp/providers/oidc/oidc.go | 18 +- internal/idp/providers/oidc/oidc_test.go | 9 +- internal/idp/providers/oidc/session_test.go | 7 +- internal/query/idp_login_policy_link.go | 27 +- internal/query/idp_login_policy_link_test.go | 21 +- internal/query/idp_template.go | 20 +- internal/query/idp_user_link.go | 16 +- internal/query/idp_user_link_test.go | 12 +- internal/query/projection/idp_template.go | 4 +- proto/zitadel/admin.proto | 4 +- 62 files changed, 1254 insertions(+), 2165 deletions(-) delete mode 100644 internal/api/ui/login/external_login_handler.go create mode 100644 internal/api/ui/login/external_provider_handler.go delete mode 100644 internal/api/ui/login/external_register_handler.go delete mode 100644 internal/api/ui/login/static/templates/external_register_overview.html delete mode 100644 internal/auth/repository/eventsourcing/handler/idp_config.go delete mode 100644 internal/auth/repository/eventsourcing/handler/idp_providers.go delete mode 100644 internal/auth/repository/eventsourcing/handler/user_external_idps.go delete mode 100644 internal/auth/repository/eventsourcing/view/external_idps.go delete mode 100644 internal/auth/repository/eventsourcing/view/idp_configs.go delete mode 100644 internal/auth/repository/eventsourcing/view/idp_providers.go diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go index 24c3c0e00e..5151999723 100644 --- a/internal/api/grpc/admin/idp.go +++ b/internal/api/grpc/admin/idp.go @@ -152,7 +152,11 @@ func (s *Server) UpdateIDPJWTConfig(ctx context.Context, req *admin_pb.UpdateIDP } func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderByIDRequest) (*admin_pb.GetProviderByIDResponse, error) { - idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetInstance(ctx).InstanceID(), false) + instanceIDQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(authz.GetInstance(ctx).InstanceID()) + if err != nil { + return nil, err + } + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, instanceIDQuery) if err != nil { return nil, err } diff --git a/internal/api/grpc/idp/converter.go b/internal/api/grpc/idp/converter.go index b478f1e5da..75a72c9b23 100644 --- a/internal/api/grpc/idp/converter.go +++ b/internal/api/grpc/idp/converter.go @@ -84,13 +84,11 @@ func IDPUserLinkToPb(link *query.IDPUserLink) *idp_pb.IDPUserLink { } } -func IDPTypeToPb(idpType domain.IDPConfigType) idp_pb.IDPType { +func IDPTypeToPb(idpType domain.IDPType) idp_pb.IDPType { switch idpType { - case domain.IDPConfigTypeOIDC: + case domain.IDPTypeOIDC: return idp_pb.IDPType_IDP_TYPE_OIDC - case domain.IDPConfigTypeSAML: - return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED - case domain.IDPConfigTypeJWT: + case domain.IDPTypeJWT: return idp_pb.IDPType_IDP_TYPE_JWT default: return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go index df76c03094..571b06c6b5 100644 --- a/internal/api/grpc/management/idp.go +++ b/internal/api/grpc/management/idp.go @@ -144,7 +144,11 @@ func (s *Server) UpdateOrgIDPJWTConfig(ctx context.Context, req *mgmt_pb.UpdateO } func (s *Server) GetProviderByID(ctx context.Context, req *mgmt_pb.GetProviderByIDRequest) (*mgmt_pb.GetProviderByIDResponse, error) { - idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetCtxData(ctx).OrgID, false) + orgIDQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, orgIDQuery) if err != nil { return nil, err } diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go index 316520fbb8..43ac22c017 100644 --- a/internal/api/ui/login/custom_action.go +++ b/internal/api/ui/login/custom_action.go @@ -13,7 +13,6 @@ import ( "github.com/zitadel/zitadel/internal/actions/object" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/domain" - iam_model "github.com/zitadel/zitadel/internal/iam/model" ) func (l *Login) runPostExternalAuthenticationActions( @@ -21,18 +20,13 @@ func (l *Login) runPostExternalAuthenticationActions( tokens *oidc.Tokens, authRequest *domain.AuthRequest, httpRequest *http.Request, - config *iam_model.IDPConfigView, authenticationError error, ) (*domain.ExternalUser, error) { ctx := httpRequest.Context() resourceOwner := authRequest.RequestedOrgID if resourceOwner == "" { - resourceOwner = config.AggregateID - } - instance := authz.GetInstance(ctx) - if resourceOwner == instance.InstanceID() { - resourceOwner = instance.DefaultOrganisationID() + resourceOwner = authz.GetInstance(ctx).DefaultOrganisationID() } triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner, false) if err != nil { diff --git a/internal/api/ui/login/external_login_handler.go b/internal/api/ui/login/external_login_handler.go deleted file mode 100644 index 35d755705a..0000000000 --- a/internal/api/ui/login/external_login_handler.go +++ /dev/null @@ -1,538 +0,0 @@ -package login - -import ( - "context" - "encoding/base64" - "net/http" - "net/url" - "strings" - "time" - - "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" - "golang.org/x/oauth2" - "golang.org/x/text/language" - - "github.com/zitadel/zitadel/internal/api/authz" - http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" - "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - "github.com/zitadel/zitadel/internal/query" -) - -const ( - queryIDPConfigID = "idpConfigID" - tmplExternalNotFoundOption = "externalnotfoundoption" -) - -type externalIDPData struct { - IDPConfigID string `schema:"idpConfigID"` -} - -type externalIDPCallbackData struct { - State string `schema:"state"` - Code string `schema:"code"` -} - -type externalNotFoundOptionFormData struct { - externalRegisterFormData - Link bool `schema:"linkbutton"` - AutoRegister bool `schema:"autoregisterbutton"` - ResetLinking bool `schema:"resetlinking"` - TermsConfirm bool `schema:"terms-confirm"` -} - -type externalNotFoundOptionData struct { - baseData - externalNotFoundOptionFormData - ExternalIDPID string - ExternalIDPUserID string - ExternalIDPUserDisplayName string - ShowUsername bool - ShowUsernameSuffix bool - OrgRegister bool - ExternalEmail string - ExternalEmailVerified bool - ExternalPhone string - ExternalPhoneVerified bool -} - -func (l *Login) handleExternalLoginStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPConfigID string) { - for _, idp := range authReq.AllowedExternalIDPs { - if idp.IDPConfigID == selectedIDPConfigID { - l.handleIDP(w, r, authReq, selectedIDPConfigID) - return - } - } - l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "VIEW-Fsj7f", "Errors.User.ExternalIDP.NotAllowed")) -} - -func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) { - data := new(externalIDPData) - authReq, err := l.getAuthRequestAndParseData(r, data) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - if authReq == nil { - l.defaultRedirect(w, r) - return - } - l.handleIDP(w, r, authReq, data.IDPConfigID) -} - -func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPConfigID string) { - idpConfig, err := l.getIDPConfigByID(r, selectedIDPConfigID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID) - if err != nil { - l.renderLogin(w, r, authReq, err) - return - } - if !idpConfig.IsOIDC { - l.handleJWTAuthorize(w, r, authReq, idpConfig) - return - } - l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalLoginCallback) -} - -func (l *Login) handleOIDCAuthorize(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) { - provider, err := l.getRPConfig(r.Context(), idpConfig, callbackEndpoint) - if err != nil { - l.renderLogin(w, r, authReq, err) - return - } - http.Redirect(w, r, rp.AuthURL(authReq.ID, provider, rp.WithPrompt(oidc.PromptSelectAccount)), http.StatusFound) -} - -func (l *Login) handleJWTAuthorize(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) { - redirect, err := url.Parse(idpConfig.JWTEndpoint) - if err != nil { - l.renderLogin(w, r, authReq, err) - return - } - q := redirect.Query() - q.Set(QueryAuthRequestID, authReq.ID) - userAgentID, ok := http_mw.UserAgentIDFromCtx(r.Context()) - if !ok { - l.renderLogin(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-dsgg3", "Errors.AuthRequest.UserAgentNotFound")) - return - } - nonce, err := l.idpConfigAlg.Encrypt([]byte(userAgentID)) - if err != nil { - l.renderLogin(w, r, authReq, err) - return - } - q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce)) - redirect.RawQuery = q.Encode() - http.Redirect(w, r, redirect.String(), http.StatusFound) -} - -func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) { - data := new(externalIDPCallbackData) - err := l.getParseData(r, data) - if err != nil { - l.renderError(w, r, nil, err) - return - } - userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - if idpConfig.IsOIDC { - provider, err := l.getRPConfig(r.Context(), idpConfig, EndpointExternalLoginCallback) - if err != nil { - emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}} - if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil { - logging.WithError(err).Error("both external user authentication and action post authentication failed") - } - - l.renderLogin(w, r, authReq, err) - return - } - tokens, err := rp.CodeExchange(r.Context(), data.Code, provider) - if err != nil { - emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}} - if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil { - logging.WithError(err).Error("both external user authentication and action post authentication failed") - } - - l.renderLogin(w, r, authReq, err) - return - } - l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens) - return - } - - err = errors.ThrowPreconditionFailed(nil, "RP-asff2", "Errors.ExternalIDP.IDPTypeNotImplemented") - emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}} - if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil { - logging.WithError(err).Error("both external user authentication and action post authentication failed") - } - - l.renderError(w, r, authReq, err) -} - -func (l *Login) getRPConfig(ctx context.Context, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) (rp.RelyingParty, error) { - oidcClientSecret, err := crypto.DecryptString(idpConfig.OIDCClientSecret, l.idpConfigAlg) - if err != nil { - return nil, err - } - if idpConfig.OIDCIssuer != "" { - return rp.NewRelyingPartyOIDC(idpConfig.OIDCIssuer, idpConfig.OIDCClientID, oidcClientSecret, l.baseURL(ctx)+callbackEndpoint, idpConfig.OIDCScopes, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second))) - } - if idpConfig.OAuthAuthorizationEndpoint == "" || idpConfig.OAuthTokenEndpoint == "" { - return nil, errors.ThrowPreconditionFailed(nil, "RP-4n0fs", "Errors.IdentityProvider.InvalidConfig") - } - oauth2Config := &oauth2.Config{ - ClientID: idpConfig.OIDCClientID, - ClientSecret: oidcClientSecret, - Endpoint: oauth2.Endpoint{ - AuthURL: idpConfig.OAuthAuthorizationEndpoint, - TokenURL: idpConfig.OAuthTokenEndpoint, - }, - RedirectURL: l.baseURL(ctx) + callbackEndpoint, - Scopes: idpConfig.OIDCScopes, - } - return rp.NewRelyingPartyOAuth(oauth2Config, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second))) -} - -func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) { - externalUser := l.mapTokenToLoginUser(tokens, idpConfig) - externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens, authReq, r, idpConfig, nil) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - - err = l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, userAgentID, externalUser, domain.BrowserInfoFromRequest(r)) - if err != nil { - if errors.IsNotFound(err) { - err = nil - } - resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() - - if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { - resourceOwner = authReq.RequestedOrgID - } - - orgIAMPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) - return - } - - human, idpLinking, _ := l.mapExternalUserToLoginUser(orgIAMPolicy, externalUser, idpConfig) - if !idpConfig.AutoRegister { - l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLinking, err) - return - } - authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLinking, err) - return - } - l.handleAutoRegister(w, r, authReq, false) - return - } - if len(externalUser.Metadatas) > 0 { - authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID) - if err != nil { - return - } - _, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.Metadatas...) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - } - l.renderNextStep(w, r, authReq) -} - -func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, human *domain.Human, externalIDP *domain.UserIDPLink, err error) { - var errID, errMessage string - if err != nil { - errID, errMessage = l.getErrorMessage(r, err) - } - if orgIAMPolicy == nil { - resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() - - if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { - resourceOwner = authReq.RequestedOrgID - } - - orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - - } - - if human == nil || externalIDP == nil { - idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1] - human, externalIDP, _ = l.mapExternalUserToLoginUser(orgIAMPolicy, linkingUser, idpConfig) - } - - var resourceOwner string - if authReq != nil { - resourceOwner = authReq.RequestedOrgID - } - if resourceOwner == "" { - resourceOwner = authz.GetInstance(r.Context()).DefaultOrganisationID() - } - labelPolicy, err := l.getLabelPolicy(r, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - - translator := l.getTranslator(r.Context(), authReq) - data := externalNotFoundOptionData{ - baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage), - externalNotFoundOptionFormData: externalNotFoundOptionFormData{ - externalRegisterFormData: externalRegisterFormData{ - Email: human.EmailAddress, - Username: human.Username, - Firstname: human.FirstName, - Lastname: human.LastName, - Nickname: human.NickName, - Language: human.PreferredLanguage.String(), - }, - }, - ExternalIDPID: externalIDP.IDPConfigID, - ExternalIDPUserID: externalIDP.ExternalUserID, - ExternalIDPUserDisplayName: externalIDP.DisplayName, - ExternalEmail: human.EmailAddress, - ExternalEmailVerified: human.IsEmailVerified, - ShowUsername: orgIAMPolicy.UserLoginMustBeDomain, - ShowUsernameSuffix: !labelPolicy.HideLoginNameSuffix, - OrgRegister: orgIAMPolicy.UserLoginMustBeDomain, - } - if human.Phone != nil { - data.Phone = human.PhoneNumber - data.ExternalPhone = human.PhoneNumber - data.ExternalPhoneVerified = human.IsPhoneVerified - } - funcs := map[string]interface{}{ - "selectedLanguage": func(l string) bool { - return data.Language == l - }, - } - l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplExternalNotFoundOption], data, funcs) -} - -func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) { - data := new(externalNotFoundOptionFormData) - authReq, err := l.getAuthRequestAndParseData(r, data) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) - return - } - if data.Link { - l.renderLogin(w, r, authReq, nil) - return - } else if data.ResetLinking { - userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.authRepo.ResetLinkingUsers(r.Context(), authReq.ID, userAgentID) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) - } - l.handleLogin(w, r) - return - } - l.handleAutoRegister(w, r, authReq, true) -} - -func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userNotFound bool) { - resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() - - if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { - resourceOwner = authReq.RequestedOrgID - } - - orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) - return - } - - idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, nil, nil, err) - return - } - - userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - if len(authReq.LinkingUsers) == 0 { - l.renderError(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData")) - return - } - - linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1] - if userNotFound { - data := new(externalNotFoundOptionFormData) - err := l.getParseData(r, data) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) - return - } - linkingUser = l.mapExternalNotFoundOptionFormDataToLoginUser(data) - } - - user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, linkingUser, idpConfig) - - user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, nil, nil, err) - return - } - err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, userAgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r)) - if err != nil { - l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, user, externalIDP, err) - return - } - authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - err = l.appendUserGrants(r.Context(), userGrants, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - l.renderNextStep(w, r, authReq) -} - -func (l *Login) mapExternalNotFoundOptionFormDataToLoginUser(formData *externalNotFoundOptionFormData) *domain.ExternalUser { - isEmailVerified := formData.ExternalEmailVerified && formData.Email == formData.ExternalEmail - isPhoneVerified := formData.ExternalPhoneVerified && formData.Phone == formData.ExternalPhone - return &domain.ExternalUser{ - IDPConfigID: formData.ExternalIDPConfigID, - ExternalUserID: formData.ExternalIDPExtUserID, - PreferredUsername: formData.Username, - DisplayName: formData.Email, - FirstName: formData.Firstname, - LastName: formData.Lastname, - NickName: formData.Nickname, - Email: formData.Email, - IsEmailVerified: isEmailVerified, - Phone: formData.Phone, - IsPhoneVerified: isPhoneVerified, - PreferredLanguage: language.Make(formData.Language), - } -} - -func (l *Login) mapTokenToLoginUser(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) *domain.ExternalUser { - displayName := tokens.IDTokenClaims.GetPreferredUsername() - if displayName == "" && tokens.IDTokenClaims.GetEmail() != "" { - displayName = tokens.IDTokenClaims.GetEmail() - } - switch idpConfig.OIDCIDPDisplayNameMapping { - case iam_model.OIDCMappingFieldEmail: - if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" { - displayName = tokens.IDTokenClaims.GetEmail() - } - } - - externalUser := &domain.ExternalUser{ - IDPConfigID: idpConfig.IDPConfigID, - ExternalUserID: tokens.IDTokenClaims.GetSubject(), - PreferredUsername: tokens.IDTokenClaims.GetPreferredUsername(), - DisplayName: displayName, - FirstName: tokens.IDTokenClaims.GetGivenName(), - LastName: tokens.IDTokenClaims.GetFamilyName(), - NickName: tokens.IDTokenClaims.GetNickname(), - Email: tokens.IDTokenClaims.GetEmail(), - IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(), - PreferredLanguage: tokens.IDTokenClaims.GetLocale(), - } - - if tokens.IDTokenClaims.GetPhoneNumber() != "" { - externalUser.Phone = tokens.IDTokenClaims.GetPhoneNumber() - externalUser.IsPhoneVerified = tokens.IDTokenClaims.IsPhoneNumberVerified() - } - return externalUser -} -func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *query.DomainPolicy, linkingUser *domain.ExternalUser, idpConfig *iam_model.IDPConfigView) (*domain.Human, *domain.UserIDPLink, []*domain.Metadata) { - username := linkingUser.PreferredUsername - switch idpConfig.OIDCUsernameMapping { - case iam_model.OIDCMappingFieldEmail: - if linkingUser.IsEmailVerified && linkingUser.Email != "" && username == "" { - username = linkingUser.Email - } - } - if username == "" { - username = linkingUser.Email - } - - if orgIamPolicy.UserLoginMustBeDomain { - index := strings.LastIndex(username, "@") - if index > 1 { - username = username[:index] - } - } - - human := &domain.Human{ - Username: username, - Profile: &domain.Profile{ - FirstName: linkingUser.FirstName, - LastName: linkingUser.LastName, - PreferredLanguage: linkingUser.PreferredLanguage, - NickName: linkingUser.NickName, - }, - Email: &domain.Email{ - EmailAddress: linkingUser.Email, - IsEmailVerified: linkingUser.IsEmailVerified, - }, - } - if linkingUser.Phone != "" { - human.Phone = &domain.Phone{ - PhoneNumber: linkingUser.Phone, - IsPhoneVerified: linkingUser.IsPhoneVerified, - } - } - - displayName := linkingUser.PreferredUsername - switch idpConfig.OIDCIDPDisplayNameMapping { - case iam_model.OIDCMappingFieldEmail: - if linkingUser.IsEmailVerified && linkingUser.Email != "" && displayName == "" { - displayName = linkingUser.Email - } - } - if displayName == "" { - displayName = linkingUser.Email - } - - externalIDP := &domain.UserIDPLink{ - IDPConfigID: idpConfig.IDPConfigID, - ExternalUserID: linkingUser.ExternalUserID, - DisplayName: displayName, - } - return human, externalIDP, linkingUser.Metadatas -} diff --git a/internal/api/ui/login/external_provider_handler.go b/internal/api/ui/login/external_provider_handler.go new file mode 100644 index 0000000000..9e6d7d3787 --- /dev/null +++ b/internal/api/ui/login/external_provider_handler.go @@ -0,0 +1,707 @@ +package login + +import ( + "context" + "net/http" + "strings" + + "github.com/zitadel/logging" + "github.com/zitadel/oidc/v2/pkg/client/rp" + "github.com/zitadel/oidc/v2/pkg/oidc" + "golang.org/x/oauth2" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/idp/providers/google" + "github.com/zitadel/zitadel/internal/idp/providers/jwt" + openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/query" +) + +const ( + queryIDPConfigID = "idpConfigID" + tmplExternalNotFoundOption = "externalnotfoundoption" +) + +type externalIDPData struct { + IDPConfigID string `schema:"idpConfigID"` +} + +type externalIDPCallbackData struct { + State string `schema:"state"` + Code string `schema:"code"` +} + +type externalNotFoundOptionFormData struct { + externalRegisterFormData + Link bool `schema:"linkbutton"` + AutoRegister bool `schema:"autoregisterbutton"` + ResetLinking bool `schema:"resetlinking"` + TermsConfirm bool `schema:"terms-confirm"` +} + +type externalNotFoundOptionData struct { + baseData + externalNotFoundOptionFormData + IsLinkingAllowed bool + IsCreationAllowed bool + ExternalIDPID string + ExternalIDPUserID string + ExternalIDPUserDisplayName string + ShowUsername bool + ShowUsernameSuffix bool + OrgRegister bool + ExternalEmail string + ExternalEmailVerified bool + ExternalPhone string + ExternalPhoneVerified bool +} + +type externalRegisterFormData struct { + ExternalIDPConfigID string `schema:"external-idp-config-id"` + ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"` + ExternalIDPDisplayName string `schema:"external-idp-display-name"` + ExternalEmail string `schema:"external-email"` + ExternalEmailVerified bool `schema:"external-email-verified"` + Email string `schema:"email"` + Username string `schema:"username"` + Firstname string `schema:"firstname"` + Lastname string `schema:"lastname"` + Nickname string `schema:"nickname"` + ExternalPhone string `schema:"external-phone"` + ExternalPhoneVerified bool `schema:"external-phone-verified"` + Phone string `schema:"phone"` + Language string `schema:"language"` + TermsConfirm bool `schema:"terms-confirm"` +} + +// handleExternalLoginStep is called as nextStep +func (l *Login) handleExternalLoginStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPID string) { + for _, idp := range authReq.AllowedExternalIDPs { + if idp.IDPConfigID == selectedIDPID { + l.handleIDP(w, r, authReq, selectedIDPID) + return + } + } + l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "VIEW-Fsj7f", "Errors.User.ExternalIDP.NotAllowed")) +} + +// handleExternalLogin is called when a user selects the idp on the login page +func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) { + data := new(externalIDPData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + if authReq == nil { + l.defaultRedirect(w, r) + return + } + l.handleIDP(w, r, authReq, data.IDPConfigID) +} + +// handleExternalRegister is called when a user selects the idp on the register options page +func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) { + data := new(externalIDPData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + l.handleIDP(w, r, authReq, data.IDPConfigID) +} + +// handleIDP start the authentication of the selected IDP +// it will redirect to the IDPs auth page +func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, id string) { + identityProvider, err := l.getIDPByID(r, id) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, identityProvider.ID, userAgentID) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + var provider idp.Provider + switch identityProvider.Type { + case domain.IDPTypeOIDC: + provider, err = l.oidcProvider(r.Context(), identityProvider) + case domain.IDPTypeJWT: + provider, err = l.jwtProvider(r.Context(), identityProvider) + case domain.IDPTypeGoogle: + provider, err = l.googleProvider(r.Context(), identityProvider) + case domain.IDPTypeOAuth, + domain.IDPTypeLDAP, + domain.IDPTypeAzureAD, + domain.IDPTypeGitHub, + domain.IDPTypeGitHubEE, + domain.IDPTypeGitLab, + domain.IDPTypeGitLabSelfHosted, + domain.IDPTypeUnspecified: + fallthrough + default: + l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "LOGIN-AShek", "Errors.ExternalIDP.IDPTypeNotImplemented")) + return + } + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + session, err := provider.BeginAuth(r.Context(), authReq.ID, authReq.AgentID) + if err != nil { + l.renderLogin(w, r, authReq, err) + return + } + http.Redirect(w, r, session.GetAuthURL(), http.StatusFound) +} + +// handleExternalLoginCallback handles the callback from a IDP +// and tries to extract the user with the provided data +func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) { + data := new(externalIDPCallbackData) + err := l.getParseData(r, data) + if err != nil { + l.renderLogin(w, r, nil, err) + return + } + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID) + if err != nil { + l.externalAuthFailed(w, r, authReq, nil, err) + return + } + identityProvider, err := l.getIDPByID(r, authReq.SelectedIDPConfigID) + if err != nil { + l.externalAuthFailed(w, r, authReq, nil, err) + return + } + var provider idp.Provider + var session idp.Session + switch identityProvider.Type { + case domain.IDPTypeOIDC: + provider, err = l.oidcProvider(r.Context(), identityProvider) + if err != nil { + l.externalAuthFailed(w, r, authReq, nil, err) + return + } + session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code} + case domain.IDPTypeGoogle: + provider, err = l.googleProvider(r.Context(), identityProvider) + if err != nil { + l.externalAuthFailed(w, r, authReq, nil, err) + return + } + session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code} + case domain.IDPTypeJWT, + domain.IDPTypeOAuth, + domain.IDPTypeLDAP, + domain.IDPTypeAzureAD, + domain.IDPTypeGitHub, + domain.IDPTypeGitHubEE, + domain.IDPTypeGitLab, + domain.IDPTypeGitLabSelfHosted, + domain.IDPTypeUnspecified: + fallthrough + default: + l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "LOGIN-SFefg", "Errors.ExternalIDP.IDPTypeNotImplemented")) + return + } + + user, err := session.FetchUser(r.Context()) + if err != nil { + l.externalAuthFailed(w, r, authReq, tokens(session), err) + return + } + l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep) +} + +// handleExternalUserAuthenticated maps the IDP user, checks for a corresponding externalID +func (l *Login) handleExternalUserAuthenticated( + w http.ResponseWriter, + r *http.Request, + authReq *domain.AuthRequest, + provider *query.IDPTemplate, + session idp.Session, + user idp.User, + callback func(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest), +) { + externalUser := mapIDPUserToExternalUser(user, provider.ID) + externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens(session), authReq, r, nil) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + err = l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r)) + if err != nil { + if !errors.IsNotFound(err) { + l.renderError(w, r, authReq, err) + return + } + l.externalUserNotExisting(w, r, authReq, provider, externalUser) + return + } + if provider.IsAutoUpdate || len(externalUser.Metadatas) > 0 { + // read current auth request state (incl. authorized user) + authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + } + if provider.IsAutoUpdate { + err = l.updateExternalUser(r.Context(), authReq, externalUser) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + } + if len(externalUser.Metadatas) > 0 { + _, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.Metadatas...) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + } + callback(w, r, authReq) +} + +// externalUserNotExisting is called if an externalAuthentication couldn't find a corresponding externalID +// possible solutions are: +// +// * auto creation +// * external not found overview: +// - creation by user +// - linking to existing user +func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) { + resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() + + if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { + resourceOwner = authReq.RequestedOrgID + } + + orgIAMPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) + return + } + + human, idpLink, _ := mapExternalUserToLoginUser(externalUser, orgIAMPolicy.UserLoginMustBeDomain) + // if auto creation or creation itself is disabled, send the user to the notFoundOption + if !provider.IsCreationAllowed || !provider.IsAutoCreation { + l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err) + return + } + + // reload auth request, to ensure current state (checked external login) + authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err) + return + } + l.autoCreateExternalUser(w, r, authReq) +} + +// autoCreateExternalUser takes the externalUser and creates it automatically (without user interaction) +func (l *Login) autoCreateExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { + if len(authReq.LinkingUsers) == 0 { + l.renderError(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData")) + return + } + + // TODO (LS): how do we get multiple and why do we use the last of them (taken as is)? + linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1] + + l.registerExternalUser(w, r, authReq, linkingUser) +} + +// renderExternalNotFoundOption renders a page, where the user is able to edit the IDP data, +// create a new externalUser of link to existing on (based on the IDP template) +func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, human *domain.Human, idpLink *domain.UserIDPLink, err error) { + var errID, errMessage string + if err != nil { + errID, errMessage = l.getErrorMessage(r, err) + } + if orgIAMPolicy == nil { + resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() + + if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { + resourceOwner = authReq.RequestedOrgID + } + + orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + + } + + if human == nil || idpLink == nil { + + // TODO (LS): how do we get multiple and why do we use the last of them (taken as is)? + linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1] + human, idpLink, _ = mapExternalUserToLoginUser(linkingUser, orgIAMPolicy.UserLoginMustBeDomain) + } + + var resourceOwner string + if authReq != nil { + resourceOwner = authReq.RequestedOrgID + } + if resourceOwner == "" { + resourceOwner = authz.GetInstance(r.Context()).DefaultOrganisationID() + } + labelPolicy, err := l.getLabelPolicy(r, resourceOwner) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + + idpTemplate, err := l.getIDPByID(r, idpLink.IDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + + translator := l.getTranslator(r.Context(), authReq) + data := externalNotFoundOptionData{ + baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage), + externalNotFoundOptionFormData: externalNotFoundOptionFormData{ + externalRegisterFormData: externalRegisterFormData{ + Email: human.EmailAddress, + Username: human.Username, + Firstname: human.FirstName, + Lastname: human.LastName, + Nickname: human.NickName, + Language: human.PreferredLanguage.String(), + }, + }, + IsLinkingAllowed: idpTemplate.IsLinkingAllowed, + IsCreationAllowed: idpTemplate.IsCreationAllowed, + ExternalIDPID: idpLink.IDPConfigID, + ExternalIDPUserID: idpLink.ExternalUserID, + ExternalIDPUserDisplayName: idpLink.DisplayName, + ExternalEmail: human.EmailAddress, + ExternalEmailVerified: human.IsEmailVerified, + ShowUsername: orgIAMPolicy.UserLoginMustBeDomain, + ShowUsernameSuffix: !labelPolicy.HideLoginNameSuffix, + OrgRegister: orgIAMPolicy.UserLoginMustBeDomain, + } + if human.Phone != nil { + data.Phone = human.PhoneNumber + data.ExternalPhone = human.PhoneNumber + data.ExternalPhoneVerified = human.IsPhoneVerified + } + funcs := map[string]interface{}{ + "selectedLanguage": func(l string) bool { + return data.Language == l + }, + } + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplExternalNotFoundOption], data, funcs) +} + +// handleExternalNotFoundOptionCheck takes the data from the submitted externalNotFound page +// and either links or creates an externalUser +func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) { + data := new(externalNotFoundOptionFormData) + authReq, err := l.getAuthRequestAndParseData(r, data) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) + return + } + + idpTemplate, err := l.getIDPByID(r, authReq.SelectedIDPConfigID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + // if the user click on the cancel button / back icon + if data.ResetLinking { + userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) + err = l.authRepo.ResetLinkingUsers(r.Context(), authReq.ID, userAgentID) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) + } + l.handleLogin(w, r) + return + } + // if the user selects the linking button + if data.Link { + if !idpTemplate.IsLinkingAllowed { + l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, errors.ThrowPreconditionFailed(nil, "LOGIN-AS3ff", "Errors.ExternalIDP.LinkingNotAllowed")) + return + } + l.renderLogin(w, r, authReq, nil) + return + } + // if the user selects the creation button + if !idpTemplate.IsCreationAllowed { + l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, errors.ThrowPreconditionFailed(nil, "LOGIN-dsfd3", "Errors.ExternalIDP.CreationNotAllowed")) + return + } + linkingUser := mapExternalNotFoundOptionFormDataToLoginUser(data) + l.registerExternalUser(w, r, authReq, linkingUser) +} + +// registerExternalUser creates an externalUser with the provided data +// incl. execution of pre and post creation actions +// +// it is called from either the [autoCreateExternalUser] or [handleExternalNotFoundOptionCheck] +func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) { + resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() + + if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { + resourceOwner = authReq.RequestedOrgID + } + + orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) + return + } + user, externalIDP, metadata := mapExternalUserToLoginUser(externalUser, orgIamPolicy.UserLoginMustBeDomain) + + user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, nil, nil, err) + return + } + err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r)) + if err != nil { + l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, user, externalIDP, err) + return + } + // read auth request again to get current state including userID + authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + err = l.appendUserGrants(r.Context(), userGrants, resourceOwner) + if err != nil { + l.renderError(w, r, authReq, err) + return + } + l.renderNextStep(w, r, authReq) +} + +// updateExternalUser will update the existing user (email, phone, profile) with data provided by the IDP +func (l *Login) updateExternalUser(ctx context.Context, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) error { + user, err := l.query.GetUserByID(ctx, true, authReq.UserID, false) + if err != nil { + return err + } + if user.Human == nil { + return errors.ThrowPreconditionFailed(nil, "LOGIN-WLTce", "Errors.User.NotHuman") + } + if externalUser.Email != "" && externalUser.Email != user.Human.Email && externalUser.IsEmailVerified != user.Human.IsEmailVerified { + emailCodeGenerator, err := l.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg) + logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update email") + if err == nil { + _, err = l.command.ChangeHumanEmail(setContext(ctx, authReq.UserOrgID), + &domain.Email{ + ObjectRoot: models.ObjectRoot{AggregateID: authReq.UserID}, + EmailAddress: externalUser.Email, + IsEmailVerified: externalUser.IsEmailVerified, + }, + emailCodeGenerator) + logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update email") + } + } + if externalUser.Phone != "" && externalUser.Phone != user.Human.Phone && externalUser.IsPhoneVerified != user.Human.IsPhoneVerified { + phoneCodeGenerator, err := l.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, l.userCodeAlg) + logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update phone") + if err == nil { + _, err = l.command.ChangeHumanPhone(setContext(ctx, authReq.UserOrgID), + &domain.Phone{ + ObjectRoot: models.ObjectRoot{AggregateID: authReq.UserID}, + PhoneNumber: externalUser.Phone, + IsPhoneVerified: externalUser.IsPhoneVerified, + }, + authReq.UserOrgID, + phoneCodeGenerator) + logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update phone") + } + } + if externalUser.FirstName != user.Human.FirstName || + externalUser.LastName != user.Human.LastName || + externalUser.NickName != user.Human.NickName || + externalUser.DisplayName != user.Human.DisplayName || + externalUser.PreferredLanguage != user.Human.PreferredLanguage { + _, err = l.command.ChangeHumanProfile(setContext(ctx, authReq.UserOrgID), &domain.Profile{ + ObjectRoot: models.ObjectRoot{AggregateID: authReq.UserID}, + FirstName: externalUser.FirstName, + LastName: externalUser.LastName, + NickName: externalUser.NickName, + DisplayName: externalUser.DisplayName, + PreferredLanguage: externalUser.PreferredLanguage, + Gender: user.Human.Gender, + }) + logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update profile") + } + return nil +} + +func (l *Login) googleProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*google.Provider, error) { + errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) { + logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state) + rp.DefaultErrorHandler(w, r, errorType, errorDesc, state) + } + openid.WithRelyingPartyOption(rp.WithErrorHandler(errorHandler)) + secret, err := crypto.DecryptString(identityProvider.GoogleIDPTemplate.ClientSecret, l.idpConfigAlg) + if err != nil { + return nil, err + } + return google.New( + identityProvider.GoogleIDPTemplate.ClientID, + secret, + l.baseURL(ctx)+EndpointExternalLoginCallback, + identityProvider.GoogleIDPTemplate.Scopes, + ) +} + +func (l *Login) oidcProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*openid.Provider, error) { + secret, err := crypto.DecryptString(identityProvider.OIDCIDPTemplate.ClientSecret, l.idpConfigAlg) + if err != nil { + return nil, err + } + return openid.New(identityProvider.Name, + identityProvider.OIDCIDPTemplate.Issuer, + identityProvider.OIDCIDPTemplate.ClientID, + secret, + l.baseURL(ctx)+EndpointExternalLoginCallback, + identityProvider.OIDCIDPTemplate.Scopes, + openid.DefaultMapper, + ) +} + +func (l *Login) jwtProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*jwt.Provider, error) { + return jwt.New( + identityProvider.Name, + identityProvider.JWTIDPTemplate.Issuer, + identityProvider.JWTIDPTemplate.Endpoint, + identityProvider.JWTIDPTemplate.KeysEndpoint, + identityProvider.JWTIDPTemplate.HeaderName, + l.idpConfigAlg, + ) +} + +func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error { + if len(userGrants) == 0 { + return nil + } + for _, grant := range userGrants { + _, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner) + if err != nil { + return err + } + } + return nil +} + +func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, tokens *oidc.Tokens, err error) { + if tokens == nil { + tokens = &oidc.Tokens{Token: &oauth2.Token{}} + } + if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, err); actionErr != nil { + logging.WithError(err).Error("both external user authentication and action post authentication failed") + } + l.renderLogin(w, r, authReq, err) +} + +// tokens extracts the oidc.Tokens for backwards compatibility of PostExternalAuthenticationActions +func tokens(session idp.Session) *oidc.Tokens { + switch s := session.(type) { + case *openid.Session: + return s.Tokens + case *jwt.Session: + return s.Tokens + } + return nil +} + +func mapIDPUserToExternalUser(user idp.User, id string) *domain.ExternalUser { + return &domain.ExternalUser{ + IDPConfigID: id, + ExternalUserID: user.GetID(), + PreferredUsername: user.GetPreferredUsername(), + DisplayName: user.GetDisplayName(), + FirstName: user.GetFirstName(), + LastName: user.GetLastName(), + NickName: user.GetNickname(), + Email: user.GetEmail(), + IsEmailVerified: user.IsEmailVerified(), + PreferredLanguage: user.GetPreferredLanguage(), + Phone: user.GetPhone(), + IsPhoneVerified: user.IsPhoneVerified(), + } +} + +func mapExternalUserToLoginUser(externalUser *domain.ExternalUser, mustBeDomain bool) (*domain.Human, *domain.UserIDPLink, []*domain.Metadata) { + username := externalUser.PreferredUsername + if mustBeDomain { + index := strings.LastIndex(username, "@") + if index > 1 { + username = username[:index] + } + } + human := &domain.Human{ + Username: username, + Profile: &domain.Profile{ + FirstName: externalUser.FirstName, + LastName: externalUser.LastName, + PreferredLanguage: externalUser.PreferredLanguage, + NickName: externalUser.NickName, + DisplayName: externalUser.DisplayName, + }, + Email: &domain.Email{ + EmailAddress: externalUser.Email, + IsEmailVerified: externalUser.IsEmailVerified, + }, + } + if externalUser.Phone != "" { + human.Phone = &domain.Phone{ + PhoneNumber: externalUser.Phone, + IsPhoneVerified: externalUser.IsPhoneVerified, + } + } + externalIDP := &domain.UserIDPLink{ + IDPConfigID: externalUser.IDPConfigID, + ExternalUserID: externalUser.ExternalUserID, + DisplayName: externalUser.DisplayName, + } + return human, externalIDP, externalUser.Metadatas +} + +func mapExternalNotFoundOptionFormDataToLoginUser(formData *externalNotFoundOptionFormData) *domain.ExternalUser { + isEmailVerified := formData.ExternalEmailVerified && formData.Email == formData.ExternalEmail + isPhoneVerified := formData.ExternalPhoneVerified && formData.Phone == formData.ExternalPhone + return &domain.ExternalUser{ + IDPConfigID: formData.ExternalIDPConfigID, + ExternalUserID: formData.ExternalIDPExtUserID, + PreferredUsername: formData.Username, + DisplayName: formData.Email, + FirstName: formData.Firstname, + LastName: formData.Lastname, + NickName: formData.Nickname, + Email: formData.Email, + IsEmailVerified: isEmailVerified, + Phone: formData.Phone, + IsPhoneVerified: isPhoneVerified, + PreferredLanguage: language.Make(formData.Language), + } +} diff --git a/internal/api/ui/login/external_register_handler.go b/internal/api/ui/login/external_register_handler.go deleted file mode 100644 index 713d41b607..0000000000 --- a/internal/api/ui/login/external_register_handler.go +++ /dev/null @@ -1,304 +0,0 @@ -package login - -import ( - "net/http" - - "github.com/zitadel/oidc/v2/pkg/client/rp" - "github.com/zitadel/oidc/v2/pkg/oidc" - "golang.org/x/text/language" - - "github.com/zitadel/zitadel/internal/api/authz" - http_mw "github.com/zitadel/zitadel/internal/api/http/middleware" - "github.com/zitadel/zitadel/internal/domain" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - "github.com/zitadel/zitadel/internal/query" -) - -const ( - tmplExternalRegisterOverview = "externalregisteroverview" -) - -type externalRegisterFormData struct { - ExternalIDPConfigID string `schema:"external-idp-config-id"` - ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"` - ExternalIDPDisplayName string `schema:"external-idp-display-name"` - ExternalEmail string `schema:"external-email"` - ExternalEmailVerified bool `schema:"external-email-verified"` - Email string `schema:"email"` - Username string `schema:"username"` - Firstname string `schema:"firstname"` - Lastname string `schema:"lastname"` - Nickname string `schema:"nickname"` - ExternalPhone string `schema:"external-phone"` - ExternalPhoneVerified bool `schema:"external-phone-verified"` - Phone string `schema:"phone"` - Language string `schema:"language"` - TermsConfirm bool `schema:"terms-confirm"` -} - -type externalRegisterData struct { - baseData - externalRegisterFormData - ExternalIDPID string - ExternalIDPUserID string - ExternalIDPUserDisplayName string - ShowUsername bool - ShowUsernameSuffix bool - OrgRegister bool - ExternalEmail string - ExternalEmailVerified bool - ExternalPhone string - ExternalPhoneVerified bool -} - -func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) { - data := new(externalIDPData) - authReq, err := l.getAuthRequestAndParseData(r, data) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - l.handleExternalRegisterByConfigID(w, r, authReq, data.IDPConfigID) -} - -func (l *Login) handleExternalRegisterByConfigID(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, configID string) { - if authReq == nil { - l.defaultRedirect(w, r) - return - } - idpConfig, err := l.getIDPConfigByID(r, configID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID) - if err != nil { - l.renderLogin(w, r, authReq, err) - return - } - if !idpConfig.IsOIDC { - l.handleJWTAuthorize(w, r, authReq, idpConfig) - return - } - l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalRegisterCallback) -} - -func (l *Login) handleExternalRegisterCallback(w http.ResponseWriter, r *http.Request) { - data := new(externalIDPCallbackData) - err := l.getParseData(r, data) - if err != nil { - l.renderError(w, r, nil, err) - return - } - userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) - authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - provider, err := l.getRPConfig(r.Context(), idpConfig, EndpointExternalRegisterCallback) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - tokens, err := rp.CodeExchange(r.Context(), data.Code, provider) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - l.handleExternalUserRegister(w, r, authReq, idpConfig, userAgentID, tokens) -} - -func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) { - resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() - if authReq.RequestedOrgID != "" { - resourceOwner = authReq.RequestedOrgID - } - externalUser, externalIDP := l.mapTokenToLoginHumanAndExternalIDP(tokens, idpConfig) - externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens, authReq, r, idpConfig, nil) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - if idpConfig.AutoRegister { - l.registerExternalUser(w, r, authReq, externalUser) - return - } - orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - labelPolicy, err := l.getLabelPolicy(r, resourceOwner) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - l.renderExternalRegisterOverview(w, r, authReq, orgIamPolicy, externalUser, externalIDP, labelPolicy.HideLoginNameSuffix, nil) -} - -func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) { - resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() - - if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { - resourceOwner = authReq.RequestedOrgID - } - orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - - idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, externalUser, idpConfig) - user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r)) - if err != nil { - l.renderRegisterOption(w, r, authReq, err) - return - } - // read auth request again to get current state including userID - authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - err = l.appendUserGrants(r.Context(), userGrants, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - l.renderNextStep(w, r, authReq) -} - -func (l *Login) renderExternalRegisterOverview(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, externalUser *domain.ExternalUser, idp *domain.UserIDPLink, hideLoginNameSuffix bool, err error) { - var errID, errMessage string - if err != nil { - errID, errMessage = l.getErrorMessage(r, err) - } - - translator := l.getTranslator(r.Context(), authReq) - data := externalRegisterData{ - baseData: l.getBaseData(r, authReq, "ExternalRegistrationUserOverview.Title", "ExternalRegistrationUserOverview.Description", errID, errMessage), - externalRegisterFormData: externalRegisterFormData{ - Email: externalUser.Email, - Username: externalUser.PreferredUsername, - Firstname: externalUser.FirstName, - Lastname: externalUser.LastName, - Nickname: externalUser.NickName, - Language: externalUser.PreferredLanguage.String(), - }, - ExternalIDPID: idp.IDPConfigID, - ExternalIDPUserID: idp.ExternalUserID, - ExternalIDPUserDisplayName: idp.DisplayName, - ExternalEmail: externalUser.Email, - ExternalEmailVerified: externalUser.IsEmailVerified, - ShowUsername: orgIAMPolicy.UserLoginMustBeDomain, - OrgRegister: orgIAMPolicy.UserLoginMustBeDomain, - ShowUsernameSuffix: !hideLoginNameSuffix, - } - data.Phone = externalUser.Phone - data.ExternalPhone = externalUser.Phone - data.ExternalPhoneVerified = externalUser.IsPhoneVerified - - funcs := map[string]interface{}{ - "selectedLanguage": func(l string) bool { - return data.Language == l - }, - } - l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplExternalRegisterOverview], data, funcs) -} - -func (l *Login) handleExternalRegisterCheck(w http.ResponseWriter, r *http.Request) { - data := new(externalRegisterFormData) - authReq, err := l.getAuthRequestAndParseData(r, data) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - - resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID() - - if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { - resourceOwner = authReq.RequestedOrgID - } - - user := l.mapExternalRegisterDataToUser(data) - l.registerExternalUser(w, r, authReq, user) -} - -func (l *Login) mapTokenToLoginHumanAndExternalIDP(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) (*domain.ExternalUser, *domain.UserIDPLink) { - displayName := tokens.IDTokenClaims.GetPreferredUsername() - switch idpConfig.OIDCIDPDisplayNameMapping { - case iam_model.OIDCMappingFieldEmail: - if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" { - displayName = tokens.IDTokenClaims.GetEmail() - } - } - if displayName == "" { - displayName = tokens.IDTokenClaims.GetEmail() - } - - externalUser := &domain.ExternalUser{ - IDPConfigID: idpConfig.IDPConfigID, - ExternalUserID: tokens.IDTokenClaims.GetSubject(), - PreferredUsername: tokens.IDTokenClaims.GetPreferredUsername(), - DisplayName: displayName, - FirstName: tokens.IDTokenClaims.GetGivenName(), - LastName: tokens.IDTokenClaims.GetFamilyName(), - NickName: tokens.IDTokenClaims.GetNickname(), - Email: tokens.IDTokenClaims.GetEmail(), - IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(), - PreferredLanguage: tokens.IDTokenClaims.GetLocale(), - } - - if tokens.IDTokenClaims.GetPhoneNumber() != "" { - externalUser.Phone = tokens.IDTokenClaims.GetPhoneNumber() - externalUser.IsPhoneVerified = tokens.IDTokenClaims.IsPhoneNumberVerified() - } - - externalIDP := &domain.UserIDPLink{ - IDPConfigID: idpConfig.IDPConfigID, - ExternalUserID: tokens.IDTokenClaims.GetSubject(), - DisplayName: displayName, - } - return externalUser, externalIDP -} - -func (l *Login) mapExternalRegisterDataToUser(data *externalRegisterFormData) *domain.ExternalUser { - isEmailVerified := data.ExternalEmailVerified && data.Email == data.ExternalEmail - isPhoneVerified := data.ExternalPhoneVerified && data.Phone == data.ExternalPhone - return &domain.ExternalUser{ - IDPConfigID: data.ExternalIDPConfigID, - ExternalUserID: data.ExternalIDPExtUserID, - PreferredUsername: data.Username, - DisplayName: data.Email, - FirstName: data.Firstname, - LastName: data.Lastname, - NickName: data.Nickname, - PreferredLanguage: language.Make(data.Language), - Email: data.Email, - IsEmailVerified: isEmailVerified, - Phone: data.Phone, - IsPhoneVerified: isPhoneVerified, - } -} diff --git a/internal/api/ui/login/jwt_handler.go b/internal/api/ui/login/jwt_handler.go index ac100dd6a1..94c5a39e01 100644 --- a/internal/api/ui/login/jwt_handler.go +++ b/internal/api/ui/login/jwt_handler.go @@ -6,17 +6,16 @@ import ( "net/http" "net/url" "strings" - "time" "github.com/zitadel/logging" - "github.com/zitadel/oidc/v2/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/oauth2" http_util "github.com/zitadel/zitadel/internal/api/http" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" - iam_model "github.com/zitadel/zitadel/internal/iam/model" + "github.com/zitadel/zitadel/internal/idp/providers/jwt" + "github.com/zitadel/zitadel/internal/query" ) type jwtRequest struct { @@ -50,12 +49,12 @@ func (l *Login) handleJWTRequest(w http.ResponseWriter, r *http.Request) { l.renderError(w, r, authReq, err) return } - idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) + idpConfig, err := l.getIDPByID(r, authReq.SelectedIDPConfigID) if err != nil { l.renderError(w, r, authReq, err) return } - if idpConfig.IsOIDC { + if idpConfig.Type != domain.IDPTypeJWT { if err != nil { l.renderError(w, r, nil, err) return @@ -64,50 +63,39 @@ func (l *Login) handleJWTRequest(w http.ResponseWriter, r *http.Request) { l.handleJWTExtraction(w, r, authReq, idpConfig) } -func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) { - token, err := getToken(r, idpConfig.JWTHeaderName) +func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, identityProvider *query.IDPTemplate) { + token, err := getToken(r, identityProvider.JWTIDPTemplate.HeaderName) if err != nil { - emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}} - if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil { + emptyTokens := &oidc.Tokens{Token: &oauth2.Token{}} + if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emptyTokens, authReq, r, err); actionErr != nil { logging.WithError(err).Error("both external user authentication and action post authentication failed") } l.renderError(w, r, authReq, err) return } - tokenClaims, err := validateToken(r.Context(), token, idpConfig) - tokens := &oidc.Tokens{IDToken: token, IDTokenClaims: tokenClaims, Token: &oauth2.Token{}} + provider, err := l.jwtProvider(r.Context(), identityProvider) if err != nil { - if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, idpConfig, err); actionErr != nil { + emptyTokens := &oidc.Tokens{Token: &oauth2.Token{}} + if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emptyTokens, authReq, r, err); actionErr != nil { logging.WithError(err).Error("both external user authentication and action post authentication failed") } l.renderError(w, r, authReq, err) return } - externalUser := l.mapTokenToLoginUser(tokens, idpConfig) - externalUser, err = l.runPostExternalAuthenticationActions(externalUser, tokens, authReq, r, idpConfig, nil) + session := &jwt.Session{Provider: provider, Tokens: &oidc.Tokens{IDToken: token, Token: &oauth2.Token{}}} + user, err := session.FetchUser(r.Context()) if err != nil { + if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens(session), authReq, r, err); actionErr != nil { + logging.WithError(err).Error("both external user authentication and action post authentication failed") + } l.renderError(w, r, authReq, err) return } - metadata := externalUser.Metadatas - err = l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r)) - if err != nil { - l.jwtExtractionUserNotFound(w, r, authReq, idpConfig, tokens, err) - return - } - if len(metadata) > 0 { - authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - _, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, metadata...) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - } + l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.jwtCallback) +} + +func (l *Login) jwtCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { redirect, err := l.redirectToJWTCallback(r.Context(), authReq) if err != nil { l.renderError(w, r, nil, err) @@ -116,73 +104,6 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth http.Redirect(w, r, redirect, http.StatusFound) } -func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, tokens *oidc.Tokens, err error) { - if errors.IsNotFound(err) { - err = nil - } - if !idpConfig.AutoRegister { - l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err) - return - } - authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - resourceOwner := l.getOrgID(r, authReq) - orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - - user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig) - user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r)) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - err = l.appendUserGrants(r.Context(), userGrants, resourceOwner) - if err != nil { - l.renderError(w, r, authReq, err) - return - } - redirect, err := l.redirectToJWTCallback(r.Context(), authReq) - if err != nil { - l.renderError(w, r, nil, err) - return - } - http.Redirect(w, r, redirect, http.StatusFound) -} - -func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error { - if len(userGrants) == 0 { - return nil - } - for _, grant := range userGrants { - _, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner) - if err != nil { - return err - } - } - return nil -} - func (l *Login) redirectToJWTCallback(ctx context.Context, authReq *domain.AuthRequest) (string, error) { redirect, err := url.Parse(l.baseURL(ctx) + EndpointJWTCallback) if err != nil { @@ -221,52 +142,18 @@ func (l *Login) handleJWTCallback(w http.ResponseWriter, r *http.Request) { l.renderError(w, r, authReq, err) return } - idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID) + idpConfig, err := l.getIDPByID(r, authReq.SelectedIDPConfigID) if err != nil { l.renderError(w, r, authReq, err) return } - if idpConfig.IsOIDC { + if idpConfig.Type != domain.IDPTypeJWT { l.renderLogin(w, r, authReq, err) return } l.renderNextStep(w, r, authReq) } -func validateToken(ctx context.Context, token string, config *iam_model.IDPConfigView) (oidc.IDTokenClaims, error) { - logging.Log("LOGIN-ADf42").Debug("begin token validation") - offset := 3 * time.Second - maxAge := time.Hour - claims := oidc.EmptyIDTokenClaims() - payload, err := oidc.ParseToken(token, claims) - if err != nil { - return nil, err - } - - if err = oidc.CheckIssuer(claims, config.JWTIssuer); err != nil { - return nil, err - } - - logging.Log("LOGIN-Dfg22").Debug("begin signature validation") - keySet := rp.NewRemoteKeySet(http.DefaultClient, config.JWTKeysEndpoint) - if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil { - return nil, err - } - - if !claims.GetExpiration().IsZero() { - if err = oidc.CheckExpiration(claims, offset); err != nil { - return nil, err - } - } - - if !claims.GetIssuedAt().IsZero() { - if err = oidc.CheckIssuedAt(claims, maxAge, offset); err != nil { - return nil, err - } - } - return claims, nil -} - func getToken(r *http.Request, headerName string) (string, error) { if headerName == "" { headerName = http_util.Authorization diff --git a/internal/api/ui/login/policy_handler.go b/internal/api/ui/login/policy_handler.go index 8b8bcd6987..e5e6336068 100644 --- a/internal/api/ui/login/policy_handler.go +++ b/internal/api/ui/login/policy_handler.go @@ -3,7 +3,6 @@ package login import ( "net/http" - iam_model "github.com/zitadel/zitadel/internal/iam/model" "github.com/zitadel/zitadel/internal/query" ) @@ -18,8 +17,8 @@ func (l *Login) getOrgDomainPolicy(r *http.Request, orgID string) (*query.Domain return l.query.DomainPolicyByOrg(r.Context(), false, orgID, false) } -func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) { - return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID) +func (l *Login) getIDPByID(r *http.Request, id string) (*query.IDPTemplate, error) { + return l.query.IDPTemplateByID(r.Context(), false, id, false) } func (l *Login) getLoginPolicy(r *http.Request, orgID string) (*query.LoginPolicy, error) { diff --git a/internal/api/ui/login/register_option_handler.go b/internal/api/ui/login/register_option_handler.go index c00ee18a2d..5488dc4918 100644 --- a/internal/api/ui/login/register_option_handler.go +++ b/internal/api/ui/login/register_option_handler.go @@ -42,7 +42,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut if err == nil { // if only external allowed with a single idp then use that if !allowed && externalAllowed && len(authReq.AllowedExternalIDPs) == 1 { - l.handleExternalRegisterByConfigID(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) + l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID) return } // if only direct registration is allowed, show the form diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index 95ea08637f..e0d42aabd7 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -69,7 +69,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage tmplChangePasswordDone: "change_password_done.html", tmplRegisterOption: "register_option.html", tmplRegister: "register.html", - tmplExternalRegisterOverview: "external_register_overview.html", tmplLogoutDone: "logout_done.html", tmplRegisterOrg: "register_org.html", tmplChangeUsername: "change_username.html", @@ -193,9 +192,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage "orgRegistrationUrl": func() string { return path.Join(r.pathPrefix, EndpointRegisterOrg) }, - "externalRegistrationUrl": func() string { - return path.Join(r.pathPrefix, EndpointExternalRegister) - }, "changeUsernameUrl": func() string { return path.Join(r.pathPrefix, EndpointChangeUsername) }, @@ -220,8 +216,8 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage "hasRegistration": func() bool { return true }, - "idpProviderClass": func(stylingType domain.IDPConfigStylingType) string { - return stylingType.GetCSSClass() + "idpProviderClass": func(idpType domain.IDPType) string { + return idpType.GetCSSClass() }, } var err error diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index f4c335d5d2..e0f776f241 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -95,8 +95,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointRegister, login.handleRegister).Methods(http.MethodGet) router.HandleFunc(EndpointRegister, login.handleRegisterCheck).Methods(http.MethodPost) router.HandleFunc(EndpointExternalRegister, login.handleExternalRegister).Methods(http.MethodGet) - router.HandleFunc(EndpointExternalRegister, login.handleExternalRegisterCheck).Methods(http.MethodPost) - router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalRegisterCallback).Methods(http.MethodGet) + router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalLoginCallback).Methods(http.MethodGet) router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet) router.HandleFunc(EndpointDynamicResources, login.handleDynamicResources).Methods(http.MethodGet) router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet) diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index d5ff684fe0..301bf33b6c 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -370,6 +370,8 @@ Errors: ExternalUserIDEmpty: Externe User ID ist leer UserDisplayNameEmpty: Benutzer Anzeige Name ist leer NoExternalUserData: Keine externe User Daten erhalten + CreationNotAllowed: Erstellen eines neuen User ist auf diesem Provider nicht erlaubt + LinkingNotAllowed: Linken eines Users ist auf diesem Provider nicht erlaubt GrantRequired: Der Login an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte melde dich bei deinem Administrator. ProjectRequired: Der Login an diese Applikation ist nicht möglich. Die Organisation des Benutzer benötigt Berechtigung auf das Projekt. Bitte melde dich bei deinem Administrator. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index b939aefb24..d36b0bf5d2 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -370,6 +370,8 @@ Errors: ExternalUserIDEmpty: External User ID is empty UserDisplayNameEmpty: User Display Name is empty NoExternalUserData: No external User Data received + CreationNotAllowed: Creation of a new user is not allowed on this Provider + LinkingNotAllowed: Linking of a user is not allowed on this Provider GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator. ProjectRequired: Login not possible. The organisation of the user must be granted to the project. Please contact your administrator. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index 226fe48627..afad746eb9 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -370,6 +370,8 @@ Errors: ExternalUserIDEmpty: L'ID de l'utilisateur externe est vide UserDisplayNameEmpty: Le nom d'affichage de l'utilisateur est vide NoExternalUserData: Aucune donnée d'utilisateur externe reçue + CreationNotAllowed : La création d'un nouvel utilisateur n'est pas autorisée sur ce fournisseur. + LinkingNotAllowed : La création d'un lien vers un utilisateur n'est pas autorisée pour ce fournisseur. GrantRequired: Connexion impossible. L'utilisateur doit avoir au moins une subvention sur l'application. Veuillez contacter votre administrateur. ProjectRequired: Connexion impossible. L'organisation de l'utilisateur doit être accordée au projet. Veuillez contacter votre administrateur. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index 9aa67f0519..5eacf81b02 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -370,6 +370,8 @@ Errors: ExternalUserIDEmpty: L'ID utente esterno è vuoto UserDisplayNameEmpty: Il nome visualizzato dell'utente è vuoto NoExternalUserData: Nessun dato utente esterno ricevuto + CreationNotAllowed: La creazione di un nuovo utente non è consentita su questo provider. + LinkingNotAllowed: Il collegamento di un utente non è consentito su questo provider. GrantRequired: Accesso non possibile. L'utente deve avere almeno una sovvenzione sull'applicazione. Contatta il tuo amministratore. ProjectRequired: Accesso non possibile. L'organizzazione dell'utente deve essere concessa al progetto. Contatta il tuo amministratore. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index a9e7c5ecd7..2650463d6c 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -370,6 +370,8 @@ Errors: ExternalUserIDEmpty: Identyfikator użytkownika zewnętrznego jest pusty UserDisplayNameEmpty: Nazwa wyświetlana użytkownika jest pusta NoExternalUserData: Nie otrzymano danych użytkownika zewnętrznego + CreationNotAllowed: Tworzenie nowego użytkownika nie jest dozwolone w tym Providencie + LinkingNotAllowed: Linkowanie użytkownika nie jest dozwolone na tym Providencie GrantRequired: Logowanie nie jest możliwe. Użytkownik musi posiadać przynajmniej jedno uprawnienie w aplikacji. Skontaktuj się z administratorem. ProjectRequired: Logowanie nie jest możliwe. Organizacja użytkownika musi zostać udzielona projektowi. Skontaktuj się z administratorem. IdentityProvider: diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index 078b098408..da380c5ab4 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -370,6 +370,8 @@ Errors: ExternalUserIDEmpty: 外部用户 ID 为空 UserDisplayNameEmpty: 用户显示名称为空 NoExternalUserData: 未收到外部用户数据 + CreationNotAllowed: 不允许在该供应商上创建新用户 + LinkingNotAllowed: 在此提供者上不允许链接一个用户 GrantRequired: 无法登录,用户需要在应用程序上拥有至少一项授权,请联系您的管理员。 ProjectRequired: 无法登录,用户的组织必须授予项目,请联系您的管理员。 IdentityProvider: diff --git a/internal/api/ui/login/static/templates/external_not_found_option.html b/internal/api/ui/login/static/templates/external_not_found_option.html index fe30fca7a3..9069261e8e 100644 --- a/internal/api/ui/login/static/templates/external_not_found_option.html +++ b/internal/api/ui/login/static/templates/external_not_found_option.html @@ -118,13 +118,17 @@ + {{ if .IsLinkingAllowed }} - + {{ end }} + + {{ if .IsCreationAllowed }} + {{ end }} diff --git a/internal/api/ui/login/static/templates/external_register_overview.html b/internal/api/ui/login/static/templates/external_register_overview.html deleted file mode 100644 index 07875a4137..0000000000 --- a/internal/api/ui/login/static/templates/external_register_overview.html +++ /dev/null @@ -1,128 +0,0 @@ -{{template "main-top" .}} - -
-

{{t "ExternalRegistrationUserOverview.Title"}}

-

{{t "ExternalRegistrationUserOverview.Description"}}

-
- - -
- - {{ .CSRF }} - - - - - - - - - - -
- -
-
- - -
-
- - -
-
- -
- -
- - {{if .ShowUsernameSuffix}} - @{{.PrimaryDomain}} - {{end}} -
-
- -
- - -
- -
- - -
- -
-
- - -
-
- - {{ if or .TOSLink .PrivacyLink }} -
- - {{ if .TOSLink }} -
- - -
- {{end}} - {{ if and .TOSLink .PrivacyLink }} -
- {{end}} - {{ if .PrivacyLink }} -
- - -
- {{end}} -
- {{ end }} -
- - {{template "error-message" .}} - -
- - {{t "ExternalRegistrationUserOverview.BackButtonText"}} - - - -
-
- - - - - -{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/login.html b/internal/api/ui/login/static/templates/login.html index b9ca4dcb4c..c811e001cb 100644 --- a/internal/api/ui/login/static/templates/login.html +++ b/internal/api/ui/login/static/templates/login.html @@ -47,7 +47,7 @@ {{ $reqid := .AuthReqID}} {{range $provider := .IDPProviders}} + class="lgn-idp {{idpProviderClass $provider.IDPType}}"> {{$provider.Name}} diff --git a/internal/api/ui/login/static/templates/register_option.html b/internal/api/ui/login/static/templates/register_option.html index df7d6b4cc4..8ff122d9e0 100644 --- a/internal/api/ui/login/static/templates/register_option.html +++ b/internal/api/ui/login/static/templates/register_option.html @@ -27,7 +27,7 @@ {{ $reqid := .AuthReqID}} {{range $provider := .IDPProviders}} + class="lgn-idp {{idpProviderClass $provider.IDPType}}"> {{$provider.Name}} diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 62271fd5db..edbd01e98f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -17,8 +17,6 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" v1 "github.com/zitadel/zitadel/internal/eventstore/v1" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model" "github.com/zitadel/zitadel/internal/id" project_view_model "github.com/zitadel/zitadel/internal/project/repository/view/model" "github.com/zitadel/zitadel/internal/query" @@ -81,7 +79,7 @@ type lockoutPolicyViewProvider interface { } type idpProviderViewProvider interface { - IDPProvidersByAggregateIDAndState(string, string, iam_model.IDPConfigState) ([]*iam_view_model.IDPProviderView, error) + IDPLoginPolicyLinks(context.Context, string, *query.IDPLoginPolicyLinksSearchQuery, bool) (*query.IDPLoginPolicyLinks, error) } type idpUserLinksProvider interface { @@ -554,13 +552,11 @@ func (repo *AuthRequestRepo) getLoginPolicyAndIDPProviders(ctx context.Context, if !policy.AllowExternalIDPs { return policy, nil, nil } - idpProviders, err := getLoginPolicyIDPProviders(repo.IDPProviderViewProvider, authz.GetInstance(ctx).InstanceID(), orgID, policy.IsDefault) + idpProviders, err := getLoginPolicyIDPProviders(ctx, repo.IDPProviderViewProvider, authz.GetInstance(ctx).InstanceID(), orgID, policy.IsDefault) if err != nil { return nil, nil, err } - - providers := iam_model.IdpProviderViewsToDomain(idpProviders) - return policy, providers, nil + return policy, idpProviders, nil } func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.AuthRequest) error { @@ -850,16 +846,32 @@ func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *domain.AuthReques } func (repo *AuthRequestRepo) checkExternalUserLogin(ctx context.Context, request *domain.AuthRequest, idpConfigID, externalUserID string) (err error) { - var externalIDP *user_view_model.ExternalIDPView - if request.RequestedOrgID != "" { - externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, request.RequestedOrgID, request.InstanceID) - } else { - externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID, request.InstanceID) - } + idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpConfigID) if err != nil { return err } - user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, externalIDP.UserID, false) + externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID) + if err != nil { + return err + } + queries := []query.SearchQuery{ + idQuery, externalIDQuery, + } + if request.RequestedOrgID != "" { + orgIDQuery, err := query.NewIDPUserLinksResourceOwnerSearchQuery(idpConfigID) + if err != nil { + return err + } + queries = append(queries, orgIDQuery) + } + links, err := repo.Query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + if err != nil { + return err + } + if len(links.Links) != 1 { + return errors.ThrowNotFound(nil, "AUTH-Sf8sd", "Errors.ExternalIDP.NotFound") + } + user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, links.Links[0].UserID, false) if err != nil { return err } @@ -1233,19 +1245,25 @@ func setOrgID(ctx context.Context, orgViewProvider orgViewProvider, request *dom return nil } -func getLoginPolicyIDPProviders(provider idpProviderViewProvider, iamID, orgID string, defaultPolicy bool) ([]*iam_model.IDPProviderView, error) { - if defaultPolicy { - idpProviders, err := provider.IDPProvidersByAggregateIDAndState(iamID, iamID, iam_model.IDPConfigStateActive) - if err != nil { - return nil, err - } - return iam_view_model.IDPProviderViewsToModel(idpProviders), nil +func getLoginPolicyIDPProviders(ctx context.Context, provider idpProviderViewProvider, iamID, orgID string, defaultPolicy bool) ([]*domain.IDPProvider, error) { + resourceOwner := iamID + if !defaultPolicy { + resourceOwner = orgID } - idpProviders, err := provider.IDPProvidersByAggregateIDAndState(orgID, iamID, iam_model.IDPConfigStateActive) + links, err := provider.IDPLoginPolicyLinks(ctx, resourceOwner, &query.IDPLoginPolicyLinksSearchQuery{}, false) if err != nil { return nil, err } - return iam_view_model.IDPProviderViewsToModel(idpProviders), nil + providers := make([]*domain.IDPProvider, len(links.Links)) + for i, link := range links.Links { + providers[i] = &domain.IDPProvider{ + Type: link.OwnerType, + IDPConfigID: link.IDPID, + Name: link.IDPName, + IDPType: link.IDPType, + } + } + return providers, nil } func checkVerificationTimeMaxAge(verificationTime time.Time, lifetime time.Duration, request *domain.AuthRequest) bool { diff --git a/internal/auth/repository/eventsourcing/eventstore/org.go b/internal/auth/repository/eventsourcing/eventstore/org.go index d8d19e9ef9..fa15bfc2cc 100644 --- a/internal/auth/repository/eventsourcing/eventstore/org.go +++ b/internal/auth/repository/eventsourcing/eventstore/org.go @@ -22,14 +22,6 @@ type OrgRepository struct { Query *query.Queries } -func (repo *OrgRepository) GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error) { - idpConfig, err := repo.View.IDPConfigByID(idpConfigID, authz.GetInstance(ctx).InstanceID()) - if err != nil { - return nil, err - } - return iam_view_model.IDPConfigViewToModel(idpConfig), nil -} - func (repo *OrgRepository) GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error) { policy, err := repo.Query.PasswordComplexityPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false) if err != nil { diff --git a/internal/auth/repository/eventsourcing/handler/handler.go b/internal/auth/repository/eventsourcing/handler/handler.go index 6eb3a836c6..161500de3a 100644 --- a/internal/auth/repository/eventsourcing/handler/handler.go +++ b/internal/auth/repository/eventsourcing/handler/handler.go @@ -41,14 +41,6 @@ func Register(ctx context.Context, configs Configs, bulkLimit, errorCount uint64 handler{view, bulkLimit, configs.cycleDuration("UserSession"), errorCount, es}, queries), newToken(ctx, handler{view, bulkLimit, configs.cycleDuration("Token"), errorCount, es}), - newIDPConfig(ctx, - handler{view, bulkLimit, configs.cycleDuration("IDPConfig"), errorCount, es}), - newIDPProvider(ctx, - handler{view, bulkLimit, configs.cycleDuration("IDPProvider"), errorCount, es}, - systemDefaults, queries), - newExternalIDP(ctx, - handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount, es}, - systemDefaults, queries), newRefreshToken(ctx, handler{view, bulkLimit, configs.cycleDuration("RefreshToken"), errorCount, es}), newOrgProjectMapping(ctx, handler{view, bulkLimit, configs.cycleDuration("OrgProjectMapping"), errorCount, es}), } diff --git a/internal/auth/repository/eventsourcing/handler/idp_config.go b/internal/auth/repository/eventsourcing/handler/idp_config.go deleted file mode 100644 index 342daa5e9a..0000000000 --- a/internal/auth/repository/eventsourcing/handler/idp_config.go +++ /dev/null @@ -1,142 +0,0 @@ -package handler - -import ( - "context" - - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/eventstore" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/eventstore/v1/query" - "github.com/zitadel/zitadel/internal/eventstore/v1/spooler" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model" - "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/org" -) - -const ( - idpConfigTable = "auth.idp_configs2" -) - -type IDPConfig struct { - handler - subscription *v1.Subscription -} - -func newIDPConfig(ctx context.Context, h handler) *IDPConfig { - idpConfig := &IDPConfig{ - handler: h, - } - - idpConfig.subscribe(ctx) - - return idpConfig -} - -func (i *IDPConfig) subscribe(ctx context.Context) { - i.subscription = i.es.Subscribe(i.AggregateTypes()...) - go func() { - for event := range i.subscription.Events { - query.ReduceEvent(ctx, i, event) - } - }() -} - -func (i *IDPConfig) ViewModel() string { - return idpConfigTable -} - -func (i *IDPConfig) Subscription() *v1.Subscription { - return i.subscription -} - -func (_ *IDPConfig) AggregateTypes() []models.AggregateType { - return []models.AggregateType{org.AggregateType, instance.AggregateType} -} - -func (i *IDPConfig) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := i.view.GetLatestIDPConfigSequence(instanceID) - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (i *IDPConfig) EventQuery(instanceIDs []string) (*models.SearchQuery, error) { - sequences, err := i.view.GetLatestIDPConfigSequences(instanceIDs) - if err != nil { - return nil, err - } - return newSearchQuery(sequences, i.AggregateTypes(), instanceIDs), nil -} - -func (i *IDPConfig) Reduce(event *models.Event) (err error) { - switch event.AggregateType { - case org.AggregateType: - err = i.processIdpConfig(iam_model.IDPProviderTypeOrg, event) - case instance.AggregateType: - err = i.processIdpConfig(iam_model.IDPProviderTypeSystem, event) - } - return err -} - -func (i *IDPConfig) processIdpConfig(providerType iam_model.IDPProviderType, event *models.Event) (err error) { - idp := new(iam_view_model.IDPConfigView) - switch eventstore.EventType(event.Type) { - case org.IDPConfigAddedEventType, - instance.IDPConfigAddedEventType: - err = idp.AppendEvent(providerType, event) - case org.IDPConfigChangedEventType, instance.IDPConfigChangedEventType, - org.IDPOIDCConfigAddedEventType, instance.IDPOIDCConfigAddedEventType, - org.IDPOIDCConfigChangedEventType, instance.IDPOIDCConfigChangedEventType, - org.IDPJWTConfigAddedEventType, instance.IDPJWTConfigAddedEventType, - org.IDPJWTConfigChangedEventType, instance.IDPJWTConfigChangedEventType: - err = idp.SetData(event) - if err != nil { - return err - } - idp, err = i.view.IDPConfigByID(idp.IDPConfigID, event.InstanceID) - if err != nil { - return err - } - err = idp.AppendEvent(providerType, event) - case org.IDPConfigDeactivatedEventType, instance.IDPConfigDeactivatedEventType, - org.IDPConfigReactivatedEventType, instance.IDPConfigReactivatedEventType: - err = idp.SetData(event) - if err != nil { - return err - } - idp, err = i.view.IDPConfigByID(idp.IDPConfigID, event.InstanceID) - if err != nil { - return err - } - err = idp.AppendEvent(providerType, event) - case org.IDPConfigRemovedEventType, instance.IDPConfigRemovedEventType: - err = idp.SetData(event) - if err != nil { - return err - } - return i.view.DeleteIDPConfig(idp.IDPConfigID, event) - case instance.InstanceRemovedEventType: - return i.view.DeleteInstanceIDPs(event) - case org.OrgRemovedEventType: - return i.view.UpdateOrgOwnerRemovedIDPs(event) - default: - return i.view.ProcessedIDPConfigSequence(event) - } - if err != nil { - return err - } - return i.view.PutIDPConfig(idp, event) -} - -func (i *IDPConfig) OnError(event *models.Event, err error) error { - logging.WithFields("id", event.AggregateID).WithError(err).Warn("something went wrong in idp config handler") - return spooler.HandleError(event, err, i.view.GetLatestIDPConfigFailedEvent, i.view.ProcessedIDPConfigFailedEvent, i.view.ProcessedIDPConfigSequence, i.errorCountUntilSkip) -} - -func (i *IDPConfig) OnSuccess(instanceIDs []string) error { - return spooler.HandleSuccess(i.view.UpdateIDPConfigSpoolerRunTimestamp, instanceIDs) -} diff --git a/internal/auth/repository/eventsourcing/handler/idp_providers.go b/internal/auth/repository/eventsourcing/handler/idp_providers.go deleted file mode 100644 index 72b3d06514..0000000000 --- a/internal/auth/repository/eventsourcing/handler/idp_providers.go +++ /dev/null @@ -1,204 +0,0 @@ -package handler - -import ( - "context" - - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/config/systemdefaults" - "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/eventstore" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/eventstore/v1/query" - "github.com/zitadel/zitadel/internal/eventstore/v1/spooler" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model" - query2 "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/org" -) - -const ( - idpProviderTable = "auth.idp_providers2" -) - -type IDPProvider struct { - handler - systemDefaults systemdefaults.SystemDefaults - subscription *v1.Subscription - queries *query2.Queries -} - -func newIDPProvider( - ctx context.Context, - h handler, - defaults systemdefaults.SystemDefaults, - queries *query2.Queries, -) *IDPProvider { - idpProvider := &IDPProvider{ - handler: h, - systemDefaults: defaults, - queries: queries, - } - - idpProvider.subscribe(ctx) - - return idpProvider -} - -func (i *IDPProvider) subscribe(ctx context.Context) { - i.subscription = i.es.Subscribe(i.AggregateTypes()...) - go func() { - for event := range i.subscription.Events { - query.ReduceEvent(ctx, i, event) - } - }() -} - -func (i *IDPProvider) ViewModel() string { - return idpProviderTable -} - -func (i *IDPProvider) Subscription() *v1.Subscription { - return i.subscription -} - -func (_ *IDPProvider) AggregateTypes() []models.AggregateType { - return []es_models.AggregateType{instance.AggregateType, org.AggregateType} -} - -func (i *IDPProvider) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := i.view.GetLatestIDPProviderSequence(instanceID) - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (i *IDPProvider) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) { - sequences, err := i.view.GetLatestIDPProviderSequences(instanceIDs) - if err != nil { - return nil, err - } - - return newSearchQuery(sequences, i.AggregateTypes(), instanceIDs), nil -} - -func (i *IDPProvider) Reduce(event *models.Event) (err error) { - switch event.AggregateType { - case instance.AggregateType, org.AggregateType: - err = i.processIdpProvider(event) - } - return err -} - -func (i *IDPProvider) processIdpProvider(event *models.Event) (err error) { - provider := new(iam_view_model.IDPProviderView) - switch eventstore.EventType(event.Type) { - case instance.LoginPolicyIDPProviderAddedEventType, org.LoginPolicyIDPProviderAddedEventType: - err = provider.AppendEvent(event) - if err != nil { - return err - } - err = i.fillData(provider) - case instance.LoginPolicyIDPProviderRemovedEventType, instance.LoginPolicyIDPProviderCascadeRemovedEventType, - org.LoginPolicyIDPProviderRemovedEventType, org.LoginPolicyIDPProviderCascadeRemovedEventType: - err = provider.SetData(event) - if err != nil { - return err - } - return i.view.DeleteIDPProvider(event.AggregateID, provider.IDPConfigID, event.InstanceID, event) - case instance.IDPConfigChangedEventType, org.IDPConfigChangedEventType: - esConfig := new(iam_view_model.IDPConfigView) - providerType := iam_model.IDPProviderTypeSystem - if event.AggregateID != event.InstanceID { - providerType = iam_model.IDPProviderTypeOrg - } - err = esConfig.AppendEvent(providerType, event) - if err != nil { - return err - } - providers, err := i.view.IDPProvidersByIDPConfigID(esConfig.IDPConfigID, event.InstanceID) - if err != nil { - return err - } - config := new(query2.IDP) - if event.AggregateID == event.InstanceID { - config, err = i.getDefaultIDPConfig(event.InstanceID, esConfig.IDPConfigID) - } else { - config, err = i.getOrgIDPConfig(event.InstanceID, event.AggregateID, esConfig.IDPConfigID) - } - if err != nil { - return err - } - for _, provider := range providers { - i.fillConfigData(provider, config) - } - return i.view.PutIDPProviders(event, providers...) - case org.LoginPolicyRemovedEventType: - return i.view.DeleteIDPProvidersByAggregateID(event.AggregateID, event.InstanceID, event) - case instance.InstanceRemovedEventType: - return i.view.DeleteInstanceIDPProviders(event) - case org.OrgRemovedEventType: - return i.view.UpdateOrgOwnerRemovedIDPProviders(event) - default: - return i.view.ProcessedIDPProviderSequence(event) - } - if err != nil { - return err - } - return i.view.PutIDPProvider(provider, event) -} - -func (i *IDPProvider) fillData(provider *iam_view_model.IDPProviderView) (err error) { - var config *query2.IDP - if provider.IDPProviderType == int32(iam_model.IDPProviderTypeSystem) { - config, err = i.getDefaultIDPConfig(provider.InstanceID, provider.IDPConfigID) - } else { - config, err = i.getOrgIDPConfig(provider.InstanceID, provider.AggregateID, provider.IDPConfigID) - } - if err != nil { - return err - } - i.fillConfigData(provider, config) - return nil -} - -func (i *IDPProvider) fillConfigData(provider *iam_view_model.IDPProviderView, config *query2.IDP) { - provider.Name = config.Name - provider.StylingType = int32(config.StylingType) - if config.OIDCIDP != nil { - provider.IDPConfigType = int32(domain.IDPConfigTypeOIDC) - } else if config.JWTIDP != nil { - provider.IDPConfigType = int32(domain.IDPConfigTypeJWT) - } - switch config.State { - case domain.IDPConfigStateActive: - provider.IDPState = int32(iam_model.IDPConfigStateActive) - case domain.IDPConfigStateInactive: - provider.IDPState = int32(iam_model.IDPConfigStateActive) - case domain.IDPConfigStateRemoved: - provider.IDPState = int32(iam_model.IDPConfigStateRemoved) - default: - provider.IDPState = int32(iam_model.IDPConfigStateActive) - } -} - -func (i *IDPProvider) OnError(event *es_models.Event, err error) error { - logging.WithFields("id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler") - return spooler.HandleError(event, err, i.view.GetLatestIDPProviderFailedEvent, i.view.ProcessedIDPProviderFailedEvent, i.view.ProcessedIDPProviderSequence, i.errorCountUntilSkip) -} - -func (i *IDPProvider) OnSuccess(instanceIDs []string) error { - return spooler.HandleSuccess(i.view.UpdateIDPProviderSpoolerRunTimestamp, instanceIDs) -} - -func (i *IDPProvider) getOrgIDPConfig(instanceID, aggregateID, idpConfigID string) (*query2.IDP, error) { - return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, aggregateID, false) -} - -func (i *IDPProvider) getDefaultIDPConfig(instanceID, idpConfigID string) (*query2.IDP, error) { - return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, instanceID, false) -} diff --git a/internal/auth/repository/eventsourcing/handler/user_external_idps.go b/internal/auth/repository/eventsourcing/handler/user_external_idps.go deleted file mode 100644 index 4733cca4a8..0000000000 --- a/internal/auth/repository/eventsourcing/handler/user_external_idps.go +++ /dev/null @@ -1,194 +0,0 @@ -package handler - -import ( - "context" - - "github.com/zitadel/logging" - - "github.com/zitadel/zitadel/internal/config/systemdefaults" - caos_errs "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/eventstore" - v1 "github.com/zitadel/zitadel/internal/eventstore/v1" - es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/eventstore/v1/query" - "github.com/zitadel/zitadel/internal/eventstore/v1/spooler" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model" - query2 "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/internal/repository/instance" - "github.com/zitadel/zitadel/internal/repository/org" - "github.com/zitadel/zitadel/internal/repository/user" - usr_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" -) - -const ( - externalIDPTable = "auth.user_external_idps2" -) - -type ExternalIDP struct { - handler - systemDefaults systemdefaults.SystemDefaults - subscription *v1.Subscription - queries *query2.Queries -} - -func newExternalIDP( - ctx context.Context, - handler handler, - defaults systemdefaults.SystemDefaults, - queries *query2.Queries, -) *ExternalIDP { - h := &ExternalIDP{ - handler: handler, - systemDefaults: defaults, - queries: queries, - } - - h.subscribe(ctx) - - return h -} - -func (i *ExternalIDP) subscribe(ctx context.Context) { - i.subscription = i.es.Subscribe(i.AggregateTypes()...) - go func() { - for event := range i.subscription.Events { - query.ReduceEvent(ctx, i, event) - } - }() -} - -func (i *ExternalIDP) ViewModel() string { - return externalIDPTable -} - -func (i *ExternalIDP) Subscription() *v1.Subscription { - return i.subscription -} - -func (_ *ExternalIDP) AggregateTypes() []es_models.AggregateType { - return []es_models.AggregateType{user.AggregateType, instance.AggregateType, org.AggregateType} -} - -func (i *ExternalIDP) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := i.view.GetLatestExternalIDPSequence(instanceID) - if err != nil { - return 0, err - } - return sequence.CurrentSequence, nil -} - -func (i *ExternalIDP) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) { - sequences, err := i.view.GetLatestExternalIDPSequences(instanceIDs) - if err != nil { - return nil, err - } - return newSearchQuery(sequences, i.AggregateTypes(), instanceIDs), nil -} - -func (i *ExternalIDP) Reduce(event *es_models.Event) (err error) { - switch event.AggregateType { - case user.AggregateType: - err = i.processUser(event) - case instance.AggregateType, org.AggregateType: - err = i.processIdpConfig(event) - } - return err -} - -func (i *ExternalIDP) processUser(event *es_models.Event) (err error) { - externalIDP := new(usr_view_model.ExternalIDPView) - switch eventstore.EventType(event.Type) { - case user.UserIDPLinkAddedType: - err = externalIDP.AppendEvent(event) - if err != nil { - return err - } - err = i.fillData(externalIDP) - case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType: - err = externalIDP.SetData(event) - if err != nil { - return err - } - return i.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.InstanceID, event) - case user.UserRemovedType: - return i.view.DeleteExternalIDPsByUserID(event.AggregateID, event.InstanceID, event) - default: - return i.view.ProcessedExternalIDPSequence(event) - } - if err != nil { - return err - } - return i.view.PutExternalIDP(externalIDP, event) -} - -func (i *ExternalIDP) processIdpConfig(event *es_models.Event) (err error) { - switch eventstore.EventType(event.Type) { - case instance.IDPConfigChangedEventType, org.IDPConfigChangedEventType: - configView := new(iam_view_model.IDPConfigView) - var config *query2.IDP - if eventstore.EventType(event.Type) == instance.IDPConfigChangedEventType { - err = configView.AppendEvent(iam_model.IDPProviderTypeSystem, event) - } else { - err = configView.AppendEvent(iam_model.IDPProviderTypeOrg, event) - } - if err != nil { - return err - } - exterinalIDPs, err := i.view.ExternalIDPsByIDPConfigID(configView.IDPConfigID, event.InstanceID) - if err != nil { - return err - } - if event.AggregateType == instance.AggregateType { - config, err = i.getDefaultIDPConfig(event.InstanceID, configView.IDPConfigID) - } else { - config, err = i.getOrgIDPConfig(event.InstanceID, event.AggregateID, configView.IDPConfigID) - } - if err != nil { - return err - } - for _, provider := range exterinalIDPs { - i.fillConfigData(provider, config) - } - return i.view.PutExternalIDPs(event, exterinalIDPs...) - case instance.InstanceRemovedEventType: - return i.view.DeleteInstanceExternalIDPs(event) - case org.OrgRemovedEventType: - return i.view.UpdateOrgOwnerRemovedExternalIDPs(event) - default: - return i.view.ProcessedExternalIDPSequence(event) - } -} - -func (i *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error { - config, err := i.getOrgIDPConfig(externalIDP.InstanceID, externalIDP.ResourceOwner, externalIDP.IDPConfigID) - if caos_errs.IsNotFound(err) { - config, err = i.getDefaultIDPConfig(externalIDP.InstanceID, externalIDP.IDPConfigID) - } - if err != nil { - return err - } - i.fillConfigData(externalIDP, config) - return nil -} - -func (i *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *query2.IDP) { - externalIDP.IDPName = config.Name -} - -func (i *ExternalIDP) OnError(event *es_models.Event, err error) error { - logging.WithFields("id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler") - return spooler.HandleError(event, err, i.view.GetLatestExternalIDPFailedEvent, i.view.ProcessedExternalIDPFailedEvent, i.view.ProcessedExternalIDPSequence, i.errorCountUntilSkip) -} - -func (i *ExternalIDP) OnSuccess(instanceIDs []string) error { - return spooler.HandleSuccess(i.view.UpdateExternalIDPSpoolerRunTimestamp, instanceIDs) -} - -func (i *ExternalIDP) getOrgIDPConfig(instanceID, aggregateID, idpConfigID string) (*query2.IDP, error) { - return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, aggregateID, false) -} - -func (i *ExternalIDP) getDefaultIDPConfig(instanceID, idpConfigID string) (*query2.IDP, error) { - return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, instanceID, false) -} diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index bc19fdab6c..34df290c7a 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -80,7 +80,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c UserViewProvider: view, UserCommandProvider: command, UserEventProvider: &userRepo, - IDPProviderViewProvider: view, + IDPProviderViewProvider: queries, IDPUserLinksProvider: queries, LockoutPolicyViewProvider: queries, LoginPolicyViewProvider: queries, diff --git a/internal/auth/repository/eventsourcing/view/external_idps.go b/internal/auth/repository/eventsourcing/view/external_idps.go deleted file mode 100644 index 3379b70ae4..0000000000 --- a/internal/auth/repository/eventsourcing/view/external_idps.go +++ /dev/null @@ -1,97 +0,0 @@ -package view - -import ( - "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - "github.com/zitadel/zitadel/internal/user/repository/view" - "github.com/zitadel/zitadel/internal/user/repository/view/model" - global_view "github.com/zitadel/zitadel/internal/view/repository" -) - -const ( - externalIDPTable = "auth.user_external_idps2" -) - -func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID, instanceID string) (*model.ExternalIDPView, error) { - return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID, instanceID) -} - -func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner, instanceID string) (*model.ExternalIDPView, error) { - return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner, instanceID) -} - -func (v *View) ExternalIDPsByIDPConfigID(idpConfigID, instanceID string) ([]*model.ExternalIDPView, error) { - return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID, instanceID) -} - -func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, event *models.Event) error { - err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP) - if err != nil { - return err - } - return v.ProcessedExternalIDPSequence(event) -} - -func (v *View) PutExternalIDPs(event *models.Event, externalIDPs ...*model.ExternalIDPView) error { - err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...) - if err != nil { - return err - } - return v.ProcessedExternalIDPSequence(event) -} - -func (v *View) DeleteExternalIDP(externalUserID, idpConfigID, instanceID string, event *models.Event) error { - err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID, instanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedExternalIDPSequence(event) -} - -func (v *View) DeleteExternalIDPsByUserID(userID, instanceID string, event *models.Event) error { - err := view.DeleteExternalIDPsByUserID(v.Db, externalIDPTable, userID, instanceID) - if err != nil { - return err - } - return v.ProcessedExternalIDPSequence(event) -} - -func (v *View) DeleteInstanceExternalIDPs(event *models.Event) error { - err := view.DeleteInstanceExternalIDPs(v.Db, externalIDPTable, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedExternalIDPSequence(event) -} - -func (v *View) UpdateOrgOwnerRemovedExternalIDPs(event *models.Event) error { - err := view.UpdateOrgOwnerRemovedExternalIDPs(v.Db, externalIDPTable, event.InstanceID, event.ResourceOwner) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedExternalIDPSequence(event) -} - -func (v *View) GetLatestExternalIDPSequence(instanceID string) (*global_view.CurrentSequence, error) { - return v.latestSequence(externalIDPTable, instanceID) -} - -func (v *View) GetLatestExternalIDPSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) { - return v.latestSequences(externalIDPTable, instanceIDs) -} - -func (v *View) ProcessedExternalIDPSequence(event *models.Event) error { - return v.saveCurrentSequence(externalIDPTable, event) -} - -func (v *View) UpdateExternalIDPSpoolerRunTimestamp(instanceIDs []string) error { - return v.updateSpoolerRunSequence(externalIDPTable, instanceIDs) -} - -func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64, instanceID string) (*global_view.FailedEvent, error) { - return v.latestFailedEvent(externalIDPTable, instanceID, sequence) -} - -func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/auth/repository/eventsourcing/view/idp_configs.go b/internal/auth/repository/eventsourcing/view/idp_configs.go deleted file mode 100644 index 439b637112..0000000000 --- a/internal/auth/repository/eventsourcing/view/idp_configs.go +++ /dev/null @@ -1,82 +0,0 @@ -package view - -import ( - "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - "github.com/zitadel/zitadel/internal/iam/repository/view" - iam_es_model "github.com/zitadel/zitadel/internal/iam/repository/view/model" - global_view "github.com/zitadel/zitadel/internal/view/repository" -) - -const ( - idpConfigTable = "auth.idp_configs2" -) - -func (v *View) IDPConfigByID(idpID, instanceID string) (*iam_es_model.IDPConfigView, error) { - return view.IDPByID(v.Db, idpConfigTable, idpID, instanceID) -} - -func (v *View) GetIDPConfigsByAggregateID(aggregateID, instanceID string) ([]*iam_es_model.IDPConfigView, error) { - return view.GetIDPConfigsByAggregateID(v.Db, idpConfigTable, aggregateID, instanceID) -} - -func (v *View) SearchIDPConfigs(request *iam_model.IDPConfigSearchRequest) ([]*iam_es_model.IDPConfigView, uint64, error) { - return view.SearchIDPs(v.Db, idpConfigTable, request) -} - -func (v *View) PutIDPConfig(idp *iam_es_model.IDPConfigView, event *models.Event) error { - err := view.PutIDP(v.Db, idpConfigTable, idp) - if err != nil { - return err - } - return v.ProcessedIDPConfigSequence(event) -} - -func (v *View) DeleteIDPConfig(idpID string, event *models.Event) error { - err := view.DeleteIDP(v.Db, idpConfigTable, idpID, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedIDPConfigSequence(event) -} - -func (v *View) DeleteInstanceIDPs(event *models.Event) error { - err := view.DeleteInstanceIDPs(v.Db, idpConfigTable, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedIDPConfigSequence(event) -} - -func (v *View) UpdateOrgOwnerRemovedIDPs(event *models.Event) error { - err := view.UpdateOrgOwnerRemovedIDPs(v.Db, idpConfigTable, event.InstanceID, event.AggregateID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedIDPConfigSequence(event) -} - -func (v *View) GetLatestIDPConfigSequence(instanceID string) (*global_view.CurrentSequence, error) { - return v.latestSequence(idpConfigTable, instanceID) -} - -func (v *View) GetLatestIDPConfigSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) { - return v.latestSequences(idpConfigTable, instanceIDs) -} - -func (v *View) ProcessedIDPConfigSequence(event *models.Event) error { - return v.saveCurrentSequence(idpConfigTable, event) -} - -func (v *View) UpdateIDPConfigSpoolerRunTimestamp(instanceIDs []string) error { - return v.updateSpoolerRunSequence(idpConfigTable, instanceIDs) -} - -func (v *View) GetLatestIDPConfigFailedEvent(sequence uint64, instanceID string) (*global_view.FailedEvent, error) { - return v.latestFailedEvent(idpConfigTable, instanceID, sequence) -} - -func (v *View) ProcessedIDPConfigFailedEvent(failedEvent *global_view.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/auth/repository/eventsourcing/view/idp_providers.go b/internal/auth/repository/eventsourcing/view/idp_providers.go deleted file mode 100644 index ead8f7c0a0..0000000000 --- a/internal/auth/repository/eventsourcing/view/idp_providers.go +++ /dev/null @@ -1,102 +0,0 @@ -package view - -import ( - "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/internal/eventstore/v1/models" - iam_model "github.com/zitadel/zitadel/internal/iam/model" - "github.com/zitadel/zitadel/internal/iam/repository/view" - "github.com/zitadel/zitadel/internal/iam/repository/view/model" - global_view "github.com/zitadel/zitadel/internal/view/repository" -) - -const ( - idpProviderTable = "auth.idp_providers2" -) - -func (v *View) IDPProviderByAggregateAndIDPConfigID(aggregateID, idpConfigID, instanceID string) (*model.IDPProviderView, error) { - return view.GetIDPProviderByAggregateIDAndConfigID(v.Db, idpProviderTable, aggregateID, idpConfigID, instanceID) -} - -func (v *View) IDPProvidersByIDPConfigID(idpConfigID, instanceID string) ([]*model.IDPProviderView, error) { - return view.IDPProvidersByIdpConfigID(v.Db, idpProviderTable, idpConfigID, instanceID) -} - -func (v *View) IDPProvidersByAggregateIDAndState(aggregateID, instanceID string, idpConfigState iam_model.IDPConfigState) ([]*model.IDPProviderView, error) { - return view.IDPProvidersByAggregateIDAndState(v.Db, idpProviderTable, aggregateID, instanceID, idpConfigState) -} - -func (v *View) SearchIDPProviders(request *iam_model.IDPProviderSearchRequest) ([]*model.IDPProviderView, uint64, error) { - return view.SearchIDPProviders(v.Db, idpProviderTable, request) -} - -func (v *View) PutIDPProvider(provider *model.IDPProviderView, event *models.Event) error { - err := view.PutIDPProvider(v.Db, idpProviderTable, provider) - if err != nil { - return err - } - return v.ProcessedIDPProviderSequence(event) -} - -func (v *View) PutIDPProviders(event *models.Event, providers ...*model.IDPProviderView) error { - err := view.PutIDPProviders(v.Db, idpProviderTable, providers...) - if err != nil { - return err - } - return v.ProcessedIDPProviderSequence(event) -} - -func (v *View) DeleteIDPProvider(aggregateID, idpConfigID, instanceID string, event *models.Event) error { - err := view.DeleteIDPProvider(v.Db, idpProviderTable, aggregateID, idpConfigID, instanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedIDPProviderSequence(event) -} - -func (v *View) DeleteIDPProvidersByAggregateID(aggregateID, instanceID string, event *models.Event) error { - err := view.DeleteIDPProvidersByAggregateID(v.Db, idpProviderTable, aggregateID, instanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedIDPProviderSequence(event) -} - -func (v *View) DeleteInstanceIDPProviders(event *models.Event) error { - err := view.DeleteInstanceIDPProviders(v.Db, idpProviderTable, event.InstanceID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedIDPProviderSequence(event) -} - -func (v *View) UpdateOrgOwnerRemovedIDPProviders(event *models.Event) error { - err := view.UpdateOrgOwnerRemovedIDPProviders(v.Db, idpProviderTable, event.InstanceID, event.AggregateID) - if err != nil && !errors.IsNotFound(err) { - return err - } - return v.ProcessedIDPProviderSequence(event) -} - -func (v *View) GetLatestIDPProviderSequence(instanceID string) (*global_view.CurrentSequence, error) { - return v.latestSequence(idpProviderTable, instanceID) -} - -func (v *View) GetLatestIDPProviderSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) { - return v.latestSequences(idpProviderTable, instanceIDs) -} - -func (v *View) ProcessedIDPProviderSequence(event *models.Event) error { - return v.saveCurrentSequence(idpProviderTable, event) -} - -func (v *View) UpdateIDPProviderSpoolerRunTimestamp(instanceIDs []string) error { - return v.updateSpoolerRunSequence(idpProviderTable, instanceIDs) -} - -func (v *View) GetLatestIDPProviderFailedEvent(sequence uint64, instanceID string) (*global_view.FailedEvent, error) { - return v.latestFailedEvent(idpProviderTable, instanceID, sequence) -} - -func (v *View) ProcessedIDPProviderFailedEvent(failedEvent *global_view.FailedEvent) error { - return v.saveFailedEvent(failedEvent) -} diff --git a/internal/auth/repository/org.go b/internal/auth/repository/org.go index 0781ea2740..13dd726fca 100644 --- a/internal/auth/repository/org.go +++ b/internal/auth/repository/org.go @@ -8,7 +8,6 @@ import ( ) type OrgRepository interface { - GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error) GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error) GetLoginText(ctx context.Context, orgID string) ([]*domain.CustomText, error) } diff --git a/internal/command/idp.go b/internal/command/idp.go index f6de9cdf80..811bc2d1d7 100644 --- a/internal/command/idp.go +++ b/internal/command/idp.go @@ -1,6 +1,12 @@ package command -import "github.com/zitadel/zitadel/internal/repository/idp" +import ( + "context" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command/preparation" + "github.com/zitadel/zitadel/internal/repository/idp" +) type GenericOAuthProvider struct { Name string @@ -52,3 +58,34 @@ type LDAPProvider struct { LDAPAttributes idp.LDAPAttributes IDPOptions idp.Options } + +func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) { + writeModel := NewOrgIDPRemoveWriteModel(orgID, id) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return false, err + } + + if len(events) > 0 { + writeModel.AppendEvents(events...) + if err := writeModel.Reduce(); err != nil { + return false, err + } + return writeModel.State.Exists(), nil + } + + instanceWriteModel := NewInstanceIDPRemoveWriteModel(authz.GetInstance(ctx).InstanceID(), id) + events, err = filter(ctx, instanceWriteModel.Query()) + if err != nil { + return false, err + } + + if len(events) == 0 { + return false, nil + } + instanceWriteModel.AppendEvents(events...) + if err := instanceWriteModel.Reduce(); err != nil { + return false, err + } + return instanceWriteModel.State.Exists(), nil +} diff --git a/internal/command/instance_idp.go b/internal/command/instance_idp.go index 37b581f55c..0139b996bc 100644 --- a/internal/command/instance_idp.go +++ b/internal/command/instance_idp.go @@ -236,6 +236,23 @@ func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*doma return pushedEventsToObjectDetails(pushedEvents), nil } +func ExistsInstanceIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (exists bool, err error) { + instanceWriteModel := NewInstanceIDPRemoveWriteModel(authz.GetInstance(ctx).InstanceID(), id) + events, err := filter(ctx, instanceWriteModel.Query()) + if err != nil { + return false, err + } + + if len(events) == 0 { + return false, nil + } + instanceWriteModel.AppendEvents(events...) + if err := instanceWriteModel.Reduce(); err != nil { + return false, err + } + return instanceWriteModel.State.Exists(), nil +} + func (c *Commands) prepareAddInstanceOAuthProvider(a *instance.Aggregate, writeModel *InstanceOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { diff --git a/internal/command/instance_policy_login.go b/internal/command/instance_policy_login.go index 395c107230..d2fa5cb2d4 100644 --- a/internal/command/instance_policy_login.go +++ b/internal/command/instance_policy_login.go @@ -41,8 +41,8 @@ func (c *Commands) AddIDPProviderToDefaultLoginPolicy(ctx context.Context, idpPr return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-GVDfe", "Errors.IAM.LoginPolicy.NotFound") } - _, err = c.getInstanceIDPConfigByID(ctx, idpProvider.IDPConfigID) - if err != nil { + exists, err := ExistsInstanceIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID) + if err != nil || !exists { return nil, caos_errs.ThrowPreconditionFailed(err, "INSTANCE-m8fsd", "Errors.IDPConfig.NotExisting") } idpModel := NewInstanceIdentityProviderWriteModel(ctx, idpProvider.IDPConfigID) diff --git a/internal/command/org_idp.go b/internal/command/org_idp.go index fc559fdd9d..734d0c5b71 100644 --- a/internal/command/org_idp.go +++ b/internal/command/org_idp.go @@ -225,6 +225,23 @@ func (c *Commands) DeleteOrgProvider(ctx context.Context, resourceOwner, id stri return pushedEventsToObjectDetails(pushedEvents), nil } +func ExistsOrgIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) { + writeModel := NewOrgIDPRemoveWriteModel(orgID, id) + events, err := filter(ctx, writeModel.Query()) + if err != nil { + return false, err + } + + if len(events) == 0 { + return false, nil + } + writeModel.AppendEvents(events...) + if err := writeModel.Reduce(); err != nil { + return false, err + } + return writeModel.State.Exists(), nil +} + func (c *Commands) prepareAddOrgOAuthProvider(a *org.Aggregate, writeModel *OrgOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation { return func() (preparation.CreateCommands, error) { if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" { diff --git a/internal/command/org_policy_login.go b/internal/command/org_policy_login.go index bee73e079c..b8e551b250 100644 --- a/internal/command/org_policy_login.go +++ b/internal/command/org_policy_login.go @@ -146,12 +146,13 @@ func (c *Commands) AddIDPToLoginPolicy(ctx context.Context, resourceOwner string return nil, caos_errs.ThrowNotFound(nil, "Org-Ffgw2", "Errors.Org.LoginPolicy.NotFound") } + var exists bool if idpProvider.Type == domain.IdentityProviderTypeOrg { - _, err = c.getOrgIDPConfigByID(ctx, idpProvider.IDPConfigID, resourceOwner) + exists, err = ExistsOrgIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID, resourceOwner) } else { - _, err = c.getInstanceIDPConfigByID(ctx, idpProvider.IDPConfigID) + exists, err = ExistsInstanceIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID) } - if err != nil { + if !exists || err != nil { return nil, caos_errs.ThrowPreconditionFailed(err, "Org-3N9fs", "Errors.IDPConfig.NotExisting") } idpModel := NewOrgIdentityProviderWriteModel(resourceOwner, idpProvider.IDPConfigID) diff --git a/internal/command/user_idp_link.go b/internal/command/user_idp_link.go index ea0a3381c8..9db06bf6ab 100644 --- a/internal/command/user_idp_link.go +++ b/internal/command/user_idp_link.go @@ -66,14 +66,12 @@ func (c *Commands) addUserIDPLink(ctx context.Context, human *eventstore.Aggrega return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6m9Kd", "Errors.User.ExternalIDP.Invalid") } - _, err := c.getOrgIDPConfigByID(ctx, link.IDPConfigID, human.ResourceOwner) - if caos_errs.IsNotFound(err) { - _, err = c.getInstanceIDPConfigByID(ctx, link.IDPConfigID) - } - if err != nil { + exists, err := ExistsIDP(ctx, c.eventstore.Filter, link.IDPConfigID, human.ResourceOwner) + if !exists || err != nil { return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-39nfs", "Errors.IDPConfig.NotExisting") } return user.NewUserIDPLinkAddedEvent(ctx, human, link.IDPConfigID, link.DisplayName, link.ExternalUserID), nil + } func (c *Commands) RemoveUserIDPLink(ctx context.Context, link *domain.UserIDPLink) (*domain.ObjectDetails, error) { diff --git a/internal/domain/idp.go b/internal/domain/idp.go index 9df87f6ba4..af34731c1e 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -34,3 +34,12 @@ const ( IDPTypeGitLabSelfHosted IDPTypeGoogle ) + +func (t IDPType) GetCSSClass() string { + switch t { //nolint:exhaustive + case IDPTypeGoogle: + return "google" + default: + return "" + } +} diff --git a/internal/domain/idp_config.go b/internal/domain/idp_config.go index 762d0da5d6..e8f8b29dc4 100644 --- a/internal/domain/idp_config.go +++ b/internal/domain/idp_config.go @@ -69,6 +69,8 @@ type JWTIDPConfig struct { HeaderName string } +// IDPConfigType +// Deprecated: use [IDPType] type IDPConfigType int32 const ( @@ -85,6 +87,8 @@ func (f IDPConfigType) Valid() bool { return f >= 0 && f < idpConfigTypeCount } +// IDPConfigState +// Deprecated: use [IDPStateType] type IDPConfigState int32 const ( @@ -104,6 +108,8 @@ func (s IDPConfigState) Exists() bool { return s != IDPConfigStateUnspecified && s != IDPConfigStateRemoved } +// IDPConfigStylingType +// Deprecated: use a concrete provider type IDPConfigStylingType int32 const ( diff --git a/internal/domain/policy_login.go b/internal/domain/policy_login.go index 823a51220c..a0c9d4bce7 100644 --- a/internal/domain/policy_login.go +++ b/internal/domain/policy_login.go @@ -55,10 +55,10 @@ type IDPProvider struct { Type IdentityProviderType IDPConfigID string - Name string - StylingType IDPConfigStylingType - IDPConfigType IDPConfigType - IDPState IDPConfigState + Name string + StylingType IDPConfigStylingType // deprecated + IDPType IDPType + IDPState IDPConfigState } func (p IDPProvider) IsValid() bool { diff --git a/internal/iam/model/idp_provider_view.go b/internal/iam/model/idp_provider_view.go index c6e1b4aeab..562e371724 100644 --- a/internal/iam/model/idp_provider_view.go +++ b/internal/iam/model/idp_provider_view.go @@ -68,65 +68,3 @@ func (r *IDPProviderSearchRequest) EnsureLimit(limit uint64) error { func (r *IDPProviderSearchRequest) AppendAggregateIDQuery(aggregateID string) { r.Queries = append(r.Queries, &IDPProviderSearchQuery{Key: IDPProviderSearchKeyAggregateID, Method: domain.SearchMethodEquals, Value: aggregateID}) } - -func IdpProviderViewsToDomain(idpProviders []*IDPProviderView) []*domain.IDPProvider { - providers := make([]*domain.IDPProvider, len(idpProviders)) - for i, provider := range idpProviders { - p := &domain.IDPProvider{ - IDPConfigID: provider.IDPConfigID, - Type: idpProviderTypeToDomain(provider.IDPProviderType), - Name: provider.Name, - IDPConfigType: idpConfigTypeToDomain(provider.IDPConfigType), - StylingType: idpStylingTypeToDomain(provider.StylingType), - IDPState: idpStateToDomain(provider.IDPState), - } - providers[i] = p - } - return providers -} - -func idpProviderTypeToDomain(idpType IDPProviderType) domain.IdentityProviderType { - switch idpType { - case IDPProviderTypeSystem: - return domain.IdentityProviderTypeSystem - case IDPProviderTypeOrg: - return domain.IdentityProviderTypeOrg - default: - return domain.IdentityProviderTypeSystem - } -} - -func idpConfigTypeToDomain(idpType IdpConfigType) domain.IDPConfigType { - switch idpType { - case IDPConfigTypeOIDC: - return domain.IDPConfigTypeOIDC - case IDPConfigTypeSAML: - return domain.IDPConfigTypeSAML - case IDPConfigTypeJWT: - return domain.IDPConfigTypeJWT - default: - return domain.IDPConfigTypeOIDC - } -} - -func idpStylingTypeToDomain(stylingType IDPStylingType) domain.IDPConfigStylingType { - switch stylingType { - case IDPStylingTypeGoogle: - return domain.IDPConfigStylingTypeGoogle - default: - return domain.IDPConfigStylingTypeUnspecified - } -} - -func idpStateToDomain(state IDPConfigState) domain.IDPConfigState { - switch state { - case IDPConfigStateActive: - return domain.IDPConfigStateActive - case IDPConfigStateInactive: - return domain.IDPConfigStateInactive - case IDPConfigStateRemoved: - return domain.IDPConfigStateRemoved - default: - return domain.IDPConfigStateActive - } -} diff --git a/internal/idp/providers/gitlab/gitlab.go b/internal/idp/providers/gitlab/gitlab.go index 36e7404ff6..f36aba8f23 100644 --- a/internal/idp/providers/gitlab/gitlab.go +++ b/internal/idp/providers/gitlab/gitlab.go @@ -18,14 +18,14 @@ type Provider struct { } // New creates a GitLab.com provider using the [oidc.Provider] (OIDC generic provider) -func New(clientID, clientSecret, redirectURI string, options ...oidc.ProviderOpts) (*Provider, error) { - return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, options...) +func New(clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) { + return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, scopes, options...) } // NewCustomIssuer creates a GitLab provider using the [oidc.Provider] (OIDC generic provider) // with a custom issuer for self-managed instances -func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, options ...oidc.ProviderOpts) (*Provider, error) { - rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, oidc.DefaultMapper, options...) +func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) { + rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, oidc.DefaultMapper, options...) if err != nil { return nil, err } diff --git a/internal/idp/providers/gitlab/gitlab_test.go b/internal/idp/providers/gitlab/gitlab_test.go index dd6808bb36..436a00b2ca 100644 --- a/internal/idp/providers/gitlab/gitlab_test.go +++ b/internal/idp/providers/gitlab/gitlab_test.go @@ -16,6 +16,7 @@ func TestProvider_BeginAuth(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string opts []oidc.ProviderOpts } tests := []struct { @@ -29,6 +30,7 @@ func TestProvider_BeginAuth(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, }, want: &oidc.Session{ AuthURL: "https://gitlab.com/oauth/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState", @@ -40,7 +42,7 @@ func TestProvider_BeginAuth(t *testing.T) { a := assert.New(t) r := require.New(t) - provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.opts...) + provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.opts...) r.NoError(err) session, err := provider.BeginAuth(context.Background(), "testState") diff --git a/internal/idp/providers/gitlab/session_test.go b/internal/idp/providers/gitlab/session_test.go index cdc5cd1a4a..8f9337abc7 100644 --- a/internal/idp/providers/gitlab/session_test.go +++ b/internal/idp/providers/gitlab/session_test.go @@ -22,6 +22,7 @@ func TestProvider_FetchUser(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string httpMock func() authURL string code string @@ -55,6 +56,7 @@ func TestProvider_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, httpMock: func() { gock.New("https://gitlab.com/oauth"). Get("/userinfo"). @@ -74,6 +76,7 @@ func TestProvider_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, httpMock: func() { gock.New("https://gitlab.com/oauth"). Get("/userinfo"). @@ -110,6 +113,7 @@ func TestProvider_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, httpMock: func() { gock.New("https://gitlab.com/oauth"). Get("/userinfo"). @@ -161,7 +165,7 @@ func TestProvider_FetchUser(t *testing.T) { // call the real discovery endpoint gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking() - provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...) + provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...) require.NoError(t, err) session := &oidc.Session{ diff --git a/internal/idp/providers/google/google.go b/internal/idp/providers/google/google.go index c60382632d..67a958692b 100644 --- a/internal/idp/providers/google/google.go +++ b/internal/idp/providers/google/google.go @@ -20,8 +20,8 @@ type Provider struct { } // New creates a Google provider using the [oidc.Provider] (OIDC generic provider) -func New(clientID, clientSecret, redirectURI string, opts ...oidc.ProviderOpts) (*Provider, error) { - rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, userMapper, opts...) +func New(clientID, clientSecret, redirectURI string, scopes []string, opts ...oidc.ProviderOpts) (*Provider, error) { + rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, userMapper, opts...) if err != nil { return nil, err } diff --git a/internal/idp/providers/google/google_test.go b/internal/idp/providers/google/google_test.go index c6f8603965..ff0a6d5d49 100644 --- a/internal/idp/providers/google/google_test.go +++ b/internal/idp/providers/google/google_test.go @@ -16,6 +16,7 @@ func TestProvider_BeginAuth(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string } tests := []struct { name string @@ -28,6 +29,7 @@ func TestProvider_BeginAuth(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, }, want: &oidc.Session{ AuthURL: "https://accounts.google.com/o/oauth2/v2/auth?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState", @@ -39,7 +41,7 @@ func TestProvider_BeginAuth(t *testing.T) { a := assert.New(t) r := require.New(t) - provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI) + provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes) r.NoError(err) session, err := provider.BeginAuth(context.Background(), "testState") diff --git a/internal/idp/providers/google/session_test.go b/internal/idp/providers/google/session_test.go index 941060f69b..f4696da715 100644 --- a/internal/idp/providers/google/session_test.go +++ b/internal/idp/providers/google/session_test.go @@ -22,6 +22,7 @@ func TestSession_FetchUser(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string httpMock func() authURL string code string @@ -55,6 +56,7 @@ func TestSession_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, httpMock: func() { gock.New("https://openidconnect.googleapis.com"). Get("/v1/userinfo"). @@ -74,6 +76,7 @@ func TestSession_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, httpMock: func() { gock.New("https://openidconnect.googleapis.com"). Get("/v1/userinfo"). @@ -110,6 +113,7 @@ func TestSession_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, httpMock: func() { gock.New("https://openidconnect.googleapis.com"). Get("/v1/userinfo"). @@ -162,7 +166,7 @@ func TestSession_FetchUser(t *testing.T) { // call the real discovery endpoint gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking() - provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI) + provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes) require.NoError(t, err) session := &oidc.Session{ diff --git a/internal/idp/providers/jwt/jwt.go b/internal/idp/providers/jwt/jwt.go index 104e70e6a2..bd2effac8c 100644 --- a/internal/idp/providers/jwt/jwt.go +++ b/internal/idp/providers/jwt/jwt.go @@ -18,7 +18,6 @@ const ( var _ idp.Provider = (*Provider)(nil) var ( - ErrNoTokens = errors.New("no tokens provided") ErrMissingUserAgentID = errors.New("userAgentID missing") ) diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 9aa29735a0..fb79302db1 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -2,7 +2,13 @@ package jwt import ( "context" + "errors" + "fmt" + "net/http" + "time" + "github.com/zitadel/logging" + "github.com/zitadel/oidc/v2/pkg/client/rp" "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/text/language" @@ -11,8 +17,14 @@ import ( var _ idp.Session = (*Session)(nil) +var ( + ErrNoTokens = errors.New("no tokens provided") + ErrInvalidToken = errors.New("invalid tokens provided") +) + // Session is the [idp.Session] implementation for the JWT provider type Session struct { + *Provider AuthURL string Tokens *oidc.Tokens } @@ -28,9 +40,48 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { if s.Tokens == nil { return nil, ErrNoTokens } + s.Tokens.IDTokenClaims, err = s.validateToken(ctx, s.Tokens.IDToken) + if err != nil { + return nil, err + } return &User{s.Tokens.IDTokenClaims}, nil } +func (s *Session) validateToken(ctx context.Context, token string) (oidc.IDTokenClaims, error) { + logging.Debug("begin token validation") + // TODO: be able to specify them in the template: https://github.com/zitadel/zitadel/issues/5322 + offset := 3 * time.Second + maxAge := time.Hour + claims := oidc.EmptyIDTokenClaims() + payload, err := oidc.ParseToken(token, claims) + if err != nil { + return nil, fmt.Errorf("%w: malformed jwt payload: %v", ErrInvalidToken, err) + } + + if err = oidc.CheckIssuer(claims, s.Provider.issuer); err != nil { + return nil, fmt.Errorf("%w: invalid issuer: %v", ErrInvalidToken, err) + } + + logging.Debug("begin signature validation") + keySet := rp.NewRemoteKeySet(http.DefaultClient, s.Provider.keysEndpoint) + if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil { + return nil, fmt.Errorf("%w: invalid signature: %v", ErrInvalidToken, err) + } + + if !claims.GetExpiration().IsZero() { + if err = oidc.CheckExpiration(claims, offset); err != nil { + return nil, fmt.Errorf("%w: expired: %v", ErrInvalidToken, err) + } + } + + if !claims.GetIssuedAt().IsZero() { + if err = oidc.CheckIssuedAt(claims, maxAge, offset); err != nil { + return nil, fmt.Errorf("%w: %v", ErrInvalidToken, err) + } + } + return claims, nil +} + type User struct { oidc.IDTokenClaims } diff --git a/internal/idp/providers/jwt/session_test.go b/internal/idp/providers/jwt/session_test.go index d8a92a4219..61f5526543 100644 --- a/internal/idp/providers/jwt/session_test.go +++ b/internal/idp/providers/jwt/session_test.go @@ -2,25 +2,37 @@ package jwt import ( "context" + "encoding/json" "errors" "testing" + "time" + "github.com/golang/mock/gomock" + "github.com/h2non/gock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/oauth2" "golang.org/x/text/language" + "gopkg.in/square/go-jose.v2" - "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/crypto" ) func TestSession_FetchUser(t *testing.T) { type fields struct { - authURL string - tokens *oidc.Tokens + name string + issuer string + jwtEndpoint string + keysEndpoint string + headerName string + encryptionAlg func(t *testing.T) crypto.EncryptionAlgorithm + httpMock func(issuer string) + authURL string + tokens *oidc.Tokens } type want struct { err func(error) bool - user idp.User id string firstName string lastName string @@ -41,8 +53,22 @@ func TestSession_FetchUser(t *testing.T) { want want }{ { - name: "no tokens", - fields: fields{}, + name: "no tokens", + fields: fields{ + issuer: "https://jwt.com", + jwtEndpoint: "https://auth.com/jwt", + keysEndpoint: "https://jwt.com/keys", + headerName: "jwt-header", + encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm { + return crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + }, + httpMock: func(issuer string) { + gock.New(issuer). + Get("/keys"). + Reply(200). + JSON(keys(t)) + }, + }, want: want{ err: func(err error) bool { return errors.Is(err, ErrNoTokens) @@ -50,11 +76,53 @@ func TestSession_FetchUser(t *testing.T) { }, }, { - name: "successful fetch", + name: "invalid token", fields: fields{ + issuer: "https://jwt.com", + jwtEndpoint: "https://auth.com/jwt", + keysEndpoint: "https://jwt.com/keys", + headerName: "jwt-header", + encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm { + return crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + }, + httpMock: func(issuer string) { + gock.New(issuer). + Get("/keys"). + Reply(200). + JSON(keys(t)) + }, authURL: "https://auth.com/jwt?authRequestID=testState", tokens: &oidc.Tokens{ - Token: &oauth2.Token{}, + Token: &oauth2.Token{}, + IDToken: "invalidToken", + }, + }, + want: want{ + err: func(err error) bool { + return errors.Is(err, ErrInvalidToken) + }, + }, + }, + { + name: "successful fetch", + fields: fields{ + issuer: "https://jwt.com", + jwtEndpoint: "https://auth.com/jwt", + keysEndpoint: "https://jwt.com/keys", + headerName: "jwt-header", + encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm { + return crypto.CreateMockEncryptionAlg(gomock.NewController(t)) + }, + httpMock: func(issuer string) { + gock.New(issuer). + Get("/keys"). + Reply(200). + JSON(keys(t)) + }, + authURL: "https://auth.com/jwt?authRequestID=testState", + tokens: &oidc.Tokens{ + Token: &oauth2.Token{}, + IDToken: idToken(t, "https://jwt.com"), IDTokenClaims: func() oidc.IDTokenClaims { claims := oidc.EmptyIDTokenClaims() userinfo := oidc.NewUserInfo() @@ -75,25 +143,6 @@ func TestSession_FetchUser(t *testing.T) { }, }, want: want{ - user: &User{ - IDTokenClaims: func() oidc.IDTokenClaims { - claims := oidc.EmptyIDTokenClaims() - userinfo := oidc.NewUserInfo() - userinfo.SetSubject("sub") - userinfo.SetPicture("picture") - userinfo.SetName("firstname lastname") - userinfo.SetEmail("email", true) - userinfo.SetGivenName("firstname") - userinfo.SetFamilyName("lastname") - userinfo.SetNickname("nickname") - userinfo.SetPreferredUsername("username") - userinfo.SetProfile("profile") - userinfo.SetPhone("phone", true) - userinfo.SetLocale(language.English) - claims.SetUserinfo(userinfo) - return claims - }(), - }, id: "sub", firstName: "firstname", lastName: "lastname", @@ -112,11 +161,24 @@ func TestSession_FetchUser(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + defer gock.Off() + tt.fields.httpMock(tt.fields.issuer) a := assert.New(t) + provider, err := New( + tt.fields.name, + tt.fields.issuer, + tt.fields.jwtEndpoint, + tt.fields.keysEndpoint, + tt.fields.headerName, + tt.fields.encryptionAlg(t), + ) + require.NoError(t, err) + session := &Session{ - AuthURL: tt.fields.authURL, - Tokens: tt.fields.tokens, + Provider: provider, + AuthURL: tt.fields.authURL, + Tokens: tt.fields.tokens, } user, err := session.FetchUser(context.Background()) @@ -125,7 +187,6 @@ func TestSession_FetchUser(t *testing.T) { } if tt.want.err == nil { a.NoError(err) - a.Equal(tt.want.user, user) a.Equal(tt.want.id, user.GetID()) a.Equal(tt.want.firstName, user.GetFirstName()) a.Equal(tt.want.lastName, user.GetLastName()) @@ -143,3 +204,96 @@ func TestSession_FetchUser(t *testing.T) { }) } } + +func idToken(t *testing.T, issuer string) string { + claims := oidc.NewIDTokenClaims( + issuer, + "sub", + []string{"clientID"}, + time.Now().Add(1*time.Hour), + time.Now().Add(-1*time.Minute), + "", + "", + nil, + "clientID", + 0, + ) + info := oidc.NewUserInfo() + info.SetSubject("sub") + info.SetGivenName("firstname") + info.SetFamilyName("lastname") + info.SetName("firstname lastname") + info.SetNickname("nickname") + info.SetPreferredUsername("username") + info.SetEmail("email", true) + info.SetPhone("phone", true) + info.SetLocale(language.English) + info.SetPicture("picture") + info.SetProfile("profile") + claims.SetUserinfo(info) + privateKey, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAs38btwb3c7r0tMaQpGvBmY+mPwMU/LpfuPoC0k2t4RsKp0fv +40SMl50CRrHgk395wch8PMPYbl3+8TtYAJuyrFALIj3Ff1UcKIk0hOH5DDsfh7/q +2wFuncTmS6bifYo8CfSq2vDGnM7nZnEvxY/MfSydZdcmIqlkUpfQmtzExw9+tSe5 +Dxq6gn5JtlGgLgZGt69r5iMMrTEGhhVAXzNuMZbmlCoBru+rC8ITlTX/0V1ZcsSb +L8tYWhthyu9x6yjo1bH85wiVI4gs0MhU8f2a+kjL/KGZbR14Ua2eo6tonBZLC5DH +WM2TkYXgRCDPufjcgmzN0Lm91E4P8KvBcvly6QIDAQABAoIBAQCPj1nbSPcg2KZe +73FAD+8HopyUSSK//1AP4eXfzcEECVy77g0u9+R6XlkzsZCsZ4g6NN8ounqfyw3c +YlpAIkcFCf/dowoSjT+4LASVQyatYZwWNqjgAIU4KgMG/rKnNahPTiBYe7peMB1j +EaPjnt8uPkCk8y7NCi3y4Pk24tt/WM5KbJK2NQhUi1csGnleDfE+0blV0l/e6C68 +W5cbnbWAroMqae/Yon3XVZiXX0m+l2f6ZzIgKaD18J+eEM8FjJC+jQKiRe1i9v3K +nQrLwh/gn8J10FcbKn3xqslKVidzASIrNIzHT9j/Z5T9NXuAKa7IV2x+Dtdus+wq +iBsUunwBAoGBANpYew+8i9vDwK4/SefduDTuzJ0H9lWTjtbiWQ+KYZoeJ7q3/qns +jsmi+mjxkXxXg1RrGbNbjtbl3RXXIrUeeBB0lglRJUjc3VK7VvNoyXIWsiqhCspH +IJ9Yuknv4mXB01m/glbSCS/xu4RTgf5aOG4jUiRb9+dCIpvDxI9gbXEVAoGBANJz +hIJkplIJ+biTi3G1Oz17qkUkInNXzAEzKD9Atoz5AIAiR1ivOMLOlbucfjevw/Nw +TnpkMs9xqCefKupTlsriXtZI88m7ZKzAmolYsPolOy/Jhi31h9JFVTEfKGqVS+dk +A4ndhgdW9RUeNJPY2YVCARXQrWpueweQDA1cNaeFAoGAPJsYtXqBW6PPRM5+ZiSt +78tk8iV2o7RMjqrPS7f+dXfvUS2nO2VVEPTzCtQarOfhpToBLT65vD6bimdn09w8 +OV0TFEz4y2u65y7m6LNqTwertpdy1ki97l0DgGhccCBH2P6GYDD2qd8wTH+dcot6 +ZF/begopGoDJ+HBzi9SZLC0CgYBZzPslHMevyBvr++GLwrallKhiWnns1/DwLiEl +ZHrBCtuA0Z+6IwLIdZiE9tEQ+ApYTXrfVPQteqUzSwLn/IUiy5eGPpjwYushoAoR +Q2w5QTvRN1/vKo8rVXR1woLfgBdkhFPSN1mitiNcQIhU8jpXV4PZCDOHb99FqdzK +sqcedQKBgQCOmgbqxGsnT2WQhoOdzln+NOo6Tx+FveLLqat2KzpY59W4noeI2Awn +HfIQgWUAW9dsjVVOXMP1jhq8U9hmH/PFWA11V/iCdk1NTxZEw87VAOeWuajpdDHG ++iex349j8h2BcQ4Zd0FWu07gGFnS/yuDJPn6jBhRusdieEcxLRjTKg== +-----END RSA PRIVATE KEY----- +`)) + if err != nil { + t.Fatal(err) + } + signer, err := jose.NewSigner(jose.SigningKey{Key: privateKey, Algorithm: "RS256"}, &jose.SignerOptions{}) + if err != nil { + t.Fatal(err) + } + data, err := json.Marshal(claims) + if err != nil { + t.Fatal(err) + } + jws, err := signer.Sign(data) + if err != nil { + t.Fatal(err) + } + idToken, err := jws.CompactSerialize() + if err != nil { + t.Fatal(err) + } + return idToken +} + +func keys(t *testing.T) *jose.JSONWebKeySet { + privateKey, err := crypto.BytesToPublicKey([]byte(`-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs38btwb3c7r0tMaQpGvB +mY+mPwMU/LpfuPoC0k2t4RsKp0fv40SMl50CRrHgk395wch8PMPYbl3+8TtYAJuy +rFALIj3Ff1UcKIk0hOH5DDsfh7/q2wFuncTmS6bifYo8CfSq2vDGnM7nZnEvxY/M +fSydZdcmIqlkUpfQmtzExw9+tSe5Dxq6gn5JtlGgLgZGt69r5iMMrTEGhhVAXzNu +MZbmlCoBru+rC8ITlTX/0V1ZcsSbL8tYWhthyu9x6yjo1bH85wiVI4gs0MhU8f2a ++kjL/KGZbR14Ua2eo6tonBZLC5DHWM2TkYXgRCDPufjcgmzN0Lm91E4P8KvBcvly +6QIDAQAB +-----END PUBLIC KEY----- +`)) + if err != nil { + t.Fatal(err) + } + return &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: privateKey, Algorithm: "RS256", Use: oidc.KeyUseSignature}}} +} diff --git a/internal/idp/providers/oidc/oidc.go b/internal/idp/providers/oidc/oidc.go index 06fedbab4c..890b168cb5 100644 --- a/internal/idp/providers/oidc/oidc.go +++ b/internal/idp/providers/oidc/oidc.go @@ -68,7 +68,7 @@ var DefaultMapper UserInfoMapper = func(info oidc.UserInfo) idp.User { } // New creates a generic OIDC provider -func New(name, issuer, clientID, clientSecret, redirectURI string, userInfoMapper UserInfoMapper, options ...ProviderOpts) (provider *Provider, err error) { +func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, userInfoMapper UserInfoMapper, options ...ProviderOpts) (provider *Provider, err error) { provider = &Provider{ name: name, userInfoMapper: userInfoMapper, @@ -76,13 +76,27 @@ func New(name, issuer, clientID, clientSecret, redirectURI string, userInfoMappe for _, option := range options { option(provider) } - provider.RelyingParty, err = rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, []string{oidc.ScopeOpenID}, provider.options...) + provider.RelyingParty, err = rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, setDefaultScope(scopes), provider.options...) if err != nil { return nil, err } return provider, nil } +// setDefaultScope ensures that at least openid ist set +// if none is provided it will request `openid profile email phone` +func setDefaultScope(scopes []string) []string { + if len(scopes) == 0 { + return []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone} + } + for _, scope := range scopes { + if scope == oidc.ScopeOpenID { + return scopes + } + } + return append(scopes, oidc.ScopeOpenID) +} + // Name implements the [idp.Provider] interface func (p *Provider) Name() string { return p.name diff --git a/internal/idp/providers/oidc/oidc_test.go b/internal/idp/providers/oidc/oidc_test.go index 571ab4ae5a..ca205d7e5b 100644 --- a/internal/idp/providers/oidc/oidc_test.go +++ b/internal/idp/providers/oidc/oidc_test.go @@ -20,6 +20,7 @@ func TestProvider_BeginAuth(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string userMapper func(info oidc.UserInfo) idp.User httpMock func(issuer string) } @@ -36,6 +37,7 @@ func TestProvider_BeginAuth(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, userMapper: DefaultMapper, httpMock: func(issuer string) { gock.New(issuer). @@ -59,7 +61,7 @@ func TestProvider_BeginAuth(t *testing.T) { a := assert.New(t) r := require.New(t) - provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.userMapper) + provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper) r.NoError(err) session, err := provider.BeginAuth(context.Background(), "testState") @@ -77,6 +79,7 @@ func TestProvider_Options(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string userMapper func(info oidc.UserInfo) idp.User opts []ProviderOpts httpMock func(issuer string) @@ -102,6 +105,7 @@ func TestProvider_Options(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, userMapper: DefaultMapper, opts: nil, httpMock: func(issuer string) { @@ -133,6 +137,7 @@ func TestProvider_Options(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, userMapper: DefaultMapper, opts: []ProviderOpts{ WithLinkingAllowed(), @@ -169,7 +174,7 @@ func TestProvider_Options(t *testing.T) { tt.fields.httpMock(tt.fields.issuer) a := assert.New(t) - provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.userMapper, tt.fields.opts...) + provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...) require.NoError(t, err) a.Equal(tt.want.name, provider.Name()) diff --git a/internal/idp/providers/oidc/session_test.go b/internal/idp/providers/oidc/session_test.go index e31dd13835..fe18a71b74 100644 --- a/internal/idp/providers/oidc/session_test.go +++ b/internal/idp/providers/oidc/session_test.go @@ -27,6 +27,7 @@ func TestSession_FetchUser(t *testing.T) { clientID string clientSecret string redirectURI string + scopes []string userMapper func(oidc.UserInfo) idp.User httpMock func(issuer string) authURL string @@ -62,6 +63,7 @@ func TestSession_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, userMapper: DefaultMapper, httpMock: func(issuer string) { gock.New(issuer). @@ -93,6 +95,7 @@ func TestSession_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, userMapper: DefaultMapper, httpMock: func(issuer string) { gock.New(issuer). @@ -141,6 +144,7 @@ func TestSession_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, userMapper: DefaultMapper, httpMock: func(issuer string) { gock.New(issuer). @@ -201,6 +205,7 @@ func TestSession_FetchUser(t *testing.T) { clientID: "clientID", clientSecret: "clientSecret", redirectURI: "redirectURI", + scopes: []string{"openid"}, userMapper: DefaultMapper, httpMock: func(issuer string) { gock.New(issuer). @@ -254,7 +259,7 @@ func TestSession_FetchUser(t *testing.T) { tt.fields.httpMock(tt.fields.issuer) a := assert.New(t) - provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.userMapper) + provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper) require.NoError(t, err) session := &Session{ diff --git a/internal/query/idp_login_policy_link.go b/internal/query/idp_login_policy_link.go index d0077e7d4b..afa5a8f6e8 100644 --- a/internal/query/idp_login_policy_link.go +++ b/internal/query/idp_login_policy_link.go @@ -15,9 +15,10 @@ import ( ) type IDPLoginPolicyLink struct { - IDPID string - IDPName string - IDPType domain.IDPConfigType + IDPID string + IDPName string + IDPType domain.IDPType + OwnerType domain.IdentityProviderType } type IDPLoginPolicyLinks struct { @@ -113,25 +114,28 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string, func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) { return sq.Select( IDPLoginPolicyLinkIDPIDCol.identifier(), - IDPNameCol.identifier(), - IDPTypeCol.identifier(), + IDPTemplateNameCol.identifier(), + IDPTemplateTypeCol.identifier(), + IDPTemplateOwnerTypeCol.identifier(), countColumn.identifier()). From(idpLoginPolicyLinkTable.identifier()). - LeftJoin(join(IDPIDCol, IDPLoginPolicyLinkIDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPLoginPolicyLinks, error) { links := make([]*IDPLoginPolicyLink, 0) var count uint64 for rows.Next() { var ( - idpName = sql.NullString{} - idpType = sql.NullInt16{} - link = new(IDPLoginPolicyLink) + idpName = sql.NullString{} + idpType = sql.NullInt16{} + idpOwnerType = sql.NullInt16{} + link = new(IDPLoginPolicyLink) ) err := rows.Scan( &link.IDPID, &idpName, &idpType, + &idpOwnerType, &count, ) if err != nil { @@ -140,10 +144,11 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (s link.IDPName = idpName.String //IDPType 0 is oidc so we have to set unspecified manually if idpType.Valid { - link.IDPType = domain.IDPConfigType(idpType.Int16) + link.IDPType = domain.IDPType(idpType.Int16) } else { - link.IDPType = domain.IDPConfigTypeUnspecified + link.IDPType = domain.IDPTypeUnspecified } + link.OwnerType = domain.IdentityProviderType(idpOwnerType.Int16) links = append(links, link) } diff --git a/internal/query/idp_login_policy_link_test.go b/internal/query/idp_login_policy_link_test.go index 44d6e8d6c3..f886b9f68a 100644 --- a/internal/query/idp_login_policy_link_test.go +++ b/internal/query/idp_login_policy_link_test.go @@ -13,16 +13,18 @@ import ( var ( loginPolicyIDPLinksQuery = regexp.QuoteMeta(`SELECT projections.idp_login_policy_links4.idp_id,` + - ` projections.idps3.name,` + - ` projections.idps3.type,` + + ` projections.idp_templates2.name,` + + ` projections.idp_templates2.type,` + + ` projections.idp_templates2.owner_type,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_login_policy_links4` + - ` LEFT JOIN projections.idps3 ON projections.idp_login_policy_links4.idp_id = projections.idps3.id AND projections.idp_login_policy_links4.instance_id = projections.idps3.instance_id` + + ` LEFT JOIN projections.idp_templates2 ON projections.idp_login_policy_links4.idp_id = projections.idp_templates2.id AND projections.idp_login_policy_links4.instance_id = projections.idp_templates2.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) loginPolicyIDPLinksCols = []string{ "idp_id", "name", "type", + "owner_type", "count", } ) @@ -49,7 +51,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { { "idp-id", "idp-name", - domain.IDPConfigTypeJWT, + domain.IDPTypeJWT, + domain.IdentityProviderTypeSystem, }, }, ), @@ -60,9 +63,10 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { }, Links: []*IDPLoginPolicyLink{ { - IDPID: "idp-id", - IDPName: "idp-name", - IDPType: domain.IDPConfigTypeJWT, + IDPID: "idp-id", + IDPName: "idp-name", + IDPType: domain.IDPTypeJWT, + OwnerType: domain.IdentityProviderTypeSystem, }, }, }, @@ -79,6 +83,7 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { "idp-id", nil, nil, + nil, }, }, ), @@ -91,7 +96,7 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) { { IDPID: "idp-id", IDPName: "", - IDPType: domain.IDPConfigTypeUnspecified, + IDPType: domain.IDPTypeUnspecified, }, }, }, diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go index ef781eb0a0..735efaf174 100644 --- a/internal/query/idp_template.go +++ b/internal/query/idp_template.go @@ -386,8 +386,8 @@ var ( } ) -// IDPTemplateByIDAndResourceOwner searches for the requested id in the context of the resource owner and IAM -func (q *Queries) IDPTemplateByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk bool, id, resourceOwner string, withOwnerRemoved bool) (_ *IDPTemplate, err error) { +// IDPTemplateByID searches for the requested id +func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (_ *IDPTemplate, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -403,20 +403,16 @@ func (q *Queries) IDPTemplateByIDAndResourceOwner(ctx context.Context, shouldTri if !withOwnerRemoved { eq[IDPTemplateOwnerRemovedCol.identifier()] = false } - where := sq.And{ - eq, - sq.Or{ - sq.Eq{IDPTemplateResourceOwnerCol.identifier(): resourceOwner}, - sq.Eq{IDPTemplateResourceOwnerCol.identifier(): authz.GetInstance(ctx).InstanceID()}, - }, + query, scan := prepareIDPTemplateByIDQuery(ctx, q.client) + for _, q := range queries { + query = q.toQuery(query) } - stmt, scan := prepareIDPTemplateByIDQuery(ctx, q.client) - query, args, err := stmt.Where(where).ToSql() + stmt, args, err := query.Where(eq).ToSql() if err != nil { - return nil, errors.ThrowInternal(err, "QUERY-SFAew", "Errors.Query.SQLStatement") + return nil, errors.ThrowInternal(err, "QUERY-SFefg", "Errors.Query.SQLStatement") } - row := q.client.QueryRowContext(ctx, query, args...) + row := q.client.QueryRowContext(ctx, stmt, args...) return scan(row) } diff --git a/internal/query/idp_user_link.go b/internal/query/idp_user_link.go index 465a6a3968..b2b95e85ed 100644 --- a/internal/query/idp_user_link.go +++ b/internal/query/idp_user_link.go @@ -21,7 +21,7 @@ type IDPUserLink struct { ProvidedUserID string ProvidedUsername string ResourceOwner string - IDPType domain.IDPConfigType + IDPType domain.IDPType } type IDPUserLinks struct { @@ -127,18 +127,22 @@ func NewIDPUserLinksResourceOwnerSearchQuery(value string) (SearchQuery, error) return NewTextQuery(IDPUserLinkResourceOwnerCol, value, TextEquals) } +func NewIDPUserLinksExternalIDSearchQuery(value string) (SearchQuery, error) { + return NewTextQuery(IDPUserLinkExternalUserIDCol, value, TextEquals) +} + func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPUserLinks, error)) { return sq.Select( IDPUserLinkIDPIDCol.identifier(), IDPUserLinkUserIDCol.identifier(), - IDPNameCol.identifier(), + IDPTemplateNameCol.identifier(), IDPUserLinkExternalUserIDCol.identifier(), IDPUserLinkDisplayNameCol.identifier(), - IDPTypeCol.identifier(), + IDPTemplateTypeCol.identifier(), IDPUserLinkResourceOwnerCol.identifier(), countColumn.identifier()). From(idpUserLinkTable.identifier()). - LeftJoin(join(IDPIDCol, IDPUserLinkIDPIDCol) + db.Timetravel(call.Took(ctx))). + LeftJoin(join(IDPTemplateIDCol, IDPUserLinkIDPIDCol) + db.Timetravel(call.Took(ctx))). PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*IDPUserLinks, error) { idps := make([]*IDPUserLink, 0) @@ -165,9 +169,9 @@ func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.Selec idp.IDPName = idpName.String //IDPType 0 is oidc so we have to set unspecified manually if idpType.Valid { - idp.IDPType = domain.IDPConfigType(idpType.Int16) + idp.IDPType = domain.IDPType(idpType.Int16) } else { - idp.IDPType = domain.IDPConfigTypeUnspecified + idp.IDPType = domain.IDPTypeUnspecified } idps = append(idps, idp) } diff --git a/internal/query/idp_user_link_test.go b/internal/query/idp_user_link_test.go index 859c164cb8..0343af9261 100644 --- a/internal/query/idp_user_link_test.go +++ b/internal/query/idp_user_link_test.go @@ -14,14 +14,14 @@ import ( var ( idpUserLinksQuery = regexp.QuoteMeta(`SELECT projections.idp_user_links3.idp_id,` + ` projections.idp_user_links3.user_id,` + - ` projections.idps3.name,` + + ` projections.idp_templates2.name,` + ` projections.idp_user_links3.external_user_id,` + ` projections.idp_user_links3.display_name,` + - ` projections.idps3.type,` + + ` projections.idp_templates2.type,` + ` projections.idp_user_links3.resource_owner,` + ` COUNT(*) OVER ()` + ` FROM projections.idp_user_links3` + - ` LEFT JOIN projections.idps3 ON projections.idp_user_links3.idp_id = projections.idps3.id AND projections.idp_user_links3.instance_id = projections.idps3.instance_id` + + ` LEFT JOIN projections.idp_templates2 ON projections.idp_user_links3.idp_id = projections.idp_templates2.id AND projections.idp_user_links3.instance_id = projections.idp_templates2.instance_id` + ` AS OF SYSTEM TIME '-1 ms'`) idpUserLinksCols = []string{ "idp_id", @@ -60,7 +60,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) { "idp-name", "external-user-id", "display-name", - domain.IDPConfigTypeJWT, + domain.IDPTypeJWT, "ro", }, }, @@ -77,7 +77,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) { IDPName: "idp-name", ProvidedUserID: "external-user-id", ProvidedUsername: "display-name", - IDPType: domain.IDPConfigTypeJWT, + IDPType: domain.IDPTypeJWT, ResourceOwner: "ro", }, }, @@ -114,7 +114,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) { IDPName: "", ProvidedUserID: "external-user-id", ProvidedUsername: "display-name", - IDPType: domain.IDPConfigTypeUnspecified, + IDPType: domain.IDPTypeUnspecified, ResourceOwner: "ro", }, }, diff --git a/internal/query/projection/idp_template.go b/internal/query/projection/idp_template.go index c1d5416463..09c3c3ad45 100644 --- a/internal/query/projection/idp_template.go +++ b/internal/query/projection/idp_template.go @@ -668,7 +668,7 @@ func (p *idpTemplateProjection) reduceOldConfigAdded(event eventstore.Event) (*h handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive), handler.NewCol(IDPTemplateNameCol, idpEvent.Name), handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType), - handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeOIDC), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeUnspecified), handler.NewCol(IDPTemplateIsCreationAllowedCol, true), handler.NewCol(IDPTemplateIsLinkingAllowedCol, true), handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.AutoRegister), @@ -727,6 +727,7 @@ func (p *idpTemplateProjection) reduceOldOIDCConfigAdded(event eventstore.Event) []handler.Column{ handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeOIDC), }, []handler.Condition{ handler.NewCond(IDPTemplateIDCol, idpEvent.IDPConfigID), @@ -820,6 +821,7 @@ func (p *idpTemplateProjection) reduceOldJWTConfigAdded(event eventstore.Event) []handler.Column{ handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()), handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()), + handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeJWT), }, []handler.Condition{ handler.NewCond(IDPTemplateIDCol, idpEvent.IDPConfigID), diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index eb0e9493f6..21bf720c84 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -4403,7 +4403,7 @@ message AddJWTProviderRequest { string jwt_endpoint = 3 [(validate.rules).string = {min_len: 1, max_len: 200}]; string keys_endpoint = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; string header_name = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; - zitadel.idp.v1.Options provider_options = 6 [(validate.rules).message = {required: true}]; + zitadel.idp.v1.Options provider_options = 6; } message AddJWTProviderResponse { @@ -4418,7 +4418,7 @@ message UpdateJWTProviderRequest { string jwt_endpoint = 4 [(validate.rules).string = {min_len: 1, max_len: 200}]; string keys_endpoint = 5 [(validate.rules).string = {max_len: 200}]; string header_name = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; - zitadel.idp.v1.Options provider_options = 7 [(validate.rules).message = {required: true}]; + zitadel.idp.v1.Options provider_options = 7; } message UpdateJWTProviderResponse {