VYPR
High severityNVD Advisory· Published May 6, 2025· Updated May 6, 2025

ZITADEL Allows IdP Intent Token Reuse

CVE-2025-46815

Description

The identity infrastructure software ZITADEL offers developers the ability to manage user sessions using the Session API. This API enables the use of IdPs for authentication, known as idp intents. Following a successful idp intent, the client receives an id and token on a predefined URI. These id and token can then be used to authenticate the user or their session. However, prior to versions 3.0.0, 2.71.9, and 2.70.10, it was possible to exploit this feature by repeatedly using intents. This allowed an attacker with access to the application’s URI to retrieve the id and token, enabling them to authenticate on behalf of the user. It's important to note that the use of additional factors (MFA) prevents a complete authentication process and, consequently, access to the ZITADEL API. Versions 3.0.0, 2.71.9, and 2.70.10 contain a fix for the issue. No known workarounds other than upgrading are available.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/zitadel/zitadelGo
>= 3.0.0-rc.1, < 3.0.03.0.0
github.com/zitadel/zitadelGo
< 2.70.102.70.10
github.com/zitadel/zitadelGo
>= 2.71.0, < 2.71.92.71.9

Affected products

1

Patches

1
b1e60e7398d6

Merge commit from fork

https://github.com/zitadel/zitadelLivio SpringMay 2, 2025via ghsa
48 files changed · +673 123
  • cmd/defaults.yaml+3 0 modified
    @@ -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:
    
  • internal/api/grpc/session/v2beta/integration_test/session_test.go+77 3 modified
    @@ -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,
    
  • internal/api/grpc/session/v2/integration_test/session_test.go+77 3 modified
    @@ -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,
    
  • internal/api/grpc/user/v2beta/integration_test/user_test.go+44 8 modified
    @@ -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)
    +	successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry)
    +	require.NoError(t, err)
    +	successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second))
     	require.NoError(t, err)
    -	oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
    +	// make sure the intent is expired
    +	time.Sleep(2 * time.Second)
    +	successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry)
     	require.NoError(t, err)
    -	oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
    +	// 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 xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
    +							Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, 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 xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
    +							Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
     						},
     					},
     					IdpId:    samlIdpID,
    
  • internal/api/grpc/user/v2beta/user.go+8 4 modified
    @@ -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)
     }
     
    
  • internal/api/grpc/user/v2/integration_test/user_test.go+44 8 modified
    @@ -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)
    +	successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry)
    +	require.NoError(t, err)
    +	successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second))
     	require.NoError(t, err)
    -	oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
    +	// make sure the intent is expired
    +	time.Sleep(2 * time.Second)
    +	successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry)
     	require.NoError(t, err)
    -	oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
    +	// 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 xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
    +							Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, 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 xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
    +							Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
     						},
     					},
     					IdpId:    samlIdpID,
    
  • internal/api/grpc/user/v2/intent.go+8 9 modified
    @@ -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
    
  • internal/api/idp/idp.go+1 1 modified
    @@ -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
    
  • internal/api/idp/idp_test.go+21 17 modified
    @@ -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)
     
    
  • internal/command/command.go+2 0 modified
    @@ -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,
    
  • internal/command/idp_intent.go+14 6 modified
    @@ -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
    
  • internal/command/idp_intent_model.go+27 2 modified
    @@ -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,
    
  • internal/command/idp_intent_test.go+38 18 modified
    @@ -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("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
     							},
    +							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("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
     							},
    +							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{
    
  • internal/command/session.go+29 22 modified
    @@ -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,14 +95,17 @@ 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
     		}
     		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) {
    
  • internal/command/session_test.go+112 2 modified
    @@ -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"),
     					),
    
  • internal/config/systemdefaults/system_defaults.go+10 9 modified
    @@ -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 {
    
  • internal/domain/idp.go+1 0 modified
    @@ -115,6 +115,7 @@ const (
     	IDPIntentStateStarted
     	IDPIntentStateSucceeded
     	IDPIntentStateFailed
    +	IDPIntentStateConsumed
     
     	idpIntentStateCount
     )
    
  • internal/idp/providers/apple/session.go+2 0 modified
    @@ -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 {
    
  • internal/idp/providers/azuread/session.go+10 0 modified
    @@ -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
    
  • internal/idp/providers/jwt/session.go+7 0 modified
    @@ -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
    
  • internal/idp/providers/ldap/session.go+4 0 modified
    @@ -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,
    
  • internal/idp/providers/oauth/session.go+8 0 modified
    @@ -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
    
  • internal/idp/providers/oidc/session.go+8 0 modified
    @@ -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
    
  • internal/idp/providers/saml/session.go+8 0 modified
    @@ -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 {
    
  • internal/idp/session.go+2 0 modified
    @@ -2,13 +2,15 @@ package idp
     
     import (
     	"context"
    +	"time"
     )
     
     // Session is the minimal implementation for a session of a 3rd party authentication [Provider]
     type Session interface {
     	GetAuth(ctx context.Context) (content string, redirect bool)
     	PersistentParameters() map[string]any
     	FetchUser(ctx context.Context) (User, error)
    +	ExpiresAt() time.Time
     }
     
     // SessionSupportsMigration is an optional extension to the Session interface.
    
  • internal/integration/client.go+17 0 modified
    @@ -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,
    
  • internal/integration/sink/server.go+31 11 modified
    @@ -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,14 +60,15 @@ 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
     	}
     	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,14 +79,15 @@ 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
     	}
     	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
     	}
    
  • internal/repository/idpintent/eventstore.go+1 0 modified
    @@ -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])
     }
    
  • internal/repository/idpintent/intent.go+40 0 modified
    @@ -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
    +}
    
  • internal/static/i18n/bg.yaml+1 0 modified
    @@ -554,6 +554,7 @@ Errors:
         StateMissing: В заявката липсва параметър състояние
         NotStarted: Намерението не е стартирано или вече е прекратено
         NotSucceeded: Намерението не е успешно
    +    Expired: Намерението е изтекло
         TokenCreationFailed: Неуспешно създаване на токен
         InvalidToken: Знакът за намерение е невалиден
         OtherUser: Намерение, предназначено за друг потребител
    
  • internal/static/i18n/cs.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/de.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/en.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/es.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/fr.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/hu.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/id.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/it.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/ja.yaml+1 0 modified
    @@ -537,6 +537,7 @@ Errors:
         StateMissing: リクエストに State パラメータがありません
         NotStarted: インテントが開始されなかったか、既に終了している
         NotSucceeded: インテントが成功しなかった
    +    Expired: 意図の有効期限が切れました
         TokenCreationFailed: トークンの作成に失敗しました
         InvalidToken: インテントのトークンが無効である
         OtherUser: 他のユーザーを意図している
    
  • internal/static/i18n/ko.yaml+1 0 modified
    @@ -537,6 +537,7 @@ Errors:
         StateMissing: 요청에 상태 매개변수가 누락되었습니다
         NotStarted: 의도가 시작되지 않았거나 이미 종료되었습니다
         NotSucceeded: 의도가 성공하지 않았습니다
    +    Expired: 의도의 유효 기간이 만료되었습니다
         TokenCreationFailed: 토큰 생성 실패
         InvalidToken: 의도 토큰이 유효하지 않습니다
         OtherUser: 다른 사용자를 위한 의도입니다
    
  • internal/static/i18n/mk.yaml+1 0 modified
    @@ -535,6 +535,7 @@ Errors:
         StateMissing: Параметарот State недостасува во барањето
         NotStarted: Намерата не е започната или веќе завршена
         NotSucceeded: Намерата не е успешна
    +    Expired: Намерата е истечена
         TokenCreationFailed: Неуспешно креирање на токен
         InvalidToken: Токенот за намера е невалиден
         OtherUser: Намерата е за друг корисник
    
  • internal/static/i18n/nl.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/pl.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/pt.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/ro.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/ru.yaml+1 0 modified
    @@ -525,6 +525,7 @@ Errors:
         StateMissing: В запросе отсутствует параметр State
         NotStarted: Намерение не начато или уже прекращено
         NotSucceeded: Намерение не увенчалось успехом
    +    Epired: Намерение истекло
         TokenCreationFailed: Не удалось создать токен
         InvalidToken: Маркер намерения недействителен
         OtherUser: Намерение, предназначенное для другого пользователя
    
  • internal/static/i18n/sv.yaml+1 0 modified
    @@ -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
    
  • internal/static/i18n/zh.yaml+1 0 modified
    @@ -536,6 +536,7 @@ Errors:
         StateMissing: 请求中缺少状态参数
         NotStarted: 意图没有开始或已经结束
         NotSucceeded: 意图不成功
    +    Expired: 意图已过期
         TokenCreationFailed: 令牌创建失败
         InvalidToken: 意图令牌是无效的
         OtherUser: 意图是为另一个用户准备的
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.