From 52f68f8db8f89ef45d1f8331506cf1fb15116684 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 16 Aug 2023 13:29:57 +0200 Subject: [PATCH] feat: add ldap external idp to login api (#5938) * fix: handling of ldap login through separate endpoint * fix: handling of ldap login through separate endpoint * fix: handling of ldap login through separate endpoint * fix: successful intent for ldap * fix: successful intent for ldap * fix: successful intent for ldap * fix: add changes from code review * fix: remove set intent credentials and handle ldap errors * fix: remove set intent credentials and handle ldap errors * refactor into separate methods and fix merge * remove mocks --------- Co-authored-by: Livio Spring --- .../session/v2/session_integration_test.go | 4 +- internal/api/grpc/user/v2/user.go | 182 +++++++++++++++--- .../api/grpc/user/v2/user_integration_test.go | 60 +++++- internal/api/grpc/user/v2/user_test.go | 74 ++++++- internal/api/idp/idp.go | 17 +- internal/command/idp_intent.go | 67 ++++++- internal/command/idp_intent_model.go | 33 +++- internal/command/idp_intent_test.go | 106 +++++++++- internal/idp/providers/ldap/ldap.go | 8 + internal/idp/providers/ldap/session.go | 2 + internal/idp/providers/ldap/session_test.go | 52 ++--- internal/idp/providers/ldap/user.go | 52 ++--- internal/integration/client.go | 36 +++- internal/repository/idpintent/eventstore.go | 1 + internal/repository/idpintent/intent.go | 71 ++++++- internal/static/i18n/bg.yaml | 1 + internal/static/i18n/de.yaml | 3 +- internal/static/i18n/en.yaml | 1 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.yaml | 1 + internal/static/i18n/mk.yaml | 1 + internal/static/i18n/pl.yaml | 1 + internal/static/i18n/zh.yaml | 1 + proto/zitadel/user/v2alpha/idp.proto | 70 +++++++ proto/zitadel/user/v2alpha/user_service.proto | 28 +-- 27 files changed, 726 insertions(+), 149 deletions(-) diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index 14e99c6926..a8368f5c60 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -258,7 +258,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) - intentID, token, _, _ := Tester.CreateSuccessfulIntent(t, idpID, User.GetUserId(), "id") + intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id") updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), SessionToken: createResp.GetSessionToken(), @@ -289,7 +289,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil) idpUserID := "id" - intentID, token, _, _ := Tester.CreateSuccessfulIntent(t, idpID, "", idpUserID) + intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", idpUserID) updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), SessionToken: createResp.GetSessionToken(), diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index af2da629f4..f4ec0eaa9d 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -2,6 +2,7 @@ package user import ( "context" + errs "errors" "io" "golang.org/x/text/language" @@ -14,6 +15,9 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/idp" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" + "github.com/zitadel/zitadel/internal/query" object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" ) @@ -126,11 +130,22 @@ func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ } func (s *Server) StartIdentityProviderFlow(ctx context.Context, req *user.StartIdentityProviderFlowRequest) (_ *user.StartIdentityProviderFlowResponse, err error) { - id, details, err := s.command.CreateIntent(ctx, req.GetIdpId(), req.GetSuccessUrl(), req.GetFailureUrl(), authz.GetCtxData(ctx).OrgID) + switch t := req.GetContent().(type) { + case *user.StartIdentityProviderFlowRequest_Urls: + return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls) + case *user.StartIdentityProviderFlowRequest_Ldap: + return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap) + default: + return nil, errors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderFlow not implemented", t) + } +} + +func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderFlowResponse, error) { + intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } - authURL, err := s.command.AuthURLFromProvider(ctx, req.GetIdpId(), id, s.idpCallback(ctx)) + authURL, err := s.command.AuthURLFromProvider(ctx, idpID, intentWriteModel.AggregateID, s.idpCallback(ctx)) if err != nil { return nil, err } @@ -140,6 +155,79 @@ func (s *Server) StartIdentityProviderFlow(ctx context.Context, req *user.StartI }, nil } +func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderFlowResponse, error) { + intentWriteModel, details, err := s.command.CreateIntent(ctx, idpID, "", "", authz.GetCtxData(ctx).OrgID) + if err != nil { + return nil, err + } + externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + if err != nil { + if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { + return nil, err + } + return nil, err + } + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes) + if err != nil { + return nil, err + } + return &user.StartIdentityProviderFlowResponse{ + Details: object.DomainToDetailsPb(details), + NextStep: &user.StartIdentityProviderFlowResponse_Intent{Intent: &user.Intent{IntentId: intentWriteModel.AggregateID, Token: token}}, + }, nil +} + +func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) { + idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID) + if err != nil { + return "", err + } + externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID) + if err != nil { + return "", err + } + queries := []query.SearchQuery{ + idQuery, externalIDQuery, + } + links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false) + if err != nil { + return "", err + } + if len(links.Links) == 1 { + return links.Links[0].UserID, nil + } + return "", nil +} + +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { + provider, err := s.command.GetProvider(ctx, idpID, "") + if err != nil { + return nil, "", nil, err + } + ldapProvider, ok := provider.(*ldap.Provider) + if !ok { + return nil, "", nil, errors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented") + } + session := ldapProvider.GetSession(username, password) + externalUser, err := session.FetchUser(ctx) + if errs.Is(err, ldap.ErrFailedLogin) || errs.Is(err, ldap.ErrNoSingleUser) { + return nil, "", nil, errors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed") + } + if err != nil { + return nil, "", nil, err + } + userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID()) + if err != nil { + return nil, "", nil, err + } + + attributes := make(map[string][]string, 0) + for _, item := range session.Entry.Attributes { + attributes[item.Name] = item.Values + } + return externalUser, userID, attributes, nil +} + func (s *Server) RetrieveIdentityProviderInformation(ctx context.Context, req *user.RetrieveIdentityProviderInformationRequest) (_ *user.RetrieveIdentityProviderInformationResponse, err error) { intent, err := s.command.GetIntentWriteModel(ctx, req.GetIntentId(), authz.GetCtxData(ctx).OrgID) if err != nil { @@ -155,41 +243,83 @@ func (s *Server) RetrieveIdentityProviderInformation(ctx context.Context, req *u } func intentToIDPInformationPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderInformationResponse, err error) { - var idToken *string - if intent.IDPIDToken != "" { - idToken = &intent.IDPIDToken - } - var accessToken string - if intent.IDPAccessToken != nil { - accessToken, err = crypto.DecryptString(intent.IDPAccessToken, alg) - if err != nil { - return nil, err - } - } rawInformation := new(structpb.Struct) err = rawInformation.UnmarshalJSON(intent.IDPUser) if err != nil { return nil, err } - - return &user.RetrieveIdentityProviderInformationResponse{ - Details: &object_pb.Details{ - Sequence: intent.ProcessedSequence, - ChangeDate: timestamppb.New(intent.ChangeDate), - ResourceOwner: intent.ResourceOwner, - }, + information := &user.RetrieveIdentityProviderInformationResponse{ + Details: intentToDetailsPb(intent), IdpInformation: &user.IDPInformation{ - Access: &user.IDPInformation_Oauth{ - Oauth: &user.IDPOAuthAccessInformation{ - AccessToken: accessToken, - IdToken: idToken, - }, - }, IdpId: intent.IDPID, UserId: intent.IDPUserID, UserName: intent.IDPUserName, RawInformation: rawInformation, }, + } + if intent.IDPIDToken != "" || intent.IDPAccessToken != nil { + information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg) + if err != nil { + return nil, err + } + } + + if intent.IDPEntryAttributes != nil { + access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes) + if err != nil { + return nil, err + } + information.IdpInformation.Access = access + } + + return information, nil +} + +func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) { + var idToken *string + if idpIDToken != "" { + idToken = &idpIDToken + } + var accessToken string + if idpAccessToken != nil { + accessToken, err = crypto.DecryptString(idpAccessToken, alg) + if err != nil { + return nil, err + } + } + return &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: accessToken, + IdToken: idToken, + }, + }, nil +} + +func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details { + return &object_pb.Details{ + Sequence: intent.ProcessedSequence, + ChangeDate: timestamppb.New(intent.ChangeDate), + ResourceOwner: intent.ResourceOwner, + } +} + +func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) { + values := make(map[string]interface{}, 0) + for k, v := range entryAttributes { + intValues := make([]interface{}, len(v)) + for i, value := range v { + intValues[i] = value + } + values[k] = intValues + } + attributes, err := structpb.NewStruct(values) + if err != nil { + return nil, err + } + return &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: attributes, + }, }, nil } diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go index ae8145848d..18d97f98ab 100644 --- a/internal/api/grpc/user/v2/user_integration_test.go +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -650,9 +650,13 @@ func TestServer_StartIdentityProviderFlow(t *testing.T) { args: args{ CTX, &user.StartIdentityProviderFlowRequest{ - IdpId: idpID, - SuccessUrl: "https://example.com/success", - FailureUrl: "https://example.com/failure", + IdpId: idpID, + Content: &user.StartIdentityProviderFlowRequest_Urls{ + Urls: &user.RedirectURLs{ + SuccessUrl: "https://example.com/success", + FailureUrl: "https://example.com/failure", + }, + }, }, }, want: &user.StartIdentityProviderFlowResponse{ @@ -689,7 +693,8 @@ func TestServer_StartIdentityProviderFlow(t *testing.T) { func TestServer_RetrieveIdentityProviderInformation(t *testing.T) { idpID := Tester.AddGenericOAuthProvider(t) intentID := Tester.CreateIntent(t, idpID) - successfulID, token, changeDate, sequence := Tester.CreateSuccessfulIntent(t, idpID, "", "id") + successfulID, token, changeDate, sequence := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", "id") + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence := Tester.CreateSuccessfulLDAPIntent(t, idpID, "", "id") type args struct { ctx context.Context req *user.RetrieveIdentityProviderInformationRequest @@ -759,6 +764,51 @@ func TestServer_RetrieveIdentityProviderInformation(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful ldap intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderInformationRequest{ + IntentId: ldapSuccessfulID, + Token: ldapToken, + }, + }, + want: &user.RetrieveIdentityProviderInformationResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(ldapChangeDate), + ResourceOwner: Tester.Organisation.ID, + Sequence: ldapSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"id"}, + "username": []interface{}{"username"}, + "language": []interface{}{"en"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: idpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "preferredUsername": "username", + "preferredLanguage": "en", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -769,7 +819,7 @@ func TestServer_RetrieveIdentityProviderInformation(t *testing.T) { require.NoError(t, err) } - grpc.AllFieldsEqual(t, got.ProtoReflect(), tt.want.ProtoReflect(), grpc.CustomMappers) + grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) }) } } diff --git a/internal/api/grpc/user/v2/user_test.go b/internal/api/grpc/user/v2/user_test.go index e540ae7f16..a56e641079 100644 --- a/internal/api/grpc/user/v2/user_test.go +++ b/internal/api/grpc/user/v2/user_test.go @@ -73,9 +73,10 @@ func Test_intentToIDPInformationPb(t *testing.T) { KeyID: "id", Crypted: []byte("accessToken"), }, - IDPIDToken: "idToken", - UserID: "userID", - State: domain.IDPIntentStateSucceeded, + IDPIDToken: "idToken", + IDPEntryAttributes: map[string][]string{}, + UserID: "userID", + State: domain.IDPIntentStateSucceeded, }, alg: decryption(caos_errs.ThrowInternal(nil, "id", "invalid key id")), }, @@ -85,7 +86,7 @@ func Test_intentToIDPInformationPb(t *testing.T) { }, }, { - "successful", + "successful oauth", args{ intent: &command.IDPIntentWriteModel{ WriteModel: eventstore.WriteModel{ @@ -140,16 +141,73 @@ func Test_intentToIDPInformationPb(t *testing.T) { }, err: nil, }, + }, { + "successful ldap", + args{ + intent: &command.IDPIntentWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: "intentID", + ProcessedSequence: 123, + ResourceOwner: "ro", + InstanceID: "instanceID", + ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local), + }, + IDPID: "idpID", + IDPUser: []byte(`{"userID": "idpUserID", "username": "username"}`), + IDPUserID: "idpUserID", + IDPUserName: "username", + IDPEntryAttributes: map[string][]string{ + "id": {"idpUserID"}, + "firstName": {"firstname1", "firstname2"}, + "lastName": {"lastname"}, + }, + UserID: "userID", + State: domain.IDPIntentStateSucceeded, + }, + }, + res{ + resp: &user.RetrieveIdentityProviderInformationResponse{ + Details: &object_pb.Details{ + Sequence: 123, + ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)), + ResourceOwner: "ro", + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Ldap{ + Ldap: &user.IDPLDAPAccessInformation{ + Attributes: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": []interface{}{"idpUserID"}, + "firstName": []interface{}{"firstname1", "firstname2"}, + "lastName": []interface{}{"lastname"}, + }) + require.NoError(t, err) + return s + }(), + }, + }, + IdpId: "idpID", + UserId: "idpUserID", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "userID": "idpUserID", + "username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + err: nil, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := intentToIDPInformationPb(tt.args.intent, tt.args.alg) require.ErrorIs(t, err, tt.res.err) - grpc.AllFieldsEqual(t, got.ProtoReflect(), tt.res.resp.ProtoReflect(), grpc.CustomMappers) - if tt.res.resp != nil { - grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...) - } + grpc.AllFieldsEqual(t, tt.res.resp.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) }) } } diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index f9a3cd1f3a..99e709d145 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -28,8 +28,9 @@ import ( ) const ( - HandlerPrefix = "/idps" - callbackPath = "/callback" + HandlerPrefix = "/idps" + callbackPath = "/callback" + ldapCallbackPath = callbackPath + "/ldap" paramIntentID = "id" paramToken = "token" @@ -82,18 +83,22 @@ func NewHandler( } func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() data, err := h.parseCallbackRequest(r) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - intent := h.getActiveIntent(w, r, data.State) - if intent == nil { - // if we didn't get an active intent the error was already handled (either redirected or display directly) + intent, err := h.commands.GetActiveIntent(ctx, data.State) + if err != nil { + if z_errs.IsNotFound(err) { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + redirectToFailureURLErr(w, r, intent, err) return } - ctx := r.Context() // the provider might have returned an error if data.Error != "" { cmdErr := h.commands.FailIDPIntent(ctx, intent, reason(data.Error, data.ErrorDescription)) diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 05f68c2754..9cbc1beb9b 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -50,32 +50,32 @@ func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID st } } -func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureURL, resourceOwner string) (string, *domain.ObjectDetails, error) { +func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureURL, resourceOwner string) (*IDPIntentWriteModel, *domain.ObjectDetails, error) { id, err := c.idGenerator.Next() if err != nil { - return "", nil, err + return nil, nil, err } writeModel := NewIDPIntentWriteModel(id, resourceOwner) if err != nil { - return "", nil, err + return nil, nil, err } cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL)) if err != nil { - return "", nil, err + return nil, nil, err } pushedEvents, err := c.eventstore.Push(ctx, cmds...) if err != nil { - return "", nil, err + return nil, nil, err } err = AppendAndReduce(writeModel, pushedEvents...) if err != nil { - return "", nil, err + return nil, nil, err } - return id, writeModelToObjectDetails(&writeModel.WriteModel), nil + return writeModel, writeModelToObjectDetails(&writeModel.WriteModel), nil } -func (c *Commands) GetProvider(ctx context.Context, idpID, callbackURL string) (idp.Provider, error) { +func (c *Commands) GetProvider(ctx context.Context, idpID string, callbackURL string) (idp.Provider, error) { writeModel, err := IDPProviderWriteModel(ctx, c.eventstore.Filter, idpID) if err != nil { return nil, err @@ -83,7 +83,21 @@ func (c *Commands) GetProvider(ctx context.Context, idpID, callbackURL string) ( return writeModel.ToProvider(callbackURL, c.idpConfigEncryption) } -func (c *Commands) AuthURLFromProvider(ctx context.Context, idpID, state, callbackURL string) (string, error) { +func (c *Commands) GetActiveIntent(ctx context.Context, intentID string) (*IDPIntentWriteModel, error) { + intent, err := c.GetIntentWriteModel(ctx, intentID, "") + if err != nil { + return nil, err + } + if intent.State == domain.IDPIntentStateUnspecified { + return nil, errors.ThrowNotFound(nil, "IDP-Hk38e", "Errors.Intent.NotStarted") + } + if intent.State != domain.IDPIntentStateStarted { + return nil, errors.ThrowInvalidArgument(nil, "IDP-Sfrgs", "Errors.Intent.NotStarted") + } + return intent, nil +} + +func (c *Commands) AuthURLFromProvider(ctx context.Context, idpID, state string, callbackURL string) (string, error) { provider, err := c.GetProvider(ctx, idpID, callbackURL) if err != nil { return "", err @@ -108,7 +122,7 @@ func getIDPIntentWriteModel(ctx context.Context, writeModel *IDPIntentWriteModel } func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, idpSession idp.Session, userID string) (string, error) { - token, err := c.idpConfigEncryption.Encrypt([]byte(writeModel.AggregateID)) + token, err := c.generateIntentToken(writeModel.AggregateID) if err != nil { return "", err } @@ -134,9 +148,42 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr if err != nil { return "", err } + return token, nil +} + +func (c *Commands) generateIntentToken(intentID string) (string, error) { + token, err := c.idpConfigEncryption.Encrypt([]byte(intentID)) + if err != nil { + return "", err + } return base64.RawURLEncoding.EncodeToString(token), nil } +func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, attributes map[string][]string) (string, error) { + token, err := c.generateIntentToken(writeModel.AggregateID) + if err != nil { + return "", err + } + idpInfo, err := json.Marshal(idpUser) + if err != nil { + return "", err + } + cmd := idpintent.NewLDAPSucceededEvent( + ctx, + &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, + idpInfo, + idpUser.GetID(), + idpUser.GetPreferredUsername(), + userID, + attributes, + ) + err = c.pushAppendAndReduce(ctx, writeModel, cmd) + if err != nil { + return "", err + } + return token, nil +} + func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, reason string) error { cmd := idpintent.NewFailedEvent( ctx, diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go index 182b06b3d8..bf70c78a8c 100644 --- a/internal/command/idp_intent_model.go +++ b/internal/command/idp_intent_model.go @@ -12,15 +12,18 @@ import ( type IDPIntentWriteModel struct { eventstore.WriteModel - SuccessURL *url.URL - FailureURL *url.URL - IDPID string - IDPUser []byte - IDPUserID string - IDPUserName string + SuccessURL *url.URL + FailureURL *url.URL + IDPID string + IDPUser []byte + IDPUserID string + IDPUserName string + UserID string + IDPAccessToken *crypto.CryptoValue IDPIDToken string - UserID string + + IDPEntryAttributes map[string][]string State domain.IDPIntentState aggregate *eventstore.Aggregate @@ -42,7 +45,9 @@ func (wm *IDPIntentWriteModel) Reduce() error { case *idpintent.StartedEvent: wm.reduceStartedEvent(e) case *idpintent.SucceededEvent: - wm.reduceSucceededEvent(e) + wm.reduceOAuthSucceededEvent(e) + case *idpintent.LDAPSucceededEvent: + wm.reduceLDAPSucceededEvent(e) case *idpintent.FailedEvent: wm.reduceFailedEvent(e) } @@ -59,6 +64,7 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder { EventTypes( idpintent.StartedEventType, idpintent.SucceededEventType, + idpintent.LDAPSucceededEventType, idpintent.FailedEventType, ). Builder() @@ -71,7 +77,16 @@ func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) { wm.State = domain.IDPIntentStateStarted } -func (wm *IDPIntentWriteModel) reduceSucceededEvent(e *idpintent.SucceededEvent) { +func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) { + wm.UserID = e.UserID + wm.IDPUser = e.IDPUser + wm.IDPUserID = e.IDPUserID + wm.IDPUserName = e.IDPUserName + wm.IDPEntryAttributes = e.EntryAttributes + wm.State = domain.IDPIntentStateSucceeded +} + +func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededEvent) { wm.UserID = e.UserID wm.IDPUser = e.IDPUser wm.IDPUserID = e.IDPUserID diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 0d95bd287a..5591c7d648 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/oauth2" + "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" @@ -199,9 +200,13 @@ func TestCommands_CreateIntent(t *testing.T) { eventstore: tt.fields.eventstore, idGenerator: tt.fields.idGenerator, } - intentID, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.resourceOwner) + intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.resourceOwner) require.ErrorIs(t, err, tt.res.err) - assert.Equal(t, tt.res.intentID, intentID) + if intentWriteModel != nil { + assert.Equal(t, tt.res.intentID, intentWriteModel.AggregateID) + } else { + assert.Equal(t, tt.res.intentID, "") + } assert.Equal(t, tt.res.details, details) }) } @@ -580,6 +585,103 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { } } +func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + idpConfigEncryption crypto.EncryptionAlgorithm + } + type args struct { + ctx context.Context + writeModel *IDPIntentWriteModel + idpUser idp.User + userID string + attributes map[string][]string + } + type res struct { + token string + err error + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + "encryption fails", + fields{ + idpConfigEncryption: func() crypto.EncryptionAlgorithm { + m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t)) + m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed")) + return m + }(), + }, + args{ + ctx: context.Background(), + writeModel: NewIDPIntentWriteModel("id", "ro"), + }, + res{ + err: z_errors.ThrowInternal(nil, "id", "encryption failed"), + }, + }, + { + "push", + fields{ + idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + eventstore: eventstoreExpect(t, + expectPush( + eventPusherToEvents( + idpintent.NewLDAPSucceededEvent( + context.Background(), + &idpintent.NewAggregate("id", "ro").Aggregate, + []byte(`{"id":"id","preferredUsername":"username","preferredLanguage":"und"}`), + "id", + "username", + "", + map[string][]string{"id": {"id"}}, + ), + ), + ), + ), + }, + args{ + ctx: context.Background(), + writeModel: NewIDPIntentWriteModel("id", "ro"), + attributes: map[string][]string{"id": {"id"}}, + idpUser: ldap.NewUser( + "id", + "", + "", + "", + "", + "username", + "", + false, + "", + false, + language.Tag{}, + "", + "", + ), + }, + res{ + token: "aWQ", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idpConfigEncryption: tt.fields.idpConfigEncryption, + } + got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.attributes) + require.ErrorIs(t, err, tt.res.err) + assert.Equal(t, tt.res.token, got) + }) + } +} + func TestCommands_FailIDPIntent(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore diff --git a/internal/idp/providers/ldap/ldap.go b/internal/idp/providers/ldap/ldap.go index d2c950b29b..80df9b967b 100644 --- a/internal/idp/providers/ldap/ldap.go +++ b/internal/idp/providers/ldap/ldap.go @@ -218,6 +218,14 @@ func (p *Provider) BeginAuth(ctx context.Context, state string, params ...any) ( }, nil } +func (p *Provider) GetSession(username, password string) *Session { + return &Session{ + Provider: p, + User: username, + Password: password, + } +} + func (p *Provider) IsLinkingAllowed() bool { return p.isLinkingAllowed } diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index 46bc06573e..e6422b5d26 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -26,6 +26,7 @@ type Session struct { loginUrl string User string Password string + Entry *ldap.Entry } func (s *Session) GetAuthURL() string { @@ -57,6 +58,7 @@ func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { if err != nil { return nil, err } + s.Entry = user return mapLDAPEntryToUser( user, diff --git a/internal/idp/providers/ldap/session_test.go b/internal/idp/providers/ldap/session_test.go index f6bcee6544..69ba3a3256 100644 --- a/internal/idp/providers/ldap/session_test.go +++ b/internal/idp/providers/ldap/session_test.go @@ -219,19 +219,19 @@ func TestProvider_mapLDAPEntryToUser(t *testing.T) { }, want: want{ user: &User{ - id: "", - firstName: "", - lastName: "", - displayName: "", - nickName: "", - preferredUsername: "", - email: "", - emailVerified: false, - phone: "", - phoneVerified: false, - preferredLanguage: language.Tag{}, - avatarURL: "", - profile: "", + ID: "", + FirstName: "", + LastName: "", + DisplayName: "", + NickName: "", + PreferredUsername: "", + Email: "", + EmailVerified: false, + Phone: "", + PhoneVerified: false, + PreferredLanguage: language.Tag{}, + AvatarURL: "", + Profile: "", }, }, }, @@ -351,19 +351,19 @@ func TestProvider_mapLDAPEntryToUser(t *testing.T) { }, want: want{ user: &User{ - id: "id", - firstName: "first", - lastName: "last", - displayName: "display", - nickName: "nick", - preferredUsername: "preferred", - email: "email", - emailVerified: false, - phone: "phone", - phoneVerified: false, - preferredLanguage: language.Make("und"), - avatarURL: "avatar", - profile: "profile", + ID: "id", + FirstName: "first", + LastName: "last", + DisplayName: "display", + NickName: "nick", + PreferredUsername: "preferred", + Email: "email", + EmailVerified: false, + Phone: "phone", + PhoneVerified: false, + PreferredLanguage: language.Make("und"), + AvatarURL: "avatar", + Profile: "profile", }, }, }, diff --git a/internal/idp/providers/ldap/user.go b/internal/idp/providers/ldap/user.go index 6bd208d1a0..5f9a797033 100644 --- a/internal/idp/providers/ldap/user.go +++ b/internal/idp/providers/ldap/user.go @@ -7,19 +7,19 @@ import ( ) type User struct { - id string - firstName string - lastName string - displayName string - nickName string - preferredUsername string - email domain.EmailAddress - emailVerified bool - phone domain.PhoneNumber - phoneVerified bool - preferredLanguage language.Tag - avatarURL string - profile string + ID string `json:"id,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + DisplayName string `json:"displayName,omitempty"` + NickName string `json:"nickName,omitempty"` + PreferredUsername string `json:"preferredUsername,omitempty"` + Email domain.EmailAddress `json:"email,omitempty"` + EmailVerified bool `json:"emailVerified,omitempty"` + Phone domain.PhoneNumber `json:"phone,omitempty"` + PhoneVerified bool `json:"phoneVerified,omitempty"` + PreferredLanguage language.Tag `json:"preferredLanguage,omitempty"` + AvatarURL string `json:"avatarURL,omitempty"` + Profile string `json:"profile,omitempty"` } func NewUser( @@ -55,41 +55,41 @@ func NewUser( } func (u *User) GetID() string { - return u.id + return u.ID } func (u *User) GetFirstName() string { - return u.firstName + return u.FirstName } func (u *User) GetLastName() string { - return u.lastName + return u.LastName } func (u *User) GetDisplayName() string { - return u.displayName + return u.DisplayName } func (u *User) GetNickname() string { - return u.nickName + return u.NickName } func (u *User) GetPreferredUsername() string { - return u.preferredUsername + return u.PreferredUsername } func (u *User) GetEmail() domain.EmailAddress { - return u.email + return u.Email } func (u *User) IsEmailVerified() bool { - return u.emailVerified + return u.EmailVerified } func (u *User) GetPhone() domain.PhoneNumber { - return u.phone + return u.Phone } func (u *User) IsPhoneVerified() bool { - return u.phoneVerified + return u.PhoneVerified } func (u *User) GetPreferredLanguage() language.Tag { - return u.preferredLanguage + return u.PreferredLanguage } func (u *User) GetAvatarURL() string { - return u.avatarURL + return u.AvatarURL } func (u *User) GetProfile() string { - return u.profile + return u.Profile } diff --git a/internal/integration/client.go b/internal/integration/client.go index bd3557bdb9..da7e0fb781 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -10,10 +10,12 @@ import ( "github.com/zitadel/logging" "github.com/zitadel/oidc/v2/pkg/oidc" "golang.org/x/oauth2" + "golang.org/x/text/language" "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -196,12 +198,12 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T) string { func (s *Tester) CreateIntent(t *testing.T, idpID string) string { ctx := authz.WithInstance(context.Background(), s.Instance) - id, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Organisation.ID) + writeModel, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Organisation.ID) require.NoError(t, err) - return id + return writeModel.AggregateID } -func (s *Tester) CreateSuccessfulIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { +func (s *Tester) CreateSuccessfulOAuthIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { ctx := authz.WithInstance(context.Background(), s.Instance) intentID := s.CreateIntent(t, idpID) writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Organisation.ID) @@ -227,6 +229,34 @@ func (s *Tester) CreateSuccessfulIntent(t *testing.T, idpID, userID, idpUserID s return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence } +func (s *Tester) CreateSuccessfulLDAPIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) { + ctx := authz.WithInstance(context.Background(), s.Instance) + intentID := s.CreateIntent(t, idpID) + writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Organisation.ID) + require.NoError(t, err) + username := "username" + lang := language.Make("en") + idpUser := ldap.NewUser( + idpUserID, + "", + "", + "", + "", + username, + "", + false, + "", + false, + lang, + "", + "", + ) + attributes := map[string][]string{"id": {idpUserID}, "username": {username}, "language": {lang.String()}} + token, err := s.Commands.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, userID, attributes) + require.NoError(t, err) + return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence +} + func (s *Tester) CreateVerfiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) { createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ Checks: &session.Checks{ diff --git a/internal/repository/idpintent/eventstore.go b/internal/repository/idpintent/eventstore.go index 9e9b4fa155..12129673a7 100644 --- a/internal/repository/idpintent/eventstore.go +++ b/internal/repository/idpintent/eventstore.go @@ -7,5 +7,6 @@ import ( func RegisterEventMappers(es *eventstore.Eventstore) { es.RegisterFilterEventMapper(AggregateType, StartedEventType, StartedEventMapper). RegisterFilterEventMapper(AggregateType, SucceededEventType, SucceededEventMapper). + RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper). RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper) } diff --git a/internal/repository/idpintent/intent.go b/internal/repository/idpintent/intent.go index fe26c9dfaa..a4ff650b40 100644 --- a/internal/repository/idpintent/intent.go +++ b/internal/repository/idpintent/intent.go @@ -12,9 +12,10 @@ import ( ) const ( - StartedEventType = instanceEventTypePrefix + "started" - SucceededEventType = instanceEventTypePrefix + "succeeded" - FailedEventType = instanceEventTypePrefix + "failed" + StartedEventType = instanceEventTypePrefix + "started" + SucceededEventType = instanceEventTypePrefix + "succeeded" + LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded" + FailedEventType = instanceEventTypePrefix + "failed" ) type StartedEvent struct { @@ -68,10 +69,11 @@ func StartedEventMapper(event *repository.Event) (eventstore.Event, error) { type SucceededEvent struct { eventstore.BaseEvent `json:"-"` - IDPUser []byte `json:"idpUser"` - IDPUserID string `json:"idpUserId,omitempty"` - IDPUserName string `json:"idpUserName,omitempty"` - UserID string `json:"userId,omitempty"` + IDPUser []byte `json:"idpUser"` + IDPUserID string `json:"idpUserId,omitempty"` + IDPUserName string `json:"idpUserName,omitempty"` + UserID string `json:"userId,omitempty"` + IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"` IDPIDToken string `json:"idpIdToken,omitempty"` } @@ -122,6 +124,61 @@ func SucceededEventMapper(event *repository.Event) (eventstore.Event, error) { return e, nil } +type LDAPSucceededEvent struct { + eventstore.BaseEvent `json:"-"` + + IDPUser []byte `json:"idpUser"` + IDPUserID string `json:"idpUserId,omitempty"` + IDPUserName string `json:"idpUserName,omitempty"` + UserID string `json:"userId,omitempty"` + + EntryAttributes map[string][]string `json:"user,omitempty"` +} + +func NewLDAPSucceededEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + idpUser []byte, + idpUserID, + idpUserName, + userID string, + attributes map[string][]string, +) *LDAPSucceededEvent { + return &LDAPSucceededEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + LDAPSucceededEventType, + ), + IDPUser: idpUser, + IDPUserID: idpUserID, + IDPUserName: idpUserName, + UserID: userID, + EntryAttributes: attributes, + } +} + +func (e *LDAPSucceededEvent) Data() interface{} { + return e +} + +func (e *LDAPSucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func LDAPSucceededEventMapper(event *repository.Event) (eventstore.Event, error) { + e := &LDAPSucceededEvent{ + BaseEvent: *eventstore.BaseEventFromRepo(event), + } + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "IDP-HBreq", "unable to unmarshal event") + } + + return e, nil +} + type FailedEvent struct { eventstore.BaseEvent `json:"-"` diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 2e06279675..8296ce2f59 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -139,6 +139,7 @@ Errors: MinimumExternalIDPNeeded: Трябва да се добави поне един IDP AlreadyExists: Външен IDP вече е зает NotFound: Външен IDP не е намерен + LoginFailed: Влизането във Външен IDP е неуспешно MFA: OTP: AlreadyReady: Многофакторният OTP (OneTimePassword) вече е настроен diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 04fd3a3eac..fdc52331fa 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -136,7 +136,8 @@ Errors: NotAllowed: Externer IDP ist auf dieser Organisation nicht erlaubt. MinimumExternalIDPNeeded: Mindestens ein IDP muss hinzugefügt werden. AlreadyExists: External IDP ist bereits vergeben - NotFound: Externe IDP nicht gefunden + NotFound: Externer IDP nicht gefunden + LoginFailed: Externer IDP Login fehlgeschlagen MFA: OTP: AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 43ef6e043b..c5c0354512 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -137,6 +137,7 @@ Errors: MinimumExternalIDPNeeded: At least one IDP must be added AlreadyExists: External IDP already taken NotFound: External IDP not found + LoginFailed: Login at External IDP failed MFA: OTP: AlreadyReady: Multifactor OTP (OneTimePassword) is already set up diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 825ea9ec62..175498bc7c 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -137,6 +137,7 @@ Errors: MinimumExternalIDPNeeded: Al menos de añadirse un IDP AlreadyExists: IDP externo ya cogido NotFound: IDP no encontrado + LoginFailed: Error de inicio de sesión en IDP externo MFA: OTP: AlreadyReady: Multifactor OTP (OneTimePassword) ya está configurado diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 3933e4d64c..e0ee94e5cc 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -137,6 +137,7 @@ Errors: MinimumExternalIDPNeeded: Au moins un IDP doit être ajouté AlreadyExists: External IDP déjà pris NotFound: IDP externe non trouvé + LoginFailed: Échec de la connexion à l'IDP externe MFA: OTP: AlreadyReady: L'OTP (mot de passe à usage unique) multifactoriel est déjà configuré. diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 7f73063c8e..0f6b4416d7 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -137,6 +137,7 @@ Errors: MinimumExternalIDPNeeded: Almeno un IDP deve essere aggiunto AlreadyExists: IDP esterno già preso NotFound: IDP esterno non trovato + LoginFailed: Accesso all'IDP esterno non riuscito MFA: OTP: AlreadyReady: Multifattore OTP (OneTimePassword) è già impostato diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index d0e2279ece..23136fddb7 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -129,6 +129,7 @@ Errors: MinimumExternalIDPNeeded: 少なくとも1つのIDPを追加する必要があります AlreadyExists: 外部IDPはすでに使用されています NotFound: 外部IDPが見つかりません + LoginFailed: 外部IDPでのログインに失敗 MFA: OTP: AlreadyReady: 多要素OTP(ワンタイムパスワード)は設定済みです diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index c4075da9c8..9b8e8485cc 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -137,6 +137,7 @@ Errors: MinimumExternalIDPNeeded: Мора да се додаде најмалку еден надворешен IDP AlreadyExists: Надворешниот IDP е веќе зафатен NotFound: Надворешниот IDP не е пронајден + LoginFailed: Пријавувањето на Надворешниот ВРЛ не успеа MFA: OTP: AlreadyReady: Мултифактор OTP (Еднократна Лозинка) e веќе поставен diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 07a00e3149..fab78662b1 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -137,6 +137,7 @@ Errors: MinimumExternalIDPNeeded: Przynajmniej jeden IDP musi być dodany AlreadyExists: IDP zewnętrzne już istnieje NotFound: IDP zewnętrzne nie znaleziony + LoginFailed: Logowanie w zewnętrznym IDP nie powiodło się MFA: OTP: AlreadyReady: Wieloskładnikowe OTP (OneTimePassword) jest już skonfigurowane diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 3418a7dcc4..b3d835935f 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -137,6 +137,7 @@ Errors: MinimumExternalIDPNeeded: 必须添加至少一个 IDP AlreadyExists: 外部 IDP 已存在 NotFound: 未找到外部 IDP + LoginFailed: 外部 IDP 登录失败 MFA: OTP: AlreadyReady: OTP (一次性密码) 已经设置好了 diff --git a/proto/zitadel/user/v2alpha/idp.proto b/proto/zitadel/user/v2alpha/idp.proto index 5996df57aa..07cce881c2 100644 --- a/proto/zitadel/user/v2alpha/idp.proto +++ b/proto/zitadel/user/v2alpha/idp.proto @@ -9,6 +9,67 @@ import "google/protobuf/struct.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; +message LDAPCredentials { + string username = 1[ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Username used to login through LDAP" + min_length: 1; + max_length: 200; + example: "\"username\""; + } + ]; + string password = 2[ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Password used to login through LDAP" + min_length: 1; + max_length: 200; + example: "\"Password1!\""; + } + ]; +} + +message RedirectURLs { + string success_url = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "URL on which the user will be redirected after a successful login" + min_length: 1; + max_length: 200; + example: "\"https://custom.com/login/idp/success\""; + } + ]; + string failure_url = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "URL on which the user will be redirected after a failed login" + min_length: 1; + max_length: 200; + example: "\"https://custom.com/login/idp/fail\""; + } + ]; +} + +message Intent { + string intent_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "ID of the intent" + min_length: 1; + max_length: 200; + example: "\"163840776835432705=\""; + } + ]; + string token = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "token of the intent" + min_length: 1; + max_length: 200; + example: "\"SJKL3ioIDpo342ioqw98fjp3sdf32wahb=\""; + } + ]; +} + message IDPInformation{ oneof access{ IDPOAuthAccessInformation oauth = 1 [ @@ -16,6 +77,11 @@ message IDPInformation{ description: "OAuth/OIDC access (and id_token) returned by the identity provider" } ]; + IDPLDAPAccessInformation ldap = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "LDAP entity attributes returned by the identity provider" + } + ]; } string idp_id = 2 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { @@ -47,6 +113,10 @@ message IDPOAuthAccessInformation{ optional string id_token = 2; } +message IDPLDAPAccessInformation{ + google.protobuf.Struct attributes = 1; +} + message IDPLink { string idp_id = 1 [ (validate.rules).string = {min_len: 1, max_len: 200}, diff --git a/proto/zitadel/user/v2alpha/user_service.proto b/proto/zitadel/user/v2alpha/user_service.proto index 796688747c..19dea55de4 100644 --- a/proto/zitadel/user/v2alpha/user_service.proto +++ b/proto/zitadel/user/v2alpha/user_service.proto @@ -1082,24 +1082,11 @@ message StartIdentityProviderFlowRequest{ example: "\"163840776835432705\""; } ]; - string success_url = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "URL on which the user will be redirected after a successful login" - min_length: 1; - max_length: 200; - example: "\"https://custom.com/login/idp/success\""; - } - ]; - string failure_url = 3 [ - (validate.rules).string = {min_len: 1, max_len: 200, uri_ref: true}, - (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "URL on which the user will be redirected after a failed login" - min_length: 1; - max_length: 200; - example: "\"https://custom.com/login/idp/fail\""; - } - ]; + + oneof content { + RedirectURLs urls = 2; + LDAPCredentials ldap = 3; + } } message StartIdentityProviderFlowResponse{ @@ -1111,6 +1098,11 @@ message StartIdentityProviderFlowResponse{ example: "\"https://accounts.google.com/o/oauth2/v2/auth?client_id=clientID&callback=https%3A%2F%2Fzitadel.cloud%2Fidps%2Fcallback\""; } ]; + Intent intent = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Intent information" + } + ]; } }