diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index a602dda948..676fced20a 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -768,6 +768,9 @@ SystemDefaults: DefaultQueryLimit: 100 # ZITADEL_SYSTEMDEFAULTS_DEFAULTQUERYLIMIT # MaxQueryLimit limits the number of items that can be queried in a single v3 API search request with explicitly passing a limit. MaxQueryLimit: 1000 # ZITADEL_SYSTEMDEFAULTS_MAXQUERYLIMIT + # The maximum duration of the IDP intent lifetime after which the IDP intent expires and can not be retrieved or used anymore. + # Note that this time is measured only after the IdP intent was successful and not after the IDP intent was created. + MaxIdPIntentLifetime: 1h # ZITADEL_SYSTEMDEFAULTS_MAXIDPINTENTLIFETIME Actions: HTTP: diff --git a/internal/api/grpc/session/v2/integration_test/session_test.go b/internal/api/grpc/session/v2/integration_test/session_test.go index b9a060c749..0982a56121 100644 --- a/internal/api/grpc/session/v2/integration_test/session_test.go +++ b/internal/api/grpc/session/v2/integration_test/session_test.go @@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), @@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "") + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "", time.Now().Add(time.Hour)) // link the user (with info from intent) Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) @@ -447,6 +447,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.Error(t, err) } +func TestServer_CreateSession_reuseIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) + require.NoError(t, err) + updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) + + // the reuse of the intent token is not allowed, not even on the same session + session2, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) + _ = session2 +} + +func TestServer_CreateSession_expiredIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second)) + require.NoError(t, err) + + // wait for the intent to expire + time.Sleep(2 * time.Second) + + _, err = Client.SetSession(LoginCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) +} + func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) { resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ UserId: userID, diff --git a/internal/api/grpc/session/v2beta/integration_test/session_test.go b/internal/api/grpc/session/v2beta/integration_test/session_test.go index d0fc1179ef..4c189e0f80 100644 --- a/internal/api/grpc/session/v2beta/integration_test/session_test.go +++ b/internal/api/grpc/session/v2beta/integration_test/session_test.go @@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { require.NoError(t, err) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ SessionId: createResp.GetSessionId(), @@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ Checks: &session.Checks{ @@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { // successful intent without known / linked user idpUserID := "id" - intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId()) + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) require.NoError(t, err) // link the user (with info from intent) @@ -448,6 +448,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { require.Error(t, err) } +func TestServer_CreateSession_reuseIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour)) + require.NoError(t, err) + updateResp, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor) + + // the reuse of the intent token is not allowed, not even on the same session + session2, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) + _ = session2 +} + +func TestServer_CreateSession_expiredIntent(t *testing.T) { + idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() + createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{ + UserId: User.GetUserId(), + }, + }, + }, + }) + require.NoError(t, err) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) + + intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second)) + require.NoError(t, err) + + // wait for the intent to expire + time.Sleep(2 * time.Second) + + _, err = Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{ + SessionId: createResp.GetSessionId(), + Checks: &session.Checks{ + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: token, + }, + }, + }) + require.Error(t, err) +} + func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) { resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ UserId: userID, diff --git a/internal/api/grpc/user/v2/integration_test/user_test.go b/internal/api/grpc/user/v2/integration_test/user_test.go index 6d5d112e98..b7dfb6c118 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -10,18 +10,16 @@ import ( "testing" "time" - "github.com/zitadel/logging" - "google.golang.org/protobuf/types/known/structpb" - "github.com/brianvoe/gofakeit/v6" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/logging" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" - "github.com/zitadel/zitadel/internal/api/grpc" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration/sink" "github.com/zitadel/zitadel/pkg/grpc/auth" @@ -1308,7 +1306,6 @@ func TestServer_UpdateHumanUser_Permission(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Client.UpdateHumanUser(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) @@ -2115,18 +2112,43 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() - intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId() + oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() + oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() + samlIdpID := Instance.AddSAMLPostProvider(IamCTX) + ldapIdpID := Instance.AddLDAPProvider(IamCTX) + authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) + require.NoError(t, err) + intentID := authURL.Query().Get("state") + expiry := time.Now().Add(1 * time.Hour) + expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00") - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "") + intentUser := Instance.CreateHumanUser(IamCTX) + _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username") require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user") + successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second)) require.NoError(t, err) - samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "") + // make sure the intent is expired + time.Sleep(2 * time.Second) + successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry) + require.NoError(t, err) + // make sure the intent is consumed + Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry) + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry) + require.NoError(t, err) + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") + require.NoError(t, err) + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") + require.NoError(t, err) + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry) + require.NoError(t, err) + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2161,7 +2183,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { wantErr: true, }, { - name: "retrieve successful intent", + name: "retrieve successful oauth intent", args: args{ CTX, &user.RetrieveIdentityProviderIntentRequest{ @@ -2182,13 +2204,15 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, UserId: "id", - UserName: "username", + UserName: "", RawInformation: func() *structpb.Struct { s, err := structpb.NewStruct(map[string]interface{}{ - "sub": "id", - "preferred_username": "username", + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, }) require.NoError(t, err) return s @@ -2220,7 +2244,107 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful expired intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulExpiredID, + IdpIntentToken: expiredToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful consumed intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulConsumedID, + IdpIntentToken: consumedToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful oidc intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessful, + IdpIntentToken: oidcToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcSequence, + }, + UserId: "", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessfulWithUserID, + IdpIntentToken: oidcWithUserIDToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcWithUserIDChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcWithUserIDSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2264,7 +2388,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2310,7 +2434,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2344,10 +2468,10 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, - IdpId: idpID, + IdpId: samlIdpID, UserId: "id", UserName: "", RawInformation: func() *structpb.Struct { @@ -2364,17 +2488,56 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful saml intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulWithUserID, + IdpIntentToken: samlWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: samlWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), + }, + }, + IdpId: samlIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Client.RetrieveIdentityProviderIntent(tt.args.ctx, tt.args.req) if tt.wantErr { require.Error(t, err) - } else { - require.NoError(t, err) + return } + require.NoError(t, err) - grpc.AllFieldsEqual(t, tt.want.ProtoReflect(), got.ProtoReflect(), grpc.CustomMappers) + assert.EqualExportedValues(t, tt.want, got) }) } } @@ -2874,7 +3037,6 @@ func TestServer_ListAuthenticationFactors(t *testing.T) { assert.ElementsMatch(t, tt.want.GetResult(), got.GetResult()) }, retryDuration, tick, "timeout waiting for expected auth methods result") - }) } } diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go index 9a092afacf..2e3331c3fc 100644 --- a/internal/api/grpc/user/v2/user.go +++ b/internal/api/grpc/user/v2/user.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "time" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/structpb" @@ -396,14 +397,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + externalUser, userID, session, 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) + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session) if err != nil { return nil, err } @@ -441,7 +442,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse return "", nil } -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) { provider, err := s.command.GetProvider(ctx, idpID, "", "") if err != nil { return nil, "", nil, err @@ -462,12 +463,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string 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 + return externalUser, userID, session, nil } func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { @@ -481,6 +477,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R if intent.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") } + if time.Now().After(intent.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-SAf42", "Errors.Intent.Expired") + } return idpIntentToIDPIntentPb(intent, s.idpAlg) } diff --git a/internal/api/grpc/user/v2beta/integration_test/user_test.go b/internal/api/grpc/user/v2beta/integration_test/user_test.go index ab2e3215ee..a5a1309d1a 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -2146,17 +2146,43 @@ func TestServer_StartIdentityProviderIntent(t *testing.T) { } func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { - idpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() - intentID := Instance.CreateIntent(CTX, idpID).GetIdpIntent().GetIdpIntentId() - successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "") + oauthIdpID := Instance.AddGenericOAuthProvider(IamCTX, gofakeit.AppName()).GetId() + oidcIdpID := Instance.AddGenericOIDCProvider(IamCTX, gofakeit.AppName()).GetId() + samlIdpID := Instance.AddSAMLPostProvider(IamCTX) + ldapIdpID := Instance.AddLDAPProvider(IamCTX) + authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) require.NoError(t, err) - successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", "user") + intentID := authURL.Query().Get("state") + expiry := time.Now().Add(1 * time.Hour) + expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00") + + intentUser := Instance.CreateHumanUser(IamCTX) + _, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username") require.NoError(t, err) - ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), idpID, "id", "user") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), idpID, "id", "") + successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second)) + require.NoError(t, err) + // make sure the intent is expired + time.Sleep(2 * time.Second) + successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry) + require.NoError(t, err) + // make sure the intent is consumed + Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken) + oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry) + require.NoError(t, err) + oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry) + require.NoError(t, err) + ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") + require.NoError(t, err) + ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") + require.NoError(t, err) + samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry) + require.NoError(t, err) + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2191,7 +2217,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { wantErr: true, }, { - name: "retrieve successful intent", + name: "retrieve successful oauth intent", args: args{ CTX, &user.RetrieveIdentityProviderIntentRequest{ @@ -2212,13 +2238,15 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, UserId: "id", - UserName: "username", + UserName: "", RawInformation: func() *structpb.Struct { s, err := structpb.NewStruct(map[string]interface{}{ - "sub": "id", - "preferred_username": "username", + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, }) require.NoError(t, err) return s @@ -2250,7 +2278,107 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdToken: gu.Ptr("idToken"), }, }, - IdpId: idpID, + IdpId: oauthIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "RawInfo": map[string]interface{}{ + "id": "id", + "preferred_username": "username", + }, + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful expired intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulExpiredID, + IdpIntentToken: expiredToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful consumed intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: successfulConsumedID, + IdpIntentToken: consumedToken, + }, + }, + wantErr: true, + }, + { + name: "retrieve successful oidc intent", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessful, + IdpIntentToken: oidcToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcSequence, + }, + UserId: "", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, + UserId: "id", + UserName: "username", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "sub": "id", + "preferred_username": "username", + }) + require.NoError(t, err) + return s + }(), + }, + }, + wantErr: false, + }, + { + name: "retrieve successful oidc intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: oidcSuccessfulWithUserID, + IdpIntentToken: oidcWithUserIDToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(oidcWithUserIDChangeDate), + ResourceOwner: Instance.ID(), + Sequence: oidcWithUserIDSequence, + }, + UserId: "user", + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Oauth{ + Oauth: &user.IDPOAuthAccessInformation{ + AccessToken: "accessToken", + IdToken: gu.Ptr("idToken"), + }, + }, + IdpId: oidcIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2294,7 +2422,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2340,7 +2468,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }(), }, }, - IdpId: idpID, + IdpId: ldapIdpID, UserId: "id", UserName: "username", RawInformation: func() *structpb.Struct { @@ -2374,10 +2502,10 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, - IdpId: idpID, + IdpId: samlIdpID, UserId: "id", UserName: "", RawInformation: func() *structpb.Struct { @@ -2394,6 +2522,45 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, wantErr: false, }, + { + name: "retrieve successful saml intent with linked user", + args: args{ + CTX, + &user.RetrieveIdentityProviderIntentRequest{ + IdpIntentId: samlSuccessfulWithUserID, + IdpIntentToken: samlWithUserToken, + }, + }, + want: &user.RetrieveIdentityProviderIntentResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.New(samlWithUserChangeDate), + ResourceOwner: Instance.ID(), + Sequence: samlWithUserSequence, + }, + IdpInformation: &user.IDPInformation{ + Access: &user.IDPInformation_Saml{ + Saml: &user.IDPSAMLAccessInformation{ + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), + }, + }, + IdpId: samlIdpID, + UserId: "id", + UserName: "", + RawInformation: func() *structpb.Struct { + s, err := structpb.NewStruct(map[string]interface{}{ + "id": "id", + "attributes": map[string]interface{}{ + "attribute1": []interface{}{"value1"}, + }, + }) + require.NoError(t, err) + return s + }(), + }, + UserId: "user", + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index 52da057906..9835de34c1 100644 --- a/internal/api/grpc/user/v2beta/user.go +++ b/internal/api/grpc/user/v2beta/user.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "time" "golang.org/x/text/language" "google.golang.org/protobuf/types/known/structpb" @@ -399,14 +400,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti if err != nil { return nil, err } - externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword()) + externalUser, userID, session, 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) + token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session) if err != nil { return nil, err } @@ -444,7 +445,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse return "", nil } -func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) { +func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) { provider, err := s.command.GetProvider(ctx, idpID, "", "") if err != nil { return nil, "", nil, err @@ -465,12 +466,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string 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 + return externalUser, userID, session, nil } func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { @@ -484,6 +480,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R if intent.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") } + if time.Now().After(intent.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-Afb2s", "Errors.Intent.Expired") + } return idpIntentToIDPIntentPb(intent, s.idpAlg) } diff --git a/internal/api/idp/idp.go b/internal/api/idp/idp.go index 01594c43ba..f85a81a8d6 100644 --- a/internal/api/idp/idp.go +++ b/internal/api/idp/idp.go @@ -287,7 +287,7 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) { userID, err := h.checkExternalUser(ctx, intent.IDPID, idpUser.GetID()) logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists") - token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session.Assertion) + token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session) if err != nil { redirectToFailureURLErr(w, r, intent, zerrors.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed")) return diff --git a/internal/api/idp/idp_test.go b/internal/api/idp/idp_test.go index 6804a035af..2f64f598a9 100644 --- a/internal/api/idp/idp_test.go +++ b/internal/api/idp/idp_test.go @@ -4,6 +4,7 @@ import ( "net/http/httptest" "net/url" "testing" + "time" "github.com/stretchr/testify/assert" @@ -14,11 +15,12 @@ import ( func Test_redirectToSuccessURL(t *testing.T) { type args struct { - id string - userID string - token string - failureURL string - successURL string + id string + userID string + token string + failureURL string + successURL string + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -59,7 +61,7 @@ func Test_redirectToSuccessURL(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) @@ -71,11 +73,12 @@ func Test_redirectToSuccessURL(t *testing.T) { func Test_redirectToFailureURL(t *testing.T) { type args struct { - id string - failureURL string - successURL string - err string - desc string + id string + failureURL string + successURL string + err string + desc string + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -115,7 +118,7 @@ func Test_redirectToFailureURL(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) @@ -127,10 +130,11 @@ func Test_redirectToFailureURL(t *testing.T) { func Test_redirectToFailureURLErr(t *testing.T) { type args struct { - id string - failureURL string - successURL string - err error + id string + failureURL string + successURL string + err error + maxIdPIntentLifetime time.Duration } type res struct { want string @@ -158,7 +162,7 @@ func Test_redirectToFailureURLErr(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) resp := httptest.NewRecorder() - wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id) + wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime) wm.FailureURL, _ = url.Parse(tt.args.failureURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL) diff --git a/internal/command/command.go b/internal/command/command.go index ab047fccdb..6cad011318 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -79,6 +79,7 @@ type Commands struct { publicKeyLifetime time.Duration certificateLifetime time.Duration defaultSecretGenerators *SecretGenerators + maxIdPIntentLifetime time.Duration samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error) webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error) @@ -150,6 +151,7 @@ func StartCommands( privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime, publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime, certificateLifetime: defaults.KeyConfig.CertificateLifetime, + maxIdPIntentLifetime: defaults.MaxIdPIntentLifetime, idpConfigEncryption: idpConfigEncryption, smtpEncryption: smtpEncryption, smsEncryption: smsEncryption, diff --git a/internal/command/idp_intent.go b/internal/command/idp_intent.go index 483cdcd08e..e7180bc5b6 100644 --- a/internal/command/idp_intent.go +++ b/internal/command/idp_intent.go @@ -7,7 +7,6 @@ import ( "encoding/xml" "net/url" - "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -19,8 +18,10 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/apple" "github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/jwt" + "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/zerrors" ) @@ -64,10 +65,7 @@ func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureU if err != nil { return nil, nil, err } - writeModel := NewIDPIntentWriteModel(id, resourceOwner) - if err != nil { - return nil, nil, err - } + writeModel := NewIDPIntentWriteModel(id, resourceOwner, c.maxIdPIntentLifetime) //nolint: staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL)) @@ -180,6 +178,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr userID, accessToken, idToken, + idpSession.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -188,7 +187,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr return token, nil } -func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, assertion *saml.Assertion) (string, error) { +func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *saml.Session) (string, error) { token, err := c.generateIntentToken(writeModel.AggregateID) if err != nil { return "", err @@ -197,7 +196,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte if err != nil { return "", err } - assertionData, err := xml.Marshal(assertion) + assertionData, err := xml.Marshal(session.Assertion) if err != nil { return "", err } @@ -213,6 +212,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte idpUser.GetPreferredUsername(), userID, assertionEnc, + session.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -237,7 +237,7 @@ func (c *Commands) generateIntentToken(intentID string) (string, error) { 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) { +func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *ldap.Session) (string, error) { token, err := c.generateIntentToken(writeModel.AggregateID) if err != nil { return "", err @@ -246,6 +246,10 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte if err != nil { return "", err } + attributes := make(map[string][]string, len(session.Entry.Attributes)) + for _, item := range session.Entry.Attributes { + attributes[item.Name] = item.Values + } cmd := idpintent.NewLDAPSucceededEvent( ctx, IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), @@ -254,6 +258,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte idpUser.GetPreferredUsername(), userID, attributes, + session.ExpiresAt(), ) err = c.pushAppendAndReduce(ctx, writeModel, cmd) if err != nil { @@ -273,7 +278,7 @@ func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWrite } func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner string) (*IDPIntentWriteModel, error) { - writeModel := NewIDPIntentWriteModel(id, resourceOwner) + writeModel := NewIDPIntentWriteModel(id, resourceOwner, c.maxIdPIntentLifetime) err := c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err diff --git a/internal/command/idp_intent_model.go b/internal/command/idp_intent_model.go index 62794323e1..659381f8ac 100644 --- a/internal/command/idp_intent_model.go +++ b/internal/command/idp_intent_model.go @@ -2,6 +2,7 @@ package command import ( "net/url" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" @@ -28,18 +29,29 @@ type IDPIntentWriteModel struct { RequestID string Assertion *crypto.CryptoValue - State domain.IDPIntentState + State domain.IDPIntentState + succeededAt time.Time + maxIdPIntentLifetime time.Duration + expiresAt time.Time } -func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel { +func NewIDPIntentWriteModel(id, resourceOwner string, maxIdPIntentLifetime time.Duration) *IDPIntentWriteModel { return &IDPIntentWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: id, ResourceOwner: resourceOwner, }, + maxIdPIntentLifetime: maxIdPIntentLifetime, } } +func (wm *IDPIntentWriteModel) ExpiresAt() time.Time { + if wm.expiresAt.IsZero() { + return wm.succeededAt.Add(wm.maxIdPIntentLifetime) + } + return wm.expiresAt +} + func (wm *IDPIntentWriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { @@ -55,6 +67,8 @@ func (wm *IDPIntentWriteModel) Reduce() error { wm.reduceLDAPSucceededEvent(e) case *idpintent.FailedEvent: wm.reduceFailedEvent(e) + case *idpintent.ConsumedEvent: + wm.reduceConsumedEvent(e) } } return wm.WriteModel.Reduce() @@ -73,6 +87,7 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder { idpintent.SAMLRequestEventType, idpintent.LDAPSucceededEventType, idpintent.FailedEventType, + idpintent.ConsumedEventType, ). Builder() } @@ -91,6 +106,8 @@ func (wm *IDPIntentWriteModel) reduceSAMLSucceededEvent(e *idpintent.SAMLSucceed wm.IDPUserName = e.IDPUserName wm.Assertion = e.Assertion wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) { @@ -100,6 +117,8 @@ func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceed wm.IDPUserName = e.IDPUserName wm.IDPEntryAttributes = e.EntryAttributes wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededEvent) { @@ -110,6 +129,8 @@ func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededE wm.IDPAccessToken = e.IDPAccessToken wm.IDPIDToken = e.IDPIDToken wm.State = domain.IDPIntentStateSucceeded + wm.succeededAt = e.CreationDate() + wm.expiresAt = e.ExpiresAt } func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEvent) { @@ -120,6 +141,10 @@ func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) { wm.State = domain.IDPIntentStateFailed } +func (wm *IDPIntentWriteModel) reduceConsumedEvent(e *idpintent.ConsumedEvent) { + wm.State = domain.IDPIntentStateConsumed +} + func IDPIntentAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { return &eventstore.Aggregate{ Type: idpintent.AggregateType, diff --git a/internal/command/idp_intent_test.go b/internal/command/idp_intent_test.go index 832e2e9902..b2d3f76c37 100644 --- a/internal/command/idp_intent_test.go +++ b/internal/command/idp_intent_test.go @@ -4,8 +4,10 @@ import ( "context" "net/url" "testing" + "time" - "github.com/crewjam/saml" + crewjam_saml "github.com/crewjam/saml" + goldap "github.com/go-ldap/ldap/v3" "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,6 +28,7 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/ldap" "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" + "github.com/zitadel/zitadel/internal/idp/providers/saml" rep_idp "github.com/zitadel/zitadel/internal/repository/idp" "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/instance" @@ -758,7 +761,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -779,7 +782,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), idpSession: &oauth.Session{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ @@ -813,6 +816,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { Crypted: []byte("accessToken"), }, "idToken", + time.Time{}, ) return event }(), @@ -821,7 +825,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), idpSession: &openid.Session{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ @@ -864,7 +868,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { ctx context.Context writeModel *IDPIntentWriteModel idpUser idp.User - assertion *saml.Assertion + session *saml.Session userID string } type res struct { @@ -889,7 +893,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "ro"), + writeModel: NewIDPIntentWriteModel("id", "ro", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -914,14 +918,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { KeyID: "id", Crypted: []byte(""), }, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - assertion: &saml.Assertion{ID: "id"}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &saml.Session{ + Assertion: &crewjam_saml.Assertion{ID: "id"}, + }, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", UserInfoProfile: oidc.UserInfoProfile{ @@ -952,14 +959,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { KeyID: "id", Crypted: []byte(""), }, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - assertion: &saml.Assertion{ID: "id"}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &saml.Session{ + Assertion: &crewjam_saml.Assertion{ID: "id"}, + }, idpUser: openid.NewUser(&oidc.UserInfo{ Subject: "id", UserInfoProfile: oidc.UserInfoProfile{ @@ -979,7 +989,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) { eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } - got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.assertion) + got, err := c.SucceedSAMLIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.token, got) }) @@ -1019,7 +1029,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), request: "request", }, res{}, @@ -1047,7 +1057,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { writeModel *IDPIntentWriteModel idpUser idp.User userID string - attributes map[string][]string + session *ldap.Session } type res struct { token string @@ -1071,7 +1081,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), }, res{ err: zerrors.ThrowInternal(nil, "id", "encryption failed"), @@ -1091,14 +1101,24 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { "username", "", map[string][]string{"id": {"id"}}, + time.Time{}, ), ), ), }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), - attributes: map[string][]string{"id": {"id"}}, + writeModel: NewIDPIntentWriteModel("id", "instance", 0), + session: &ldap.Session{ + Entry: &goldap.Entry{ + Attributes: []*goldap.EntryAttribute{ + { + Name: "id", + Values: []string{"id"}, + }, + }, + }, + }, idpUser: ldap.NewUser( "id", "", @@ -1126,7 +1146,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) { eventstore: tt.fields.eventstore(t), idpConfigEncryption: tt.fields.idpConfigEncryption, } - got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.attributes) + got, err := c.SucceedLDAPIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.userID, tt.args.session) require.ErrorIs(t, err, tt.res.err) assert.Equal(t, tt.res.token, got) }) @@ -1166,7 +1186,7 @@ func TestCommands_FailIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), reason: "reason", }, res{ diff --git a/internal/command/session.go b/internal/command/session.go index d00e541e62..3c06c22967 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -17,6 +17,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/notification/senders" + "github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/zerrors" @@ -32,31 +33,33 @@ type SessionCommands struct { eventstore *eventstore.Eventstore eventCommands []eventstore.Command - hasher *crypto.Hasher - intentAlg crypto.EncryptionAlgorithm - totpAlg crypto.EncryptionAlgorithm - otpAlg crypto.EncryptionAlgorithm - createCode encryptedCodeWithDefaultFunc - createPhoneCode encryptedCodeGeneratorWithDefaultFunc - createToken func(sessionID string) (id string, token string, err error) - getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) - now func() time.Time + hasher *crypto.Hasher + intentAlg crypto.EncryptionAlgorithm + totpAlg crypto.EncryptionAlgorithm + otpAlg crypto.EncryptionAlgorithm + createCode encryptedCodeWithDefaultFunc + createPhoneCode encryptedCodeGeneratorWithDefaultFunc + createToken func(sessionID string) (id string, token string, err error) + getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) + now func() time.Time + maxIdPIntentLifetime time.Duration } func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands { return &SessionCommands{ - sessionCommands: cmds, - sessionWriteModel: session, - eventstore: c.eventstore, - hasher: c.userPasswordHasher, - intentAlg: c.idpConfigEncryption, - totpAlg: c.multifactors.OTP.CryptoMFA, - otpAlg: c.userEncryption, - createCode: c.newEncryptedCodeWithDefault, - createPhoneCode: c.newPhoneCode, - createToken: c.sessionTokenCreator, - getCodeVerifier: c.phoneCodeVerifierFromConfig, - now: time.Now, + sessionCommands: cmds, + sessionWriteModel: session, + eventstore: c.eventstore, + hasher: c.userPasswordHasher, + intentAlg: c.idpConfigEncryption, + totpAlg: c.multifactors.OTP.CryptoMFA, + otpAlg: c.userEncryption, + createCode: c.newEncryptedCodeWithDefault, + createPhoneCode: c.newPhoneCode, + createToken: c.sessionTokenCreator, + getCodeVerifier: c.phoneCodeVerifierFromConfig, + now: time.Now, + maxIdPIntentLifetime: c.maxIdPIntentLifetime, } } @@ -92,7 +95,7 @@ func CheckIntent(intentID, token string) SessionCommand { if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil { return nil, err } - cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "") + cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "", cmd.maxIdPIntentLifetime) err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel) if err != nil { return nil, err @@ -100,6 +103,9 @@ func CheckIntent(intentID, token string) SessionCommand { if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded") } + if time.Now().After(cmd.intentWriteModel.ExpiresAt()) { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired") + } if cmd.intentWriteModel.UserID != "" { if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser") @@ -168,6 +174,7 @@ func (s *SessionCommands) PasswordChecked(ctx context.Context, checkedAt time.Ti func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time) { s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt)) + s.eventCommands = append(s.eventCommands, idpintent.NewConsumedEvent(ctx, IDPIntentAggregateFromWriteModel(&s.intentWriteModel.WriteModel))) } func (s *SessionCommands) WebAuthNChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement, rpid string) { diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 5fba985148..023137cce7 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -695,6 +695,7 @@ func TestCommands_updateSession(t *testing.T) { "userID2", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -757,6 +758,111 @@ func TestCommands_updateSession(t *testing.T) { err: zerrors.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken"), }, }, + { + "set user, intent token already consumed", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, + "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), + ), + eventFromEventPusher( + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + nil, + "idpUserID", + "idpUsername", + "userID", + nil, + "", + time.Now().Add(time.Hour), + ), + ), + eventFromEventPusher( + idpintent.NewConsumedEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + ), + ), + ), + ), + }, + args{ + ctx: authz.NewMockContext("instance1", "", ""), + checks: &SessionCommands{ + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), + sessionCommands: []SessionCommand{ + CheckUser("userID", "org1", &language.Afrikaans), + CheckIntent("intent", "aW50ZW50"), + }, + createToken: func(sessionID string) (string, string, error) { + return "tokenID", + "token", + nil + }, + intentAlg: decryption(nil), + now: func() time.Time { + return testNow + }, + }, + metadata: map[string][]byte{ + "key": []byte("value"), + }, + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded"), + }, + }, + { + "set user, intent token already expired", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate, + "username", "", "", "", "", language.English, domain.GenderUnspecified, "", false), + ), + eventFromEventPusher( + idpintent.NewSucceededEvent(context.Background(), + &idpintent.NewAggregate("intent", "instance1").Aggregate, + nil, + "idpUserID", + "idpUsername", + "userID", + nil, + "", + time.Now().Add(-time.Hour), + ), + ), + ), + ), + }, + args{ + ctx: authz.NewMockContext("instance1", "", ""), + checks: &SessionCommands{ + sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"), + sessionCommands: []SessionCommand{ + CheckUser("userID", "org1", &language.Afrikaans), + CheckIntent("intent", "aW50ZW50"), + }, + createToken: func(sessionID string) (string, string, error) { + return "tokenID", + "token", + nil + }, + intentAlg: decryption(nil), + now: func() time.Time { + return testNow + }, + }, + metadata: map[string][]byte{ + "key": []byte("value"), + }, + }, + res{ + err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired"), + }, + }, { "set user, intent, metadata and token", fields{ @@ -768,13 +874,14 @@ func TestCommands_updateSession(t *testing.T) { ), eventFromEventPusher( idpintent.NewSucceededEvent(context.Background(), - &idpintent.NewAggregate("id", "instance1").Aggregate, + &idpintent.NewAggregate("intent", "instance1").Aggregate, nil, "idpUserID", "idpUsername", "userID", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -783,6 +890,7 @@ func TestCommands_updateSession(t *testing.T) { "userID", "org1", testNow, &language.Afrikaans), session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), + idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate), session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, map[string][]byte{"key": []byte("value")}), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, @@ -841,13 +949,14 @@ func TestCommands_updateSession(t *testing.T) { ), eventFromEventPusher( idpintent.NewSucceededEvent(context.Background(), - &idpintent.NewAggregate("id", "instance1").Aggregate, + &idpintent.NewAggregate("intent", "instance1").Aggregate, nil, "idpUserID", "idpUsername", "", nil, "", + time.Now().Add(time.Hour), ), ), ), @@ -865,6 +974,7 @@ func TestCommands_updateSession(t *testing.T) { "userID", "org1", testNow, &language.Afrikaans), session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, testNow), + idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate), session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, "tokenID"), ), diff --git a/internal/config/systemdefaults/system_defaults.go b/internal/config/systemdefaults/system_defaults.go index f6d39befe7..827dd61f73 100644 --- a/internal/config/systemdefaults/system_defaults.go +++ b/internal/config/systemdefaults/system_defaults.go @@ -7,15 +7,16 @@ import ( ) type SystemDefaults struct { - SecretGenerators SecretGenerators - PasswordHasher crypto.HashConfig - SecretHasher crypto.HashConfig - Multifactors MultifactorConfig - DomainVerification DomainVerification - Notifications Notifications - KeyConfig KeyConfig - DefaultQueryLimit uint64 - MaxQueryLimit uint64 + SecretGenerators SecretGenerators + PasswordHasher crypto.HashConfig + SecretHasher crypto.HashConfig + Multifactors MultifactorConfig + DomainVerification DomainVerification + Notifications Notifications + KeyConfig KeyConfig + DefaultQueryLimit uint64 + MaxQueryLimit uint64 + MaxIdPIntentLifetime time.Duration } type SecretGenerators struct { diff --git a/internal/domain/idp.go b/internal/domain/idp.go index e2571f6b0d..bea106298b 100644 --- a/internal/domain/idp.go +++ b/internal/domain/idp.go @@ -115,6 +115,7 @@ const ( IDPIntentStateStarted IDPIntentStateSucceeded IDPIntentStateFailed + IDPIntentStateConsumed idpIntentStateCount ) diff --git a/internal/idp/providers/apple/session.go b/internal/idp/providers/apple/session.go index 5e9143f050..cca15141e6 100644 --- a/internal/idp/providers/apple/session.go +++ b/internal/idp/providers/apple/session.go @@ -10,6 +10,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/oidc" ) +var _ idp.Session = (*Session)(nil) + // Session extends the [oidc.Session] with the formValues returned from the callback. // This enables to parse the user (name and email), which Apple only returns as form params on registration type Session struct { diff --git a/internal/idp/providers/azuread/session.go b/internal/idp/providers/azuread/session.go index a9d8df2e8c..71c85fd1d6 100644 --- a/internal/idp/providers/azuread/session.go +++ b/internal/idp/providers/azuread/session.go @@ -3,6 +3,7 @@ package azuread import ( "context" "net/http" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -12,6 +13,8 @@ import ( "github.com/zitadel/zitadel/internal/idp/providers/oauth" ) +var _ idp.Session = (*Session)(nil) + // Session extends the [oauth.Session] to be able to handle the id_token and to implement the [idp.SessionSupportsMigration] functionality type Session struct { *Provider @@ -70,6 +73,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return user, nil } +func (s *Session) ExpiresAt() time.Time { + if s.OAuthSession == nil { + return time.Time{} + } + return s.OAuthSession.ExpiresAt() +} + // Tokens returns the [oidc.Tokens] of the underlying [oauth.Session]. func (s *Session) Tokens() *oidc.Tokens[*oidc.IDTokenClaims] { return s.oauth().Tokens diff --git a/internal/idp/providers/jwt/session.go b/internal/idp/providers/jwt/session.go index 54fcc039eb..ced2a06ef1 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -48,6 +48,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return &User{s.Tokens.IDTokenClaims}, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil || s.Tokens.IDTokenClaims == nil { + return time.Time{} + } + return s.Tokens.IDTokenClaims.GetExpiration() +} + 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 diff --git a/internal/idp/providers/ldap/session.go b/internal/idp/providers/ldap/session.go index c3ca5c6364..a6b2dad90c 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -82,6 +82,10 @@ func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) { ) } +func (s *Session) ExpiresAt() time.Time { + return time.Time{} // falls back to the default expiration time +} + func tryBind( server string, startTLS bool, diff --git a/internal/idp/providers/oauth/session.go b/internal/idp/providers/oauth/session.go index 065ad3b213..d2c464fd50 100644 --- a/internal/idp/providers/oauth/session.go +++ b/internal/idp/providers/oauth/session.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" httphelper "github.com/zitadel/oidc/v3/pkg/http" @@ -51,6 +52,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return mapper, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil { + return time.Time{} + } + return s.Tokens.Expiry +} + func (s *Session) authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing diff --git a/internal/idp/providers/oidc/session.go b/internal/idp/providers/oidc/session.go index bd6303f2e5..bad99e3db7 100644 --- a/internal/idp/providers/oidc/session.go +++ b/internal/idp/providers/oidc/session.go @@ -3,6 +3,7 @@ package oidc import ( "context" "errors" + "time" "github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/oidc" @@ -57,6 +58,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return u, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Tokens == nil { + return time.Time{} + } + return s.Tokens.Expiry +} + func (s *Session) Authorize(ctx context.Context) (err error) { if s.Code == "" { return ErrCodeMissing diff --git a/internal/idp/providers/saml/session.go b/internal/idp/providers/saml/session.go index 49a04e49cb..68259bd823 100644 --- a/internal/idp/providers/saml/session.go +++ b/internal/idp/providers/saml/session.go @@ -6,6 +6,7 @@ import ( "errors" "net/http" "net/url" + "time" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" @@ -102,6 +103,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) { return userMapper, nil } +func (s *Session) ExpiresAt() time.Time { + if s.Assertion == nil || s.Assertion.Conditions == nil { + return time.Time{} + } + return s.Assertion.Conditions.NotOnOrAfter +} + func (s *Session) transientMappingID() (string, error) { for _, statement := range s.Assertion.AttributeStatements { for _, attribute := range statement.Attributes { diff --git a/internal/idp/session.go b/internal/idp/session.go index 6d6519a54c..a26dbee041 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -2,12 +2,14 @@ package idp import ( "context" + "time" ) // Session is the minimal implementation for a session of a 3rd party authentication [Provider] type Session interface { GetAuth(ctx context.Context) (content string, redirect bool) FetchUser(ctx context.Context) (User, error) + ExpiresAt() time.Time } // SessionSupportsMigration is an optional extension to the Session interface. diff --git a/internal/integration/client.go b/internal/integration/client.go index a480a86ce0..f68956ac8a 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -472,6 +472,26 @@ func (i *Instance) AddOrgGenericOAuthProvider(ctx context.Context, name string) return resp } +func (i *Instance) AddGenericOIDCProvider(ctx context.Context, name string) *admin.AddGenericOIDCProviderResponse { + resp, err := i.Client.Admin.AddGenericOIDCProvider(ctx, &admin.AddGenericOIDCProviderRequest{ + Name: name, + Issuer: "https://example.com", + ClientId: "clientID", + ClientSecret: "clientSecret", + Scopes: []string{"openid", "profile", "email"}, + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME, + }, + IsIdTokenMapping: false, + }) + logging.OnError(err).Panic("create generic oidc idp") + return resp +} + func (i *Instance) AddSAMLProvider(ctx context.Context) string { resp, err := i.Client.Admin.AddSAMLProvider(ctx, &admin.AddSAMLProviderRequest{ Name: "saml-idp", @@ -526,6 +546,32 @@ func (i *Instance) AddSAMLPostProvider(ctx context.Context) string { return resp.GetId() } +func (i *Instance) AddLDAPProvider(ctx context.Context) string { + resp, err := i.Client.Admin.AddLDAPProvider(ctx, &admin.AddLDAPProviderRequest{ + Name: "ldap-idp-post", + Servers: []string{"https://localhost:8000"}, + StartTls: false, + BaseDn: "baseDn", + BindDn: "admin", + BindPassword: "admin", + UserBase: "dn", + UserObjectClasses: []string{"user"}, + UserFilters: []string{"(objectclass=*)"}, + Timeout: durationpb.New(10 * time.Second), + Attributes: &idp.LDAPAttributes{ + IdAttribute: "id", + }, + ProviderOptions: &idp.Options{ + IsLinkingAllowed: true, + IsCreationAllowed: true, + IsAutoCreation: true, + IsAutoUpdate: true, + }, + }) + logging.OnError(err).Panic("create ldap idp") + return resp.GetId() +} + func (i *Instance) CreateIntent(ctx context.Context, idpID string) *user_v2.StartIdentityProviderIntentResponse { resp, err := i.Client.UserV2.StartIdentityProviderIntent(ctx, &user_v2.StartIdentityProviderIntentRequest{ IdpId: idpID, @@ -597,6 +643,23 @@ func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, user createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() } +func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID, intentID, intentToken string) (id, token string, start, change time.Time) { + createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ + Checks: &session.Checks{ + User: &session.CheckUser{ + Search: &session.CheckUser_UserId{UserId: userID}, + }, + IdpIntent: &session.CheckIDPIntent{ + IdpIntentId: intentID, + IdpIntentToken: intentToken, + }, + }, + }) + require.NoError(t, err) + return createResp.GetSessionId(), createResp.GetSessionToken(), + createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() +} + func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse { resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{ GrantedOrgId: grantedOrgID, diff --git a/internal/integration/sink/server.go b/internal/integration/sink/server.go index eea7c893b6..acf9e12353 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -15,7 +15,9 @@ import ( "sync/atomic" "time" + crewjam_saml "github.com/crewjam/saml" "github.com/go-chi/chi/v5" + goldap "github.com/go-ldap/ldap/v3" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" "github.com/zitadel/logging" @@ -23,11 +25,10 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" - crewjam_saml "github.com/crewjam/saml" - "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/idp/providers/ldap" + "github.com/zitadel/zitadel/internal/idp/providers/oauth" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" "github.com/zitadel/zitadel/internal/idp/providers/saml" ) @@ -48,7 +49,7 @@ func CallURL(ch Channel) string { return u.String() } -func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -59,6 +60,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -66,7 +68,26 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } -func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) { +func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { + u := url.URL{ + Scheme: "http", + Host: host, + Path: successfulIntentOIDCPath(), + } + resp, err := callIntent(u.String(), &SuccessfulIntentRequest{ + InstanceID: instanceID, + IDPID: idpID, + IDPUserID: idpUserID, + UserID: userID, + Expiry: expiry, + }) + if err != nil { + return "", "", time.Time{}, uint64(0), err + } + return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil +} + +func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -77,6 +98,7 @@ func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -120,6 +142,7 @@ func StartServer(commands *command.Commands) (close func()) { router.HandleFunc(rootPath(ch), fwd.receiveHandler) router.HandleFunc(subscribePath(ch), fwd.subscriptionHandler) router.HandleFunc(successfulIntentOAuthPath(), successfulIntentHandler(commands, createSuccessfulOAuthIntent)) + router.HandleFunc(successfulIntentOIDCPath(), successfulIntentHandler(commands, createSuccessfulOIDCIntent)) router.HandleFunc(successfulIntentSAMLPath(), successfulIntentHandler(commands, createSuccessfulSAMLIntent)) router.HandleFunc(successfulIntentLDAPPath(), successfulIntentHandler(commands, createSuccessfulLDAPIntent)) } @@ -160,6 +183,10 @@ func successfulIntentOAuthPath() string { return path.Join(successfulIntentPath(), "/", "oauth") } +func successfulIntentOIDCPath() string { + return path.Join(successfulIntentPath(), "/", "oidc") +} + func successfulIntentSAMLPath() string { return path.Join(successfulIntentPath(), "/", "saml") } @@ -259,10 +286,11 @@ func readLoop(ws *websocket.Conn) (done chan error) { } type SuccessfulIntentRequest struct { - InstanceID string `json:"instance_id"` - IDPID string `json:"idp_id"` - IDPUserID string `json:"idp_user_id"` - UserID string `json:"user_id"` + InstanceID string `json:"instance_id"` + IDPID string `json:"idp_id"` + IDPUserID string `json:"idp_user_id"` + UserID string `json:"user_id"` + Expiry time.Time `json:"expiry"` } type SuccessfulIntentResponse struct { IntentID string `json:"intent_id"` @@ -335,6 +363,42 @@ func createIntent(ctx context.Context, cmd *command.Commands, instanceID, idpID } func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { + intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) + if err != nil { + return nil, err + } + writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) + if err != nil { + return nil, err + } + idAttribute := "id" + idpUser := oauth.NewUserMapper(idAttribute) + idpUser.RawInfo = map[string]interface{}{ + idAttribute: req.IDPUserID, + "preferred_username": "username", + } + idpSession := &oauth.Session{ + Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ + Token: &oauth2.Token{ + AccessToken: "accessToken", + Expiry: req.Expiry, + }, + IDToken: "idToken", + }, + } + token, err := cmd.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, req.UserID) + if err != nil { + return nil, err + } + return &SuccessfulIntentResponse{ + intentID, + token, + writeModel.ChangeDate, + writeModel.ProcessedSequence, + }, nil +} + +func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req *SuccessfulIntentRequest) (*SuccessfulIntentResponse, error) { intentID, err := createIntent(ctx, cmd, req.InstanceID, req.IDPID) writeModel, err := cmd.GetIntentWriteModel(ctx, intentID, req.InstanceID) idpUser := openid.NewUser( @@ -349,6 +413,7 @@ func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ AccessToken: "accessToken", + Expiry: req.Expiry, }, IDToken: "idToken", }, @@ -373,9 +438,16 @@ func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req ID: req.IDPUserID, Attributes: map[string][]string{"attribute1": {"value1"}}, } - assertion := &crewjam_saml.Assertion{ID: "id"} + session := &saml.Session{ + Assertion: &crewjam_saml.Assertion{ + ID: "id", + Conditions: &crewjam_saml.Conditions{ + NotOnOrAfter: req.Expiry, + }, + }, + } - token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, assertion) + token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, session) if err != nil { return nil, err } @@ -407,8 +479,14 @@ func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req "", "", ) - attributes := map[string][]string{"id": {req.IDPUserID}, "username": {username}, "language": {lang.String()}} - token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes) + session := &ldap.Session{Entry: &goldap.Entry{ + Attributes: []*goldap.EntryAttribute{ + {Name: "id", Values: []string{req.IDPUserID}}, + {Name: "username", Values: []string{username}}, + {Name: "language", Values: []string{lang.String()}}, + }, + }} + token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, session) if err != nil { return nil, err } diff --git a/internal/repository/idpintent/eventstore.go b/internal/repository/idpintent/eventstore.go index ea94803973..6bec32c735 100644 --- a/internal/repository/idpintent/eventstore.go +++ b/internal/repository/idpintent/eventstore.go @@ -11,4 +11,5 @@ func init() { eventstore.RegisterFilterEventMapper(AggregateType, SAMLRequestEventType, SAMLRequestEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper) + eventstore.RegisterFilterEventMapper(AggregateType, ConsumedEventType, eventstore.GenericEventMapper[ConsumedEvent]) } diff --git a/internal/repository/idpintent/intent.go b/internal/repository/idpintent/intent.go index 9ac1a875cc..a0e5180a14 100644 --- a/internal/repository/idpintent/intent.go +++ b/internal/repository/idpintent/intent.go @@ -3,6 +3,7 @@ package idpintent import ( "context" "net/url" + "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/eventstore" @@ -16,6 +17,7 @@ const ( SAMLRequestEventType = instanceEventTypePrefix + "saml.requested" LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded" FailedEventType = instanceEventTypePrefix + "failed" + ConsumedEventType = instanceEventTypePrefix + "consumed" ) type StartedEvent struct { @@ -76,6 +78,7 @@ type SucceededEvent struct { IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"` IDPIDToken string `json:"idpIdToken,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSucceededEvent( @@ -87,6 +90,7 @@ func NewSucceededEvent( userID string, idpAccessToken *crypto.CryptoValue, idpIDToken string, + expiresAt time.Time, ) *SucceededEvent { return &SucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -100,6 +104,7 @@ func NewSucceededEvent( UserID: userID, IDPAccessToken: idpAccessToken, IDPIDToken: idpIDToken, + ExpiresAt: expiresAt, } } @@ -133,6 +138,7 @@ type SAMLSucceededEvent struct { UserID string `json:"userId,omitempty"` Assertion *crypto.CryptoValue `json:"assertion,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSAMLSucceededEvent( @@ -143,6 +149,7 @@ func NewSAMLSucceededEvent( idpUserName, userID string, assertion *crypto.CryptoValue, + expiresAt time.Time, ) *SAMLSucceededEvent { return &SAMLSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -155,6 +162,7 @@ func NewSAMLSucceededEvent( IDPUserName: idpUserName, UserID: userID, Assertion: assertion, + ExpiresAt: expiresAt, } } @@ -230,6 +238,7 @@ type LDAPSucceededEvent struct { UserID string `json:"userId,omitempty"` EntryAttributes map[string][]string `json:"user,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewLDAPSucceededEvent( @@ -240,6 +249,7 @@ func NewLDAPSucceededEvent( idpUserName, userID string, attributes map[string][]string, + expiresAt time.Time, ) *LDAPSucceededEvent { return &LDAPSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -252,6 +262,7 @@ func NewLDAPSucceededEvent( IDPUserName: idpUserName, UserID: userID, EntryAttributes: attributes, + ExpiresAt: expiresAt, } } @@ -317,3 +328,32 @@ func FailedEventMapper(event eventstore.Event) (eventstore.Event, error) { return e, nil } + +type ConsumedEvent struct { + eventstore.BaseEvent `json:"-"` +} + +func NewConsumedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, +) *ConsumedEvent { + return &ConsumedEvent{ + BaseEvent: *eventstore.NewBaseEventForPush( + ctx, + aggregate, + ConsumedEventType, + ), + } +} + +func (e *ConsumedEvent) Payload() interface{} { + return e +} + +func (e *ConsumedEvent) UniqueConstraints() []*eventstore.UniqueConstraint { + return nil +} + +func (e *ConsumedEvent) SetBaseEvent(base *eventstore.BaseEvent) { + e.BaseEvent = *base +} diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index d4cf88f62e..6a7cb985e7 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -555,6 +555,7 @@ Errors: StateMissing: В заявката липсва параметър състояние NotStarted: Намерението не е стартирано или вече е прекратено NotSucceeded: Намерението не е успешно + Expired: Намерението е изтекло TokenCreationFailed: Неуспешно създаване на токен InvalidToken: Знакът за намерение е невалиден OtherUser: Намерение, предназначено за друг потребител diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 9a990e0828..d9f805ef05 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -535,6 +535,7 @@ Errors: StateMissing: V požadavku chybí parametr stavu NotStarted: Záměr nebyl zahájen nebo již byl ukončen NotSucceeded: Záměr nebyl úspěšný + Expired: Záměr vypršel TokenCreationFailed: Vytvoření tokenu selhalo InvalidToken: Token záměru je neplatný OtherUser: Záměr určený pro jiného uživatele diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 601fc2ba68..87994e317b 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: State parameter fehlt im Request NotStarted: Intent wurde nicht gestartet oder wurde bereits beendet NotSucceeded: Intent war nicht erfolgreich + Expired: Intent ist abgelaufen TokenCreationFailed: Tokenerstellung schlug fehl InvalidToken: Intent Token ist ungültig OtherUser: Intent ist für anderen Benutzer gedacht diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 164b5b6866..3cc496347b 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -538,6 +538,7 @@ Errors: StateMissing: State parameter is missing in the request NotStarted: Intent is not started or was already terminated NotSucceeded: Intent has not succeeded + Expired: Intent has expired TokenCreationFailed: Token creation failed InvalidToken: Intent Token is invalid OtherUser: Intent meant for another user diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 87da41c37d..65c885b1b4 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: Falta un parámetro de estado en la solicitud NotStarted: La intención no se ha iniciado o ya ha finalizado NotSucceeded: Intento fallido + Expired: La intención ha expirado TokenCreationFailed: Fallo en la creación del token InvalidToken: El token de la intención no es válido OtherUser: Destinado a otro usuario diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index b60b7a2ffd..f5c7ce6a6d 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: Paramètre d'état manquant dans la requête NotStarted: Intent n'a pas démarré ou s'est déjà terminé NotSucceeded: l'intention n'a pas abouti + Expired: L'intention a expiré TokenCreationFailed: La création du token a échoué InvalidToken: Le jeton d'intention n'est pas valide OtherUser: Intention destinée à un autre utilisateur diff --git a/internal/static/i18n/hu.yaml b/internal/static/i18n/hu.yaml index d33b5f47bc..f2ab9998d0 100644 --- a/internal/static/i18n/hu.yaml +++ b/internal/static/i18n/hu.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: A kérésből hiányzik a State paraméter NotStarted: Az intent nem indult el, vagy már befejeződött NotSucceeded: Az intent nem sikerült + Expired: A kérésből lejárt TokenCreationFailed: A token létrehozása nem sikerült InvalidToken: Az Intent Token érvénytelen OtherUser: Az intent egy másik felhasználónak szól diff --git a/internal/static/i18n/id.yaml b/internal/static/i18n/id.yaml index 449f91ffdc..87750008ad 100644 --- a/internal/static/i18n/id.yaml +++ b/internal/static/i18n/id.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: Parameter status tidak ada dalam permintaan NotStarted: Niat belum dimulai atau sudah dihentikan NotSucceeded: Niatnya belum berhasil + Expired: Kode sudah habis masa berlakunya TokenCreationFailed: Pembuatan token gagal InvalidToken: Token Niat tidak valid OtherUser: Maksudnya ditujukan untuk pengguna lain diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 8310032f7f..b53d99b126 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: parametro di stato mancante nella richiesta NotStarted: l'intento non è stato avviato o è già stato terminato NotSucceeded: l'intento non è andato a buon fine + Expired: L'intento è scaduto TokenCreationFailed: creazione del token fallita InvalidToken: Il token dell'intento non è valido OtherUser: Intento destinato a un altro utente diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index b61318b537..473f5e0c84 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -526,6 +526,7 @@ Errors: StateMissing: リクエストに State パラメータがありません NotStarted: インテントが開始されなかったか、既に終了している NotSucceeded: インテントが成功しなかった + Expired: 意図の有効期限が切れました TokenCreationFailed: トークンの作成に失敗しました InvalidToken: インテントのトークンが無効である OtherUser: 他のユーザーを意図している diff --git a/internal/static/i18n/ko.yaml b/internal/static/i18n/ko.yaml index 741f075ca2..c132d025a6 100644 --- a/internal/static/i18n/ko.yaml +++ b/internal/static/i18n/ko.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: 요청에 상태 매개변수가 누락되었습니다 NotStarted: 의도가 시작되지 않았거나 이미 종료되었습니다 NotSucceeded: 의도가 성공하지 않았습니다 + Expired: 의도의 유효 기간이 만료되었습니다 TokenCreationFailed: 토큰 생성 실패 InvalidToken: 의도 토큰이 유효하지 않습니다 OtherUser: 다른 사용자를 위한 의도입니다 diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index 4f320ca5c9..ae98f8e9d4 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: Параметарот State недостасува во барањето NotStarted: Намерата не е започната или веќе завршена NotSucceeded: Намерата не е успешна + Expired: Намерата е истечена TokenCreationFailed: Неуспешно креирање на токен InvalidToken: Токенот за намера е невалиден OtherUser: Намерата е за друг корисник diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 0b6c2eeb84..e4c3e0c49d 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: Staat parameter ontbreekt in het verzoek NotStarted: Intentie is niet gestart of was al beëindigd NotSucceeded: Intentie is niet geslaagd + Expired: Intentie is verlopen TokenCreationFailed: Token aanmaken mislukt InvalidToken: Intentie Token is ongeldig OtherUser: Intentie bedoeld voor een andere gebruiker diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 83645e525d..5d37df0056 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: Brak parametru stanu w żądaniu NotStarted: Intencja nie została rozpoczęta lub już się zakończyła NotSucceeded: intencja nie powiodła się + Expired: Intencja wygasła TokenCreationFailed: Tworzenie tokena nie powiodło się InvalidToken: Token intencji jest nieprawidłowy OtherUser: Intencja przeznaczona dla innego użytkownika diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index a30e1bedaf..1459d09325 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: O parâmetro de estado está faltando na solicitação NotStarted: A intenção não foi iniciada ou já foi encerrada NotSucceeded: A intenção não teve sucesso + Expired: A intenção expirou TokenCreationFailed: Falha na criação do token InvalidToken: O token da intenção é inválido OtherUser: Intenção destinada a outro usuário diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index f1eab4365e..63bfc39184 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -526,6 +526,7 @@ Errors: StateMissing: В запросе отсутствует параметр State NotStarted: Намерение не начато или уже прекращено NotSucceeded: Намерение не увенчалось успехом + Epired: Намерение истекло TokenCreationFailed: Не удалось создать токен InvalidToken: Маркер намерения недействителен OtherUser: Намерение, предназначенное для другого пользователя diff --git a/internal/static/i18n/sv.yaml b/internal/static/i18n/sv.yaml index e31095b78c..8a76372368 100644 --- a/internal/static/i18n/sv.yaml +++ b/internal/static/i18n/sv.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: State-parameter saknas i begäran NotStarted: Avsikten har inte startat eller har redan avslutats NotSucceeded: Avsikten har inte lyckats + Expired: Avsikten har gått ut TokenCreationFailed: Token-skapande misslyckades InvalidToken: Avsiktstoken är ogiltig OtherUser: Avsikten är avsedd för en annan användare diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 4c18ed458f..dfabaa303b 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: 请求中缺少状态参数 NotStarted: 意图没有开始或已经结束 NotSucceeded: 意图不成功 + Expired: 意图已过期 TokenCreationFailed: 令牌创建失败 InvalidToken: 意图令牌是无效的 OtherUser: 意图是为另一个用户准备的