diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 6ab01ab35b..0d71b4d817 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -735,6 +735,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 bf396fd25d..70e670bacc 100644 --- a/internal/api/grpc/user/v2/integration_test/user_test.go +++ b/internal/api/grpc/user/v2/integration_test/user_test.go @@ -2121,22 +2121,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { 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(), oauthIdpID, "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(), oauthIdpID, "id", "user") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + 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", "") + 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") + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2260,6 +2274,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, 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{ @@ -2469,7 +2505,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, @@ -2518,7 +2554,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, diff --git a/internal/api/grpc/user/v2/intent.go b/internal/api/grpc/user/v2/intent.go index 06966edb35..8043a9bdae 100644 --- a/internal/api/grpc/user/v2/intent.go +++ b/internal/api/grpc/user/v2/intent.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "time" oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/protobuf/types/known/structpb" @@ -71,14 +72,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 } @@ -116,7 +117,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 @@ -137,12 +138,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) { @@ -156,6 +152,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") + } idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg) if err != nil { return nil, err 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 a81de58761..a5a1309d1a 100644 --- a/internal/api/grpc/user/v2beta/integration_test/user_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/user_test.go @@ -2153,22 +2153,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { 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(), oauthIdpID, "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(), oauthIdpID, "id", "user") + + successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry) require.NoError(t, err) - oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "") + successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry) require.NoError(t, err) - oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user") + 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", "") + 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") + samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry) require.NoError(t, err) type args struct { ctx context.Context @@ -2281,6 +2295,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { }, 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{ @@ -2466,7 +2502,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, @@ -2504,7 +2540,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) { IdpInformation: &user.IDPInformation{ Access: &user.IDPInformation_Saml{ Saml: &user.IDPSAMLAccessInformation{ - Assertion: []byte(""), + Assertion: []byte(fmt.Sprintf(``, expiryFormatted)), }, }, IdpId: samlIdpID, diff --git a/internal/api/grpc/user/v2beta/user.go b/internal/api/grpc/user/v2beta/user.go index cf6dfa6304..93afbde0aa 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 @@ -470,7 +471,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string 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 +485,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 c3e9586a59..ebf904a395 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 b0e67ad52e..64b7b53b67 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -81,6 +81,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) @@ -152,6 +153,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 3cd9991679..9690117edd 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" ) @@ -68,7 +69,7 @@ func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL return nil, nil, err } } - writeModel := NewIDPIntentWriteModel(intentID, resourceOwner) + writeModel := NewIDPIntentWriteModel(intentID, resourceOwner, c.maxIdPIntentLifetime) //nolint: staticcheck cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments)) @@ -180,6 +181,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 +190,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 +199,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 +215,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 +240,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 +249,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 +261,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 +281,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 c6bc26ab06..07e0821813 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" @@ -29,18 +30,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) { @@ -56,6 +68,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() @@ -74,6 +88,7 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder { idpintent.SAMLRequestEventType, idpintent.LDAPSucceededEventType, idpintent.FailedEventType, + idpintent.ConsumedEventType, ). Builder() } @@ -93,6 +108,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) { @@ -102,6 +119,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) { @@ -112,6 +131,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) { @@ -122,6 +143,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 2400b9ee35..1be3971e87 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" @@ -867,7 +870,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"), @@ -888,7 +891,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{ @@ -922,6 +925,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) { Crypted: []byte("accessToken"), }, "idToken", + time.Time{}, ) return event }(), @@ -930,7 +934,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{ @@ -973,7 +977,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 { @@ -998,7 +1002,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"), @@ -1023,14 +1027,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{ @@ -1061,14 +1068,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{ @@ -1088,7 +1098,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) }) @@ -1128,7 +1138,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) { }, args{ ctx: context.Background(), - writeModel: NewIDPIntentWriteModel("id", "instance"), + writeModel: NewIDPIntentWriteModel("id", "instance", 0), request: "request", }, res{}, @@ -1156,7 +1166,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 @@ -1180,7 +1190,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"), @@ -1200,14 +1210,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", "", @@ -1235,7 +1255,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) }) @@ -1275,7 +1295,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 60027d3a05..e65f32fb57 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, @@ -842,13 +950,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), ), ), ), @@ -866,6 +975,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 eee68fa2a5..9395d84b2b 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 4b0a6fb844..169784fb58 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 @@ -79,6 +82,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 6df08a6998..5138812f3c 100644 --- a/internal/idp/providers/jwt/session.go +++ b/internal/idp/providers/jwt/session.go @@ -57,6 +57,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 0a6a87ba3d..1679e35b61 100644 --- a/internal/idp/providers/ldap/session.go +++ b/internal/idp/providers/ldap/session.go @@ -96,6 +96,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 247a7f8710..c9e175d1cf 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" @@ -69,6 +70,13 @@ func (s *Session) FetchUser(ctx context.Context) (_ idp.User, err error) { return user, 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 b17a3b0a0b..430a14e5bb 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" @@ -72,6 +73,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 b0748d33a3..e2a1655a26 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" @@ -107,6 +108,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 ab54bcabaa..fc593eb820 100644 --- a/internal/idp/session.go +++ b/internal/idp/session.go @@ -2,6 +2,7 @@ package idp import ( "context" + "time" ) // Session is the minimal implementation for a session of a 3rd party authentication [Provider] @@ -9,6 +10,7 @@ type Session interface { GetAuth(ctx context.Context) (content string, redirect bool) PersistentParameters() map[string]any 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 e82a6bec55..f1bcfb41bd 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -672,6 +672,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 633ebf424f..8abb31a63e 100644 --- a/internal/integration/sink/server.go +++ b/internal/integration/sink/server.go @@ -17,6 +17,7 @@ import ( 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" @@ -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,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil } -func SuccessfulOIDCIntent(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, @@ -77,6 +79,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, IDPID: idpID, IDPUserID: idpUserID, UserID: userID, + Expiry: expiry, }) if err != nil { return "", "", time.Time{}, uint64(0), err @@ -84,7 +87,7 @@ func SuccessfulOIDCIntent(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 SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) { u := url.URL{ Scheme: "http", Host: host, @@ -95,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 @@ -282,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"` @@ -376,6 +381,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", }, @@ -407,6 +413,7 @@ func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Token: &oauth2.Token{ AccessToken: "accessToken", + Expiry: req.Expiry, }, IDToken: "idToken", }, @@ -431,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 } @@ -465,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 27e6391f95..e4ee28cae9 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 { @@ -79,6 +81,7 @@ type SucceededEvent struct { IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"` IDPIDToken string `json:"idpIdToken,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSucceededEvent( @@ -90,6 +93,7 @@ func NewSucceededEvent( userID string, idpAccessToken *crypto.CryptoValue, idpIDToken string, + expiresAt time.Time, ) *SucceededEvent { return &SucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -103,6 +107,7 @@ func NewSucceededEvent( UserID: userID, IDPAccessToken: idpAccessToken, IDPIDToken: idpIDToken, + ExpiresAt: expiresAt, } } @@ -136,6 +141,7 @@ type SAMLSucceededEvent struct { UserID string `json:"userId,omitempty"` Assertion *crypto.CryptoValue `json:"assertion,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewSAMLSucceededEvent( @@ -146,6 +152,7 @@ func NewSAMLSucceededEvent( idpUserName, userID string, assertion *crypto.CryptoValue, + expiresAt time.Time, ) *SAMLSucceededEvent { return &SAMLSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -158,6 +165,7 @@ func NewSAMLSucceededEvent( IDPUserName: idpUserName, UserID: userID, Assertion: assertion, + ExpiresAt: expiresAt, } } @@ -233,6 +241,7 @@ type LDAPSucceededEvent struct { UserID string `json:"userId,omitempty"` EntryAttributes map[string][]string `json:"user,omitempty"` + ExpiresAt time.Time `json:"expiresAt,omitempty"` } func NewLDAPSucceededEvent( @@ -243,6 +252,7 @@ func NewLDAPSucceededEvent( idpUserName, userID string, attributes map[string][]string, + expiresAt time.Time, ) *LDAPSucceededEvent { return &LDAPSucceededEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -255,6 +265,7 @@ func NewLDAPSucceededEvent( IDPUserName: idpUserName, UserID: userID, EntryAttributes: attributes, + ExpiresAt: expiresAt, } } @@ -320,3 +331,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 d7dc18898b..8254b82b45 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -554,6 +554,7 @@ Errors: StateMissing: В заявката липсва параметър състояние NotStarted: Намерението не е стартирано или вече е прекратено NotSucceeded: Намерението не е успешно + Expired: Намерението е изтекло TokenCreationFailed: Неуспешно създаване на токен InvalidToken: Знакът за намерение е невалиден OtherUser: Намерение, предназначено за друг потребител diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index 80db4952f9..bb4172fbff 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -534,6 +534,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 dcb3ac5c71..a24ce7c933 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -536,6 +536,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 bd8d26d727..e8f2781de1 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -537,6 +537,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 9f11b63964..b91d055f70 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -536,6 +536,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 ff8393befc..98f2bee9a0 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -536,6 +536,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 b17c6a1225..5becd6e606 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 56a454e71d..0108d7618b 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 6713abf2e1..750c48471a 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -536,6 +536,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 f57d0f6661..fcd7920999 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -537,6 +537,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 d238142e01..d83af62235 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 898ed67360..7126925279 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -535,6 +535,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 882c58a4f2..a398e4b770 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -536,6 +536,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 13125bc2a9..049a189930 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -536,6 +536,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 4ab3573c2b..09a5fc02c5 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -535,6 +535,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/ro.yaml b/internal/static/i18n/ro.yaml index 48790da9e5..9010e57032 100644 --- a/internal/static/i18n/ro.yaml +++ b/internal/static/i18n/ro.yaml @@ -537,6 +537,7 @@ Errors: StateMissing: Parametrul de stare lipsește în cerere NotStarted: Intenția nu este pornită sau a fost deja terminată NotSucceeded: Intenția nu a reușit + Expired: Intenția a expirat TokenCreationFailed: Crearea token-ului a eșuat InvalidToken: Token-ul intenției este invalid OtherUser: Intenția este destinată altui utilizator diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 64a8ef8013..38b2847637 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -525,6 +525,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 2c292976d3..ed4b863886 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 d4b36df7ff..03aa168a50 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -536,6 +536,7 @@ Errors: StateMissing: 请求中缺少状态参数 NotStarted: 意图没有开始或已经结束 NotSucceeded: 意图不成功 + Expired: 意图已过期 TokenCreationFailed: 令牌创建失败 InvalidToken: 意图令牌是无效的 OtherUser: 意图是为另一个用户准备的