Merge commit from fork

* fix: prevent intent token reuse and add expiry

* fix duplicate

* fix expiration
This commit is contained in:
Livio Spring 2025-05-02 13:44:24 +02:00 committed by GitHub
parent bb56b362a7
commit b1e60e7398
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 673 additions and 123 deletions

View File

@ -735,6 +735,9 @@ SystemDefaults:
DefaultQueryLimit: 100 # ZITADEL_SYSTEMDEFAULTS_DEFAULTQUERYLIMIT 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 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 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: Actions:
HTTP: HTTP:

View File

@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) 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) require.NoError(t, err)
updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{ updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(), SessionId: createResp.GetSessionId(),
@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() 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) require.NoError(t, err)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{ Checks: &session.Checks{
@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
// successful intent without known / linked user // successful intent without known / linked user
idpUserID := "id" 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) // link the user (with info from intent)
Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId()) Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
@ -447,6 +447,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
require.Error(t, err) 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) { func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID, UserId: userID,

View File

@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId()) 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) require.NoError(t, err)
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(), SessionId: createResp.GetSessionId(),
@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) { func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId() 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) require.NoError(t, err)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{ createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{ Checks: &session.Checks{
@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
// successful intent without known / linked user // successful intent without known / linked user
idpUserID := "id" 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) require.NoError(t, err)
// link the user (with info from intent) // link the user (with info from intent)
@ -448,6 +448,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
require.Error(t, err) 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) { func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{ resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID, UserId: userID,

View File

@ -2121,22 +2121,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err) require.NoError(t, err)
intentID := authURL.Query().Get("state") 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) 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) 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) 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) require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err) require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err) 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) 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) require.NoError(t, err)
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -2260,6 +2274,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
}, },
wantErr: false, 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", name: "retrieve successful oidc intent",
args: args{ args: args{
@ -2469,7 +2505,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{ IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{ Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{ 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, IdpId: samlIdpID,
@ -2518,7 +2554,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{ IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{ Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{ 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, IdpId: samlIdpID,

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"time"
oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc" oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
@ -71,14 +72,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti
if err != nil { if err != nil {
return nil, err 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 != nil {
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
return nil, err return nil, err
} }
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 { if err != nil {
return nil, err return nil, err
} }
@ -116,7 +117,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse
return "", nil 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, "", "") provider, err := s.command.GetProvider(ctx, idpID, "", "")
if err != nil { if err != nil {
return nil, "", nil, err return nil, "", nil, err
@ -137,12 +138,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string
if err != nil { if err != nil {
return nil, "", nil, err return nil, "", nil, err
} }
return externalUser, userID, session, nil
attributes := make(map[string][]string, 0)
for _, item := range session.Entry.Attributes {
attributes[item.Name] = item.Values
}
return externalUser, userID, attributes, nil
} }
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) { 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 { if intent.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") 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) idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -2153,22 +2153,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl()) authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err) require.NoError(t, err)
intentID := authURL.Query().Get("state") 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) 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) 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) 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) require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "") ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err) require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user") ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err) 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) 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) require.NoError(t, err)
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -2281,6 +2295,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
}, },
wantErr: false, 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", name: "retrieve successful oidc intent",
args: args{ args: args{
@ -2466,7 +2502,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{ IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{ Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{ 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, IdpId: samlIdpID,
@ -2504,7 +2540,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{ IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{ Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{ 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, IdpId: samlIdpID,

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"io" "io"
"time"
"golang.org/x/text/language" "golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
@ -399,14 +400,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti
if err != nil { if err != nil {
return nil, err 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 != nil {
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil { if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
return nil, err return nil, err
} }
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 { if err != nil {
return nil, err return nil, err
} }
@ -444,7 +445,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse
return "", nil 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, "", "") provider, err := s.command.GetProvider(ctx, idpID, "", "")
if err != nil { if err != nil {
return nil, "", nil, err 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 { for _, item := range session.Entry.Attributes {
attributes[item.Name] = item.Values 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) { 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 { if intent.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded") 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) return idpIntentToIDPIntentPb(intent, s.idpAlg)
} }

View File

@ -287,7 +287,7 @@ func (h *Handler) handleACS(w http.ResponseWriter, r *http.Request) {
userID, err := h.checkExternalUser(ctx, intent.IDPID, idpUser.GetID()) 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") 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 { if err != nil {
redirectToFailureURLErr(w, r, intent, zerrors.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed")) redirectToFailureURLErr(w, r, intent, zerrors.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed"))
return return

View File

@ -4,6 +4,7 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -19,6 +20,7 @@ func Test_redirectToSuccessURL(t *testing.T) {
token string token string
failureURL string failureURL string
successURL string successURL string
maxIdPIntentLifetime time.Duration
} }
type res struct { type res struct {
want string want string
@ -59,7 +61,7 @@ func Test_redirectToSuccessURL(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil) req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder() 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.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL)
@ -76,6 +78,7 @@ func Test_redirectToFailureURL(t *testing.T) {
successURL string successURL string
err string err string
desc string desc string
maxIdPIntentLifetime time.Duration
} }
type res struct { type res struct {
want string want string
@ -115,7 +118,7 @@ func Test_redirectToFailureURL(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil) req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder() 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.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL)
@ -131,6 +134,7 @@ func Test_redirectToFailureURLErr(t *testing.T) {
failureURL string failureURL string
successURL string successURL string
err error err error
maxIdPIntentLifetime time.Duration
} }
type res struct { type res struct {
want string want string
@ -158,7 +162,7 @@ func Test_redirectToFailureURLErr(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil) req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder() 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.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL) wm.SuccessURL, _ = url.Parse(tt.args.successURL)

View File

@ -81,6 +81,7 @@ type Commands struct {
publicKeyLifetime time.Duration publicKeyLifetime time.Duration
certificateLifetime time.Duration certificateLifetime time.Duration
defaultSecretGenerators *SecretGenerators defaultSecretGenerators *SecretGenerators
maxIdPIntentLifetime time.Duration
samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error) 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) 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, privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime, publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
certificateLifetime: defaults.KeyConfig.CertificateLifetime, certificateLifetime: defaults.KeyConfig.CertificateLifetime,
maxIdPIntentLifetime: defaults.MaxIdPIntentLifetime,
idpConfigEncryption: idpConfigEncryption, idpConfigEncryption: idpConfigEncryption,
smtpEncryption: smtpEncryption, smtpEncryption: smtpEncryption,
smsEncryption: smsEncryption, smsEncryption: smsEncryption,

View File

@ -7,7 +7,6 @@ import (
"encoding/xml" "encoding/xml"
"net/url" "net/url"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp" "github.com/crewjam/saml/samlsp"
"github.com/zitadel/oidc/v3/pkg/oidc" "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/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread" "github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/jwt" "github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth" "github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" 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/repository/idpintent"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
) )
@ -68,7 +69,7 @@ func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL
return nil, nil, err return nil, nil, err
} }
} }
writeModel := NewIDPIntentWriteModel(intentID, resourceOwner) writeModel := NewIDPIntentWriteModel(intentID, resourceOwner, c.maxIdPIntentLifetime)
//nolint: staticcheck //nolint: staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments)) 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, userID,
accessToken, accessToken,
idToken, idToken,
idpSession.ExpiresAt(),
) )
err = c.pushAppendAndReduce(ctx, writeModel, cmd) err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil { if err != nil {
@ -188,7 +190,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
return token, nil 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) token, err := c.generateIntentToken(writeModel.AggregateID)
if err != nil { if err != nil {
return "", err return "", err
@ -197,7 +199,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte
if err != nil { if err != nil {
return "", err return "", err
} }
assertionData, err := xml.Marshal(assertion) assertionData, err := xml.Marshal(session.Assertion)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -213,6 +215,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte
idpUser.GetPreferredUsername(), idpUser.GetPreferredUsername(),
userID, userID,
assertionEnc, assertionEnc,
session.ExpiresAt(),
) )
err = c.pushAppendAndReduce(ctx, writeModel, cmd) err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil { if err != nil {
@ -237,7 +240,7 @@ func (c *Commands) generateIntentToken(intentID string) (string, error) {
return base64.RawURLEncoding.EncodeToString(token), nil 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) token, err := c.generateIntentToken(writeModel.AggregateID)
if err != nil { if err != nil {
return "", err return "", err
@ -246,6 +249,10 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte
if err != nil { if err != nil {
return "", err 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( cmd := idpintent.NewLDAPSucceededEvent(
ctx, ctx,
IDPIntentAggregateFromWriteModel(&writeModel.WriteModel), IDPIntentAggregateFromWriteModel(&writeModel.WriteModel),
@ -254,6 +261,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte
idpUser.GetPreferredUsername(), idpUser.GetPreferredUsername(),
userID, userID,
attributes, attributes,
session.ExpiresAt(),
) )
err = c.pushAppendAndReduce(ctx, writeModel, cmd) err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil { 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) { 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) err := c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -2,6 +2,7 @@ package command
import ( import (
"net/url" "net/url"
"time"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
@ -30,17 +31,28 @@ type IDPIntentWriteModel struct {
Assertion *crypto.CryptoValue 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{ return &IDPIntentWriteModel{
WriteModel: eventstore.WriteModel{ WriteModel: eventstore.WriteModel{
AggregateID: id, AggregateID: id,
ResourceOwner: resourceOwner, 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 { func (wm *IDPIntentWriteModel) Reduce() error {
for _, event := range wm.Events { for _, event := range wm.Events {
switch e := event.(type) { switch e := event.(type) {
@ -56,6 +68,8 @@ func (wm *IDPIntentWriteModel) Reduce() error {
wm.reduceLDAPSucceededEvent(e) wm.reduceLDAPSucceededEvent(e)
case *idpintent.FailedEvent: case *idpintent.FailedEvent:
wm.reduceFailedEvent(e) wm.reduceFailedEvent(e)
case *idpintent.ConsumedEvent:
wm.reduceConsumedEvent(e)
} }
} }
return wm.WriteModel.Reduce() return wm.WriteModel.Reduce()
@ -74,6 +88,7 @@ func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder {
idpintent.SAMLRequestEventType, idpintent.SAMLRequestEventType,
idpintent.LDAPSucceededEventType, idpintent.LDAPSucceededEventType,
idpintent.FailedEventType, idpintent.FailedEventType,
idpintent.ConsumedEventType,
). ).
Builder() Builder()
} }
@ -93,6 +108,8 @@ func (wm *IDPIntentWriteModel) reduceSAMLSucceededEvent(e *idpintent.SAMLSucceed
wm.IDPUserName = e.IDPUserName wm.IDPUserName = e.IDPUserName
wm.Assertion = e.Assertion wm.Assertion = e.Assertion
wm.State = domain.IDPIntentStateSucceeded wm.State = domain.IDPIntentStateSucceeded
wm.succeededAt = e.CreationDate()
wm.expiresAt = e.ExpiresAt
} }
func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) { func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceededEvent) {
@ -102,6 +119,8 @@ func (wm *IDPIntentWriteModel) reduceLDAPSucceededEvent(e *idpintent.LDAPSucceed
wm.IDPUserName = e.IDPUserName wm.IDPUserName = e.IDPUserName
wm.IDPEntryAttributes = e.EntryAttributes wm.IDPEntryAttributes = e.EntryAttributes
wm.State = domain.IDPIntentStateSucceeded wm.State = domain.IDPIntentStateSucceeded
wm.succeededAt = e.CreationDate()
wm.expiresAt = e.ExpiresAt
} }
func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededEvent) { func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededEvent) {
@ -112,6 +131,8 @@ func (wm *IDPIntentWriteModel) reduceOAuthSucceededEvent(e *idpintent.SucceededE
wm.IDPAccessToken = e.IDPAccessToken wm.IDPAccessToken = e.IDPAccessToken
wm.IDPIDToken = e.IDPIDToken wm.IDPIDToken = e.IDPIDToken
wm.State = domain.IDPIntentStateSucceeded wm.State = domain.IDPIntentStateSucceeded
wm.succeededAt = e.CreationDate()
wm.expiresAt = e.ExpiresAt
} }
func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEvent) { func (wm *IDPIntentWriteModel) reduceSAMLRequestEvent(e *idpintent.SAMLRequestEvent) {
@ -122,6 +143,10 @@ func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) {
wm.State = domain.IDPIntentStateFailed wm.State = domain.IDPIntentStateFailed
} }
func (wm *IDPIntentWriteModel) reduceConsumedEvent(e *idpintent.ConsumedEvent) {
wm.State = domain.IDPIntentStateConsumed
}
func IDPIntentAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate { func IDPIntentAggregateFromWriteModel(wm *eventstore.WriteModel) *eventstore.Aggregate {
return &eventstore.Aggregate{ return &eventstore.Aggregate{
Type: idpintent.AggregateType, Type: idpintent.AggregateType,

View File

@ -4,8 +4,10 @@ import (
"context" "context"
"net/url" "net/url"
"testing" "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/muhlemmer/gu"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth" "github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" 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" rep_idp "github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpintent" "github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/instance" "github.com/zitadel/zitadel/internal/repository/instance"
@ -867,7 +870,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"), writeModel: NewIDPIntentWriteModel("id", "ro", 0),
}, },
res{ res{
err: zerrors.ThrowInternal(nil, "id", "encryption failed"), err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
@ -888,7 +891,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"), writeModel: NewIDPIntentWriteModel("id", "ro", 0),
idpSession: &oauth.Session{ idpSession: &oauth.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{ Token: &oauth2.Token{
@ -922,6 +925,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
Crypted: []byte("accessToken"), Crypted: []byte("accessToken"),
}, },
"idToken", "idToken",
time.Time{},
) )
return event return event
}(), }(),
@ -930,7 +934,7 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "instance"), writeModel: NewIDPIntentWriteModel("id", "instance", 0),
idpSession: &openid.Session{ idpSession: &openid.Session{
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{ Token: &oauth2.Token{
@ -973,7 +977,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
ctx context.Context ctx context.Context
writeModel *IDPIntentWriteModel writeModel *IDPIntentWriteModel
idpUser idp.User idpUser idp.User
assertion *saml.Assertion session *saml.Session
userID string userID string
} }
type res struct { type res struct {
@ -998,7 +1002,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "ro"), writeModel: NewIDPIntentWriteModel("id", "ro", 0),
}, },
res{ res{
err: zerrors.ThrowInternal(nil, "id", "encryption failed"), err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
@ -1023,14 +1027,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
KeyID: "id", 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>"), 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{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "instance"), writeModel: NewIDPIntentWriteModel("id", "instance", 0),
assertion: &saml.Assertion{ID: "id"}, session: &saml.Session{
Assertion: &crewjam_saml.Assertion{ID: "id"},
},
idpUser: openid.NewUser(&oidc.UserInfo{ idpUser: openid.NewUser(&oidc.UserInfo{
Subject: "id", Subject: "id",
UserInfoProfile: oidc.UserInfoProfile{ UserInfoProfile: oidc.UserInfoProfile{
@ -1061,14 +1068,17 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
KeyID: "id", 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>"), 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{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "instance"), writeModel: NewIDPIntentWriteModel("id", "instance", 0),
assertion: &saml.Assertion{ID: "id"}, session: &saml.Session{
Assertion: &crewjam_saml.Assertion{ID: "id"},
},
idpUser: openid.NewUser(&oidc.UserInfo{ idpUser: openid.NewUser(&oidc.UserInfo{
Subject: "id", Subject: "id",
UserInfoProfile: oidc.UserInfoProfile{ UserInfoProfile: oidc.UserInfoProfile{
@ -1088,7 +1098,7 @@ func TestCommands_SucceedSAMLIDPIntent(t *testing.T) {
eventstore: tt.fields.eventstore(t), eventstore: tt.fields.eventstore(t),
idpConfigEncryption: tt.fields.idpConfigEncryption, 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) require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.token, got) assert.Equal(t, tt.res.token, got)
}) })
@ -1128,7 +1138,7 @@ func TestCommands_RequestSAMLIDPIntent(t *testing.T) {
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "instance"), writeModel: NewIDPIntentWriteModel("id", "instance", 0),
request: "request", request: "request",
}, },
res{}, res{},
@ -1156,7 +1166,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
writeModel *IDPIntentWriteModel writeModel *IDPIntentWriteModel
idpUser idp.User idpUser idp.User
userID string userID string
attributes map[string][]string session *ldap.Session
} }
type res struct { type res struct {
token string token string
@ -1180,7 +1190,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "instance"), writeModel: NewIDPIntentWriteModel("id", "instance", 0),
}, },
res{ res{
err: zerrors.ThrowInternal(nil, "id", "encryption failed"), err: zerrors.ThrowInternal(nil, "id", "encryption failed"),
@ -1200,14 +1210,24 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
"username", "username",
"", "",
map[string][]string{"id": {"id"}}, map[string][]string{"id": {"id"}},
time.Time{},
), ),
), ),
), ),
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "instance"), writeModel: NewIDPIntentWriteModel("id", "instance", 0),
attributes: map[string][]string{"id": {"id"}}, session: &ldap.Session{
Entry: &goldap.Entry{
Attributes: []*goldap.EntryAttribute{
{
Name: "id",
Values: []string{"id"},
},
},
},
},
idpUser: ldap.NewUser( idpUser: ldap.NewUser(
"id", "id",
"", "",
@ -1235,7 +1255,7 @@ func TestCommands_SucceedLDAPIDPIntent(t *testing.T) {
eventstore: tt.fields.eventstore(t), eventstore: tt.fields.eventstore(t),
idpConfigEncryption: tt.fields.idpConfigEncryption, 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) require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.token, got) assert.Equal(t, tt.res.token, got)
}) })
@ -1275,7 +1295,7 @@ func TestCommands_FailIDPIntent(t *testing.T) {
}, },
args{ args{
ctx: context.Background(), ctx: context.Background(),
writeModel: NewIDPIntentWriteModel("id", "instance"), writeModel: NewIDPIntentWriteModel("id", "instance", 0),
reason: "reason", reason: "reason",
}, },
res{ res{

View File

@ -17,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/notification/senders" "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/session"
"github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors" "github.com/zitadel/zitadel/internal/zerrors"
@ -41,6 +42,7 @@ type SessionCommands struct {
createToken func(sessionID string) (id string, token string, err error) createToken func(sessionID string) (id string, token string, err error)
getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error) getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
now func() time.Time now func() time.Time
maxIdPIntentLifetime time.Duration
} }
func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands { func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands {
@ -57,6 +59,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
createToken: c.sessionTokenCreator, createToken: c.sessionTokenCreator,
getCodeVerifier: c.phoneCodeVerifierFromConfig, getCodeVerifier: c.phoneCodeVerifierFromConfig,
now: time.Now, 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 { if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil {
return nil, err return nil, err
} }
cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "") cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "", cmd.maxIdPIntentLifetime)
err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel) err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel)
if err != nil { if err != nil {
return nil, err return nil, err
@ -100,6 +103,9 @@ func CheckIntent(intentID, token string) SessionCommand {
if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded { if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded") 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 != "" {
if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID { if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser") 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) { 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, 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) { func (s *SessionCommands) WebAuthNChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement, rpid string) {

View File

@ -695,6 +695,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID2", "userID2",
nil, 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"), 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", "set user, intent, metadata and token",
fields{ fields{
@ -768,13 +874,14 @@ func TestCommands_updateSession(t *testing.T) {
), ),
eventFromEventPusher( eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(), idpintent.NewSucceededEvent(context.Background(),
&idpintent.NewAggregate("id", "instance1").Aggregate, &idpintent.NewAggregate("intent", "instance1").Aggregate,
nil, nil,
"idpUserID", "idpUserID",
"idpUsername", "idpUsername",
"userID", "userID",
nil, nil,
"", "",
time.Now().Add(time.Hour),
), ),
), ),
), ),
@ -783,6 +890,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID", "org1", testNow, &language.Afrikaans), "userID", "org1", testNow, &language.Afrikaans),
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow), testNow),
idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate),
session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
map[string][]byte{"key": []byte("value")}), map[string][]byte{"key": []byte("value")}),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
@ -842,13 +950,14 @@ func TestCommands_updateSession(t *testing.T) {
), ),
eventFromEventPusher( eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(), idpintent.NewSucceededEvent(context.Background(),
&idpintent.NewAggregate("id", "instance1").Aggregate, &idpintent.NewAggregate("intent", "instance1").Aggregate,
nil, nil,
"idpUserID", "idpUserID",
"idpUsername", "idpUsername",
"", "",
nil, nil,
"", "",
time.Now().Add(time.Hour),
), ),
), ),
), ),
@ -866,6 +975,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID", "org1", testNow, &language.Afrikaans), "userID", "org1", testNow, &language.Afrikaans),
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow), testNow),
idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate, session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
"tokenID"), "tokenID"),
), ),

View File

@ -16,6 +16,7 @@ type SystemDefaults struct {
KeyConfig KeyConfig KeyConfig KeyConfig
DefaultQueryLimit uint64 DefaultQueryLimit uint64
MaxQueryLimit uint64 MaxQueryLimit uint64
MaxIdPIntentLifetime time.Duration
} }
type SecretGenerators struct { type SecretGenerators struct {

View File

@ -115,6 +115,7 @@ const (
IDPIntentStateStarted IDPIntentStateStarted
IDPIntentStateSucceeded IDPIntentStateSucceeded
IDPIntentStateFailed IDPIntentStateFailed
IDPIntentStateConsumed
idpIntentStateCount idpIntentStateCount
) )

View File

@ -10,6 +10,8 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/oidc" "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. // 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 // This enables to parse the user (name and email), which Apple only returns as form params on registration
type Session struct { type Session struct {

View File

@ -3,6 +3,7 @@ package azuread
import ( import (
"context" "context"
"net/http" "net/http"
"time"
"github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v3/pkg/http"
@ -12,6 +13,8 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/oauth" "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 // Session extends the [oauth.Session] to be able to handle the id_token and to implement the [idp.SessionSupportsMigration] functionality
type Session struct { type Session struct {
*Provider *Provider
@ -79,6 +82,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return user, nil 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]. // Tokens returns the [oidc.Tokens] of the underlying [oauth.Session].
func (s *Session) Tokens() *oidc.Tokens[*oidc.IDTokenClaims] { func (s *Session) Tokens() *oidc.Tokens[*oidc.IDTokenClaims] {
return s.oauth().Tokens return s.oauth().Tokens

View File

@ -57,6 +57,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return &User{s.Tokens.IDTokenClaims}, nil 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) { func (s *Session) validateToken(ctx context.Context, token string) (*oidc.IDTokenClaims, error) {
logging.Debug("begin token validation") logging.Debug("begin token validation")
// TODO: be able to specify them in the template: https://github.com/zitadel/zitadel/issues/5322 // TODO: be able to specify them in the template: https://github.com/zitadel/zitadel/issues/5322

View File

@ -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( func tryBind(
server string, server string,
startTLS bool, startTLS bool,

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"net/http" "net/http"
"time"
"github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http" 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 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) { func (s *Session) authorize(ctx context.Context) (err error) {
if s.Code == "" { if s.Code == "" {
return ErrCodeMissing return ErrCodeMissing

View File

@ -3,6 +3,7 @@ package oidc
import ( import (
"context" "context"
"errors" "errors"
"time"
"github.com/zitadel/oidc/v3/pkg/client/rp" "github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc" "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 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) { func (s *Session) Authorize(ctx context.Context) (err error) {
if s.Code == "" { if s.Code == "" {
return ErrCodeMissing return ErrCodeMissing

View File

@ -6,6 +6,7 @@ import (
"errors" "errors"
"net/http" "net/http"
"net/url" "net/url"
"time"
"github.com/crewjam/saml" "github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp" "github.com/crewjam/saml/samlsp"
@ -107,6 +108,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return userMapper, nil 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) { func (s *Session) transientMappingID() (string, error) {
for _, statement := range s.Assertion.AttributeStatements { for _, statement := range s.Assertion.AttributeStatements {
for _, attribute := range statement.Attributes { for _, attribute := range statement.Attributes {

View File

@ -2,6 +2,7 @@ package idp
import ( import (
"context" "context"
"time"
) )
// Session is the minimal implementation for a session of a 3rd party authentication [Provider] // 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) GetAuth(ctx context.Context) (content string, redirect bool)
PersistentParameters() map[string]any PersistentParameters() map[string]any
FetchUser(ctx context.Context) (User, error) FetchUser(ctx context.Context) (User, error)
ExpiresAt() time.Time
} }
// SessionSupportsMigration is an optional extension to the Session interface. // SessionSupportsMigration is an optional extension to the Session interface.

View File

@ -672,6 +672,23 @@ func (i *Instance) CreatePasswordSession(t *testing.T, ctx context.Context, user
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime() 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 { func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse {
resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{ resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{
GrantedOrgId: grantedOrgID, GrantedOrgId: grantedOrgID,

View File

@ -17,6 +17,7 @@ import (
crewjam_saml "github.com/crewjam/saml" crewjam_saml "github.com/crewjam/saml"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
goldap "github.com/go-ldap/ldap/v3"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/zitadel/logging" "github.com/zitadel/logging"
@ -48,7 +49,7 @@ func CallURL(ch Channel) string {
return u.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{ u := url.URL{
Scheme: "http", Scheme: "http",
Host: host, Host: host,
@ -59,6 +60,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID, IDPID: idpID,
IDPUserID: idpUserID, IDPUserID: idpUserID,
UserID: userID, UserID: userID,
Expiry: expiry,
}) })
if err != nil { if err != nil {
return "", "", time.Time{}, uint64(0), err 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 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{ u := url.URL{
Scheme: "http", Scheme: "http",
Host: host, Host: host,
@ -77,6 +79,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID, IDPID: idpID,
IDPUserID: idpUserID, IDPUserID: idpUserID,
UserID: userID, UserID: userID,
Expiry: expiry,
}) })
if err != nil { if err != nil {
return "", "", time.Time{}, uint64(0), err 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 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{ u := url.URL{
Scheme: "http", Scheme: "http",
Host: host, Host: host,
@ -95,6 +98,7 @@ func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID, IDPID: idpID,
IDPUserID: idpUserID, IDPUserID: idpUserID,
UserID: userID, UserID: userID,
Expiry: expiry,
}) })
if err != nil { if err != nil {
return "", "", time.Time{}, uint64(0), err return "", "", time.Time{}, uint64(0), err
@ -286,6 +290,7 @@ type SuccessfulIntentRequest struct {
IDPID string `json:"idp_id"` IDPID string `json:"idp_id"`
IDPUserID string `json:"idp_user_id"` IDPUserID string `json:"idp_user_id"`
UserID string `json:"user_id"` UserID string `json:"user_id"`
Expiry time.Time `json:"expiry"`
} }
type SuccessfulIntentResponse struct { type SuccessfulIntentResponse struct {
IntentID string `json:"intent_id"` IntentID string `json:"intent_id"`
@ -376,6 +381,7 @@ func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{ Token: &oauth2.Token{
AccessToken: "accessToken", AccessToken: "accessToken",
Expiry: req.Expiry,
}, },
IDToken: "idToken", IDToken: "idToken",
}, },
@ -407,6 +413,7 @@ func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{ Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{ Token: &oauth2.Token{
AccessToken: "accessToken", AccessToken: "accessToken",
Expiry: req.Expiry,
}, },
IDToken: "idToken", IDToken: "idToken",
}, },
@ -431,9 +438,16 @@ func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req
ID: req.IDPUserID, ID: req.IDPUserID,
Attributes: map[string][]string{"attribute1": {"value1"}}, 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 { if err != nil {
return nil, err 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()}} session := &ldap.Session{Entry: &goldap.Entry{
token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes) 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -11,4 +11,5 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SAMLRequestEventType, SAMLRequestEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, SAMLRequestEventType, SAMLRequestEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, LDAPSucceededEventType, LDAPSucceededEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper) eventstore.RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, ConsumedEventType, eventstore.GenericEventMapper[ConsumedEvent])
} }

View File

@ -3,6 +3,7 @@ package idpintent
import ( import (
"context" "context"
"net/url" "net/url"
"time"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
@ -16,6 +17,7 @@ const (
SAMLRequestEventType = instanceEventTypePrefix + "saml.requested" SAMLRequestEventType = instanceEventTypePrefix + "saml.requested"
LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded" LDAPSucceededEventType = instanceEventTypePrefix + "ldap.succeeded"
FailedEventType = instanceEventTypePrefix + "failed" FailedEventType = instanceEventTypePrefix + "failed"
ConsumedEventType = instanceEventTypePrefix + "consumed"
) )
type StartedEvent struct { type StartedEvent struct {
@ -79,6 +81,7 @@ type SucceededEvent struct {
IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"` IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"`
IDPIDToken string `json:"idpIdToken,omitempty"` IDPIDToken string `json:"idpIdToken,omitempty"`
ExpiresAt time.Time `json:"expiresAt,omitempty"`
} }
func NewSucceededEvent( func NewSucceededEvent(
@ -90,6 +93,7 @@ func NewSucceededEvent(
userID string, userID string,
idpAccessToken *crypto.CryptoValue, idpAccessToken *crypto.CryptoValue,
idpIDToken string, idpIDToken string,
expiresAt time.Time,
) *SucceededEvent { ) *SucceededEvent {
return &SucceededEvent{ return &SucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
@ -103,6 +107,7 @@ func NewSucceededEvent(
UserID: userID, UserID: userID,
IDPAccessToken: idpAccessToken, IDPAccessToken: idpAccessToken,
IDPIDToken: idpIDToken, IDPIDToken: idpIDToken,
ExpiresAt: expiresAt,
} }
} }
@ -136,6 +141,7 @@ type SAMLSucceededEvent struct {
UserID string `json:"userId,omitempty"` UserID string `json:"userId,omitempty"`
Assertion *crypto.CryptoValue `json:"assertion,omitempty"` Assertion *crypto.CryptoValue `json:"assertion,omitempty"`
ExpiresAt time.Time `json:"expiresAt,omitempty"`
} }
func NewSAMLSucceededEvent( func NewSAMLSucceededEvent(
@ -146,6 +152,7 @@ func NewSAMLSucceededEvent(
idpUserName, idpUserName,
userID string, userID string,
assertion *crypto.CryptoValue, assertion *crypto.CryptoValue,
expiresAt time.Time,
) *SAMLSucceededEvent { ) *SAMLSucceededEvent {
return &SAMLSucceededEvent{ return &SAMLSucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
@ -158,6 +165,7 @@ func NewSAMLSucceededEvent(
IDPUserName: idpUserName, IDPUserName: idpUserName,
UserID: userID, UserID: userID,
Assertion: assertion, Assertion: assertion,
ExpiresAt: expiresAt,
} }
} }
@ -233,6 +241,7 @@ type LDAPSucceededEvent struct {
UserID string `json:"userId,omitempty"` UserID string `json:"userId,omitempty"`
EntryAttributes map[string][]string `json:"user,omitempty"` EntryAttributes map[string][]string `json:"user,omitempty"`
ExpiresAt time.Time `json:"expiresAt,omitempty"`
} }
func NewLDAPSucceededEvent( func NewLDAPSucceededEvent(
@ -243,6 +252,7 @@ func NewLDAPSucceededEvent(
idpUserName, idpUserName,
userID string, userID string,
attributes map[string][]string, attributes map[string][]string,
expiresAt time.Time,
) *LDAPSucceededEvent { ) *LDAPSucceededEvent {
return &LDAPSucceededEvent{ return &LDAPSucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
@ -255,6 +265,7 @@ func NewLDAPSucceededEvent(
IDPUserName: idpUserName, IDPUserName: idpUserName,
UserID: userID, UserID: userID,
EntryAttributes: attributes, EntryAttributes: attributes,
ExpiresAt: expiresAt,
} }
} }
@ -320,3 +331,32 @@ func FailedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return e, nil 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
}

View File

@ -554,6 +554,7 @@ Errors:
StateMissing: В заявката липсва параметър състояние StateMissing: В заявката липсва параметър състояние
NotStarted: Намерението не е стартирано или вече е прекратено NotStarted: Намерението не е стартирано или вече е прекратено
NotSucceeded: Намерението не е успешно NotSucceeded: Намерението не е успешно
Expired: Намерението е изтекло
TokenCreationFailed: Неуспешно създаване на токен TokenCreationFailed: Неуспешно създаване на токен
InvalidToken: Знакът за намерение е невалиден InvalidToken: Знакът за намерение е невалиден
OtherUser: Намерение, предназначено за друг потребител OtherUser: Намерение, предназначено за друг потребител

View File

@ -534,6 +534,7 @@ Errors:
StateMissing: V požadavku chybí parametr stavu StateMissing: V požadavku chybí parametr stavu
NotStarted: Záměr nebyl zahájen nebo již byl ukončen NotStarted: Záměr nebyl zahájen nebo již byl ukončen
NotSucceeded: Záměr nebyl úspěšný NotSucceeded: Záměr nebyl úspěšný
Expired: Záměr vypršel
TokenCreationFailed: Vytvoření tokenu selhalo TokenCreationFailed: Vytvoření tokenu selhalo
InvalidToken: Token záměru je neplatný InvalidToken: Token záměru je neplatný
OtherUser: Záměr určený pro jiného uživatele OtherUser: Záměr určený pro jiného uživatele

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: State parameter fehlt im Request StateMissing: State parameter fehlt im Request
NotStarted: Intent wurde nicht gestartet oder wurde bereits beendet NotStarted: Intent wurde nicht gestartet oder wurde bereits beendet
NotSucceeded: Intent war nicht erfolgreich NotSucceeded: Intent war nicht erfolgreich
Expired: Intent ist abgelaufen
TokenCreationFailed: Tokenerstellung schlug fehl TokenCreationFailed: Tokenerstellung schlug fehl
InvalidToken: Intent Token ist ungültig InvalidToken: Intent Token ist ungültig
OtherUser: Intent ist für anderen Benutzer gedacht OtherUser: Intent ist für anderen Benutzer gedacht

View File

@ -537,6 +537,7 @@ Errors:
StateMissing: State parameter is missing in the request StateMissing: State parameter is missing in the request
NotStarted: Intent is not started or was already terminated NotStarted: Intent is not started or was already terminated
NotSucceeded: Intent has not succeeded NotSucceeded: Intent has not succeeded
Expired: Intent has expired
TokenCreationFailed: Token creation failed TokenCreationFailed: Token creation failed
InvalidToken: Intent Token is invalid InvalidToken: Intent Token is invalid
OtherUser: Intent meant for another user OtherUser: Intent meant for another user

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: Falta un parámetro de estado en la solicitud StateMissing: Falta un parámetro de estado en la solicitud
NotStarted: La intención no se ha iniciado o ya ha finalizado NotStarted: La intención no se ha iniciado o ya ha finalizado
NotSucceeded: Intento fallido NotSucceeded: Intento fallido
Expired: La intención ha expirado
TokenCreationFailed: Fallo en la creación del token TokenCreationFailed: Fallo en la creación del token
InvalidToken: El token de la intención no es válido InvalidToken: El token de la intención no es válido
OtherUser: Destinado a otro usuario OtherUser: Destinado a otro usuario

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: Paramètre d'état manquant dans la requête StateMissing: Paramètre d'état manquant dans la requête
NotStarted: Intent n'a pas démarré ou s'est déjà terminé NotStarted: Intent n'a pas démarré ou s'est déjà terminé
NotSucceeded: l'intention n'a pas abouti NotSucceeded: l'intention n'a pas abouti
Expired: L'intention a expiré
TokenCreationFailed: La création du token a échoué TokenCreationFailed: La création du token a échoué
InvalidToken: Le jeton d'intention n'est pas valide InvalidToken: Le jeton d'intention n'est pas valide
OtherUser: Intention destinée à un autre utilisateur OtherUser: Intention destinée à un autre utilisateur

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: A kérésből hiányzik a State paraméter StateMissing: A kérésből hiányzik a State paraméter
NotStarted: Az intent nem indult el, vagy már befejeződött NotStarted: Az intent nem indult el, vagy már befejeződött
NotSucceeded: Az intent nem sikerült NotSucceeded: Az intent nem sikerült
Expired: A kérésből lejárt
TokenCreationFailed: A token létrehozása nem sikerült TokenCreationFailed: A token létrehozása nem sikerült
InvalidToken: Az Intent Token érvénytelen InvalidToken: Az Intent Token érvénytelen
OtherUser: Az intent egy másik felhasználónak szól OtherUser: Az intent egy másik felhasználónak szól

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: Parameter status tidak ada dalam permintaan StateMissing: Parameter status tidak ada dalam permintaan
NotStarted: Niat belum dimulai atau sudah dihentikan NotStarted: Niat belum dimulai atau sudah dihentikan
NotSucceeded: Niatnya belum berhasil NotSucceeded: Niatnya belum berhasil
Expired: Kode sudah habis masa berlakunya
TokenCreationFailed: Pembuatan token gagal TokenCreationFailed: Pembuatan token gagal
InvalidToken: Token Niat tidak valid InvalidToken: Token Niat tidak valid
OtherUser: Maksudnya ditujukan untuk pengguna lain OtherUser: Maksudnya ditujukan untuk pengguna lain

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: parametro di stato mancante nella richiesta StateMissing: parametro di stato mancante nella richiesta
NotStarted: l'intento non è stato avviato o è già stato terminato NotStarted: l'intento non è stato avviato o è già stato terminato
NotSucceeded: l'intento non è andato a buon fine NotSucceeded: l'intento non è andato a buon fine
Expired: L'intento è scaduto
TokenCreationFailed: creazione del token fallita TokenCreationFailed: creazione del token fallita
InvalidToken: Il token dell'intento non è valido InvalidToken: Il token dell'intento non è valido
OtherUser: Intento destinato a un altro utente OtherUser: Intento destinato a un altro utente

View File

@ -537,6 +537,7 @@ Errors:
StateMissing: リクエストに State パラメータがありません StateMissing: リクエストに State パラメータがありません
NotStarted: インテントが開始されなかったか、既に終了している NotStarted: インテントが開始されなかったか、既に終了している
NotSucceeded: インテントが成功しなかった NotSucceeded: インテントが成功しなかった
Expired: 意図の有効期限が切れました
TokenCreationFailed: トークンの作成に失敗しました TokenCreationFailed: トークンの作成に失敗しました
InvalidToken: インテントのトークンが無効である InvalidToken: インテントのトークンが無効である
OtherUser: 他のユーザーを意図している OtherUser: 他のユーザーを意図している

View File

@ -537,6 +537,7 @@ Errors:
StateMissing: 요청에 상태 매개변수가 누락되었습니다 StateMissing: 요청에 상태 매개변수가 누락되었습니다
NotStarted: 의도가 시작되지 않았거나 이미 종료되었습니다 NotStarted: 의도가 시작되지 않았거나 이미 종료되었습니다
NotSucceeded: 의도가 성공하지 않았습니다 NotSucceeded: 의도가 성공하지 않았습니다
Expired: 의도의 유효 기간이 만료되었습니다
TokenCreationFailed: 토큰 생성 실패 TokenCreationFailed: 토큰 생성 실패
InvalidToken: 의도 토큰이 유효하지 않습니다 InvalidToken: 의도 토큰이 유효하지 않습니다
OtherUser: 다른 사용자를 위한 의도입니다 OtherUser: 다른 사용자를 위한 의도입니다

View File

@ -535,6 +535,7 @@ Errors:
StateMissing: Параметарот State недостасува во барањето StateMissing: Параметарот State недостасува во барањето
NotStarted: Намерата не е започната или веќе завршена NotStarted: Намерата не е започната или веќе завршена
NotSucceeded: Намерата не е успешна NotSucceeded: Намерата не е успешна
Expired: Намерата е истечена
TokenCreationFailed: Неуспешно креирање на токен TokenCreationFailed: Неуспешно креирање на токен
InvalidToken: Токенот за намера е невалиден InvalidToken: Токенот за намера е невалиден
OtherUser: Намерата е за друг корисник OtherUser: Намерата е за друг корисник

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: Staat parameter ontbreekt in het verzoek StateMissing: Staat parameter ontbreekt in het verzoek
NotStarted: Intentie is niet gestart of was al beëindigd NotStarted: Intentie is niet gestart of was al beëindigd
NotSucceeded: Intentie is niet geslaagd NotSucceeded: Intentie is niet geslaagd
Expired: Intentie is verlopen
TokenCreationFailed: Token aanmaken mislukt TokenCreationFailed: Token aanmaken mislukt
InvalidToken: Intentie Token is ongeldig InvalidToken: Intentie Token is ongeldig
OtherUser: Intentie bedoeld voor een andere gebruiker OtherUser: Intentie bedoeld voor een andere gebruiker

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: Brak parametru stanu w żądaniu StateMissing: Brak parametru stanu w żądaniu
NotStarted: Intencja nie została rozpoczęta lub już się zakończyła NotStarted: Intencja nie została rozpoczęta lub już się zakończyła
NotSucceeded: intencja nie powiodła się NotSucceeded: intencja nie powiodła się
Expired: Intencja wygasła
TokenCreationFailed: Tworzenie tokena nie powiodło się TokenCreationFailed: Tworzenie tokena nie powiodło się
InvalidToken: Token intencji jest nieprawidłowy InvalidToken: Token intencji jest nieprawidłowy
OtherUser: Intencja przeznaczona dla innego użytkownika OtherUser: Intencja przeznaczona dla innego użytkownika

View File

@ -535,6 +535,7 @@ Errors:
StateMissing: O parâmetro de estado está faltando na solicitação StateMissing: O parâmetro de estado está faltando na solicitação
NotStarted: A intenção não foi iniciada ou já foi encerrada NotStarted: A intenção não foi iniciada ou já foi encerrada
NotSucceeded: A intenção não teve sucesso NotSucceeded: A intenção não teve sucesso
Expired: A intenção expirou
TokenCreationFailed: Falha na criação do token TokenCreationFailed: Falha na criação do token
InvalidToken: O token da intenção é inválido InvalidToken: O token da intenção é inválido
OtherUser: Intenção destinada a outro usuário OtherUser: Intenção destinada a outro usuário

View File

@ -537,6 +537,7 @@ Errors:
StateMissing: Parametrul de stare lipsește în cerere StateMissing: Parametrul de stare lipsește în cerere
NotStarted: Intenția nu este pornită sau a fost deja terminată NotStarted: Intenția nu este pornită sau a fost deja terminată
NotSucceeded: Intenția nu a reușit NotSucceeded: Intenția nu a reușit
Expired: Intenția a expirat
TokenCreationFailed: Crearea token-ului a eșuat TokenCreationFailed: Crearea token-ului a eșuat
InvalidToken: Token-ul intenției este invalid InvalidToken: Token-ul intenției este invalid
OtherUser: Intenția este destinată altui utilizator OtherUser: Intenția este destinată altui utilizator

View File

@ -525,6 +525,7 @@ Errors:
StateMissing: В запросе отсутствует параметр State StateMissing: В запросе отсутствует параметр State
NotStarted: Намерение не начато или уже прекращено NotStarted: Намерение не начато или уже прекращено
NotSucceeded: Намерение не увенчалось успехом NotSucceeded: Намерение не увенчалось успехом
Epired: Намерение истекло
TokenCreationFailed: Не удалось создать токен TokenCreationFailed: Не удалось создать токен
InvalidToken: Маркер намерения недействителен InvalidToken: Маркер намерения недействителен
OtherUser: Намерение, предназначенное для другого пользователя OtherUser: Намерение, предназначенное для другого пользователя

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: State-parameter saknas i begäran StateMissing: State-parameter saknas i begäran
NotStarted: Avsikten har inte startat eller har redan avslutats NotStarted: Avsikten har inte startat eller har redan avslutats
NotSucceeded: Avsikten har inte lyckats NotSucceeded: Avsikten har inte lyckats
Expired: Avsikten har gått ut
TokenCreationFailed: Token-skapande misslyckades TokenCreationFailed: Token-skapande misslyckades
InvalidToken: Avsiktstoken är ogiltig InvalidToken: Avsiktstoken är ogiltig
OtherUser: Avsikten är avsedd för en annan användare OtherUser: Avsikten är avsedd för en annan användare

View File

@ -536,6 +536,7 @@ Errors:
StateMissing: 请求中缺少状态参数 StateMissing: 请求中缺少状态参数
NotStarted: 意图没有开始或已经结束 NotStarted: 意图没有开始或已经结束
NotSucceeded: 意图不成功 NotSucceeded: 意图不成功
Expired: 意图已过期
TokenCreationFailed: 令牌创建失败 TokenCreationFailed: 令牌创建失败
InvalidToken: 意图令牌是无效的 InvalidToken: 意图令牌是无效的
OtherUser: 意图是为另一个用户准备的 OtherUser: 意图是为另一个用户准备的