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: 意图是为另一个用户准备的