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
# MaxQueryLimit limits the number of items that can be queried in a single v3 API search request with explicitly passing a limit.
MaxQueryLimit: 1000 # ZITADEL_SYSTEMDEFAULTS_MAXQUERYLIMIT
# The maximum duration of the IDP intent lifetime after which the IDP intent expires and can not be retrieved or used anymore.
# Note that this time is measured only after the IdP intent was successful and not after the IDP intent was created.
MaxIdPIntentLifetime: 1h # ZITADEL_SYSTEMDEFAULTS_MAXIDPINTENTLIFETIME
Actions:
HTTP:

View File

@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
// successful intent without known / linked user
idpUserID := "id"
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "")
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, idpUserID, "", time.Now().Add(time.Hour))
// link the user (with info from intent)
Instance.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
@ -447,6 +447,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
require.Error(t, err)
}
func TestServer_CreateSession_reuseIntent(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
updateResp, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
// the reuse of the intent token is not allowed, not even on the same session
session2, err := Client.SetSession(LoginCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.Error(t, err)
_ = session2
}
func TestServer_CreateSession_expiredIntent(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
createResp, err := Client.CreateSession(LoginCTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second))
require.NoError(t, err)
// wait for the intent to expire
time.Sleep(2 * time.Second)
_, err = Client.SetSession(LoginCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.Error(t, err)
}
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID,

View File

@ -354,7 +354,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
@ -372,7 +372,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
func TestServer_CreateSession_successfulIntent_instant(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
@ -396,7 +396,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
// successful intent without known / linked user
idpUserID := "id"
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
// link the user (with info from intent)
@ -448,6 +448,80 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
require.Error(t, err)
}
func TestServer_CreateSession_reuseIntent(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Hour))
require.NoError(t, err)
updateResp, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId(), wantUserFactor, wantIntentFactor)
// the reuse of the intent token is not allowed, not even on the same session
session2, err := Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.Error(t, err)
_ = session2
}
func TestServer_CreateSession_expiredIntent(t *testing.T) {
idpID := Instance.AddGenericOAuthProvider(IAMOwnerCTX, gofakeit.AppName()).GetId()
createResp, err := Client.CreateSession(IAMOwnerCTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{
UserId: User.GetUserId(),
},
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, User.GetUserId())
intentID, token, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), idpID, "id", User.GetUserId(), time.Now().Add(time.Second))
require.NoError(t, err)
// wait for the intent to expire
time.Sleep(2 * time.Second)
_, err = Client.SetSession(IAMOwnerCTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
Checks: &session.Checks{
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: token,
},
},
})
require.Error(t, err)
}
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
resp, err := Instance.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
UserId: userID,

View File

@ -2121,22 +2121,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err)
intentID := authURL.Query().Get("state")
expiry := time.Now().Add(1 * time.Hour)
expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00")
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "")
intentUser := Instance.CreateHumanUser(IamCTX)
_, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username")
require.NoError(t, err)
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user")
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry)
require.NoError(t, err)
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry)
require.NoError(t, err)
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second))
require.NoError(t, err)
// make sure the intent is expired
time.Sleep(2 * time.Second)
successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry)
require.NoError(t, err)
// make sure the intent is consumed
Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken)
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry)
require.NoError(t, err)
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry)
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "")
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry)
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user")
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry)
require.NoError(t, err)
type args struct {
ctx context.Context
@ -2260,6 +2274,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
},
wantErr: false,
},
{
name: "retrieve successful expired intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulExpiredID,
IdpIntentToken: expiredToken,
},
},
wantErr: true,
},
{
name: "retrieve successful consumed intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulConsumedID,
IdpIntentToken: consumedToken,
},
},
wantErr: true,
},
{
name: "retrieve successful oidc intent",
args: args{
@ -2469,7 +2505,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
},
},
IdpId: samlIdpID,
@ -2518,7 +2554,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
},
},
IdpId: samlIdpID,

View File

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

View File

@ -2153,22 +2153,36 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
authURL, err := url.Parse(Instance.CreateIntent(CTX, oauthIdpID).GetAuthUrl())
require.NoError(t, err)
intentID := authURL.Query().Get("state")
expiry := time.Now().Add(1 * time.Hour)
expiryFormatted := expiry.Round(time.Millisecond).UTC().Format("2006-01-02T15:04:05.999Z07:00")
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "")
intentUser := Instance.CreateHumanUser(IamCTX)
_, err = Instance.CreateUserIDPlink(IamCTX, intentUser.GetUserId(), "idpUserID", oauthIdpID, "username")
require.NoError(t, err)
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user")
successfulID, token, changeDate, sequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "", expiry)
require.NoError(t, err)
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "")
successfulWithUserID, withUsertoken, withUserchangeDate, withUsersequence, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", expiry)
require.NoError(t, err)
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user")
successfulExpiredID, expiredToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "id", "user", time.Now().Add(time.Second))
require.NoError(t, err)
// make sure the intent is expired
time.Sleep(2 * time.Second)
successfulConsumedID, consumedToken, _, _, err := sink.SuccessfulOAuthIntent(Instance.ID(), oauthIdpID, "idpUserID", intentUser.GetUserId(), expiry)
require.NoError(t, err)
// make sure the intent is consumed
Instance.CreateIntentSession(t, IamCTX, intentUser.GetUserId(), successfulConsumedID, consumedToken)
oidcSuccessful, oidcToken, oidcChangeDate, oidcSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "", expiry)
require.NoError(t, err)
oidcSuccessfulWithUserID, oidcWithUserIDToken, oidcWithUserIDChangeDate, oidcWithUserIDSequence, err := sink.SuccessfulOIDCIntent(Instance.ID(), oidcIdpID, "id", "user", expiry)
require.NoError(t, err)
ldapSuccessfulID, ldapToken, ldapChangeDate, ldapSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "")
require.NoError(t, err)
ldapSuccessfulWithUserID, ldapWithUserToken, ldapWithUserChangeDate, ldapWithUserSequence, err := sink.SuccessfulLDAPIntent(Instance.ID(), ldapIdpID, "id", "user")
require.NoError(t, err)
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "")
samlSuccessfulID, samlToken, samlChangeDate, samlSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "", expiry)
require.NoError(t, err)
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user")
samlSuccessfulWithUserID, samlWithUserToken, samlWithUserChangeDate, samlWithUserSequence, err := sink.SuccessfulSAMLIntent(Instance.ID(), samlIdpID, "id", "user", expiry)
require.NoError(t, err)
type args struct {
ctx context.Context
@ -2281,6 +2295,28 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
},
wantErr: false,
},
{
name: "retrieve successful expired intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulExpiredID,
IdpIntentToken: expiredToken,
},
},
wantErr: true,
},
{
name: "retrieve successful consumed intent",
args: args{
CTX,
&user.RetrieveIdentityProviderIntentRequest{
IdpIntentId: successfulConsumedID,
IdpIntentToken: consumedToken,
},
},
wantErr: true,
},
{
name: "retrieve successful oidc intent",
args: args{
@ -2466,7 +2502,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
},
},
IdpId: samlIdpID,
@ -2504,7 +2540,7 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
IdpInformation: &user.IDPInformation{
Access: &user.IDPInformation_Saml{
Saml: &user.IDPSAMLAccessInformation{
Assertion: []byte("<Assertion xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"id\" IssueInstant=\"0001-01-01T00:00:00Z\" Version=\"\"><Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"\" SPNameQualifier=\"\" Format=\"\" SPProvidedID=\"\"></Issuer></Assertion>"),
Assertion: []byte(fmt.Sprintf(`<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="id" IssueInstant="0001-01-01T00:00:00Z" Version=""><Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion" NameQualifier="" SPNameQualifier="" Format="" SPProvidedID=""></Issuer><Conditions NotBefore="0001-01-01T00:00:00Z" NotOnOrAfter="%s"></Conditions></Assertion>`, expiryFormatted)),
},
},
IdpId: samlIdpID,

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"io"
"time"
"golang.org/x/text/language"
"google.golang.org/protobuf/types/known/structpb"
@ -399,14 +400,14 @@ func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredenti
if err != nil {
return nil, err
}
externalUser, userID, attributes, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
externalUser, userID, session, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
if err != nil {
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
return nil, err
}
return nil, err
}
token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, attributes)
token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session)
if err != nil {
return nil, err
}
@ -444,7 +445,7 @@ func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUse
return "", nil
}
func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, map[string][]string, error) {
func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) {
provider, err := s.command.GetProvider(ctx, idpID, "", "")
if err != nil {
return nil, "", nil, err
@ -470,7 +471,7 @@ func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string
for _, item := range session.Entry.Attributes {
attributes[item.Name] = item.Values
}
return externalUser, userID, attributes, nil
return externalUser, userID, session, nil
}
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
@ -484,6 +485,9 @@ func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.R
if intent.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded")
}
if time.Now().After(intent.ExpiresAt()) {
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-Afb2s", "Errors.Intent.Expired")
}
return idpIntentToIDPIntentPb(intent, s.idpAlg)
}

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())
logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists")
token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session.Assertion)
token, err := h.commands.SucceedSAMLIDPIntent(ctx, intent, idpUser, userID, session)
if err != nil {
redirectToFailureURLErr(w, r, intent, zerrors.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed"))
return

View File

@ -4,6 +4,7 @@ import (
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -14,11 +15,12 @@ import (
func Test_redirectToSuccessURL(t *testing.T) {
type args struct {
id string
userID string
token string
failureURL string
successURL string
id string
userID string
token string
failureURL string
successURL string
maxIdPIntentLifetime time.Duration
}
type res struct {
want string
@ -59,7 +61,7 @@ func Test_redirectToSuccessURL(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder()
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime)
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
@ -71,11 +73,12 @@ func Test_redirectToSuccessURL(t *testing.T) {
func Test_redirectToFailureURL(t *testing.T) {
type args struct {
id string
failureURL string
successURL string
err string
desc string
id string
failureURL string
successURL string
err string
desc string
maxIdPIntentLifetime time.Duration
}
type res struct {
want string
@ -115,7 +118,7 @@ func Test_redirectToFailureURL(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder()
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime)
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
@ -127,10 +130,11 @@ func Test_redirectToFailureURL(t *testing.T) {
func Test_redirectToFailureURLErr(t *testing.T) {
type args struct {
id string
failureURL string
successURL string
err error
id string
failureURL string
successURL string
err error
maxIdPIntentLifetime time.Duration
}
type res struct {
want string
@ -158,7 +162,7 @@ func Test_redirectToFailureURLErr(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com", nil)
resp := httptest.NewRecorder()
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id, tt.args.maxIdPIntentLifetime)
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
wm.SuccessURL, _ = url.Parse(tt.args.successURL)

View File

@ -81,6 +81,7 @@ type Commands struct {
publicKeyLifetime time.Duration
certificateLifetime time.Duration
defaultSecretGenerators *SecretGenerators
maxIdPIntentLifetime time.Duration
samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error)
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
@ -152,6 +153,7 @@ func StartCommands(
privateKeyLifetime: defaults.KeyConfig.PrivateKeyLifetime,
publicKeyLifetime: defaults.KeyConfig.PublicKeyLifetime,
certificateLifetime: defaults.KeyConfig.CertificateLifetime,
maxIdPIntentLifetime: defaults.MaxIdPIntentLifetime,
idpConfigEncryption: idpConfigEncryption,
smtpEncryption: smtpEncryption,
smsEncryption: smsEncryption,

View File

@ -7,7 +7,6 @@ import (
"encoding/xml"
"net/url"
"github.com/crewjam/saml"
"github.com/crewjam/saml/samlsp"
"github.com/zitadel/oidc/v3/pkg/oidc"
@ -19,8 +18,10 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/apple"
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/saml"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -68,7 +69,7 @@ func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL
return nil, nil, err
}
}
writeModel := NewIDPIntentWriteModel(intentID, resourceOwner)
writeModel := NewIDPIntentWriteModel(intentID, resourceOwner, c.maxIdPIntentLifetime)
//nolint: staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments))
@ -180,6 +181,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
userID,
accessToken,
idToken,
idpSession.ExpiresAt(),
)
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
@ -188,7 +190,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
return token, nil
}
func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, assertion *saml.Assertion) (string, error) {
func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *saml.Session) (string, error) {
token, err := c.generateIntentToken(writeModel.AggregateID)
if err != nil {
return "", err
@ -197,7 +199,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte
if err != nil {
return "", err
}
assertionData, err := xml.Marshal(assertion)
assertionData, err := xml.Marshal(session.Assertion)
if err != nil {
return "", err
}
@ -213,6 +215,7 @@ func (c *Commands) SucceedSAMLIDPIntent(ctx context.Context, writeModel *IDPInte
idpUser.GetPreferredUsername(),
userID,
assertionEnc,
session.ExpiresAt(),
)
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
@ -237,7 +240,7 @@ func (c *Commands) generateIntentToken(intentID string) (string, error) {
return base64.RawURLEncoding.EncodeToString(token), nil
}
func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, attributes map[string][]string) (string, error) {
func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, userID string, session *ldap.Session) (string, error) {
token, err := c.generateIntentToken(writeModel.AggregateID)
if err != nil {
return "", err
@ -246,6 +249,10 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte
if err != nil {
return "", err
}
attributes := make(map[string][]string, len(session.Entry.Attributes))
for _, item := range session.Entry.Attributes {
attributes[item.Name] = item.Values
}
cmd := idpintent.NewLDAPSucceededEvent(
ctx,
IDPIntentAggregateFromWriteModel(&writeModel.WriteModel),
@ -254,6 +261,7 @@ func (c *Commands) SucceedLDAPIDPIntent(ctx context.Context, writeModel *IDPInte
idpUser.GetPreferredUsername(),
userID,
attributes,
session.ExpiresAt(),
)
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil {
@ -273,7 +281,7 @@ func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWrite
}
func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner string) (*IDPIntentWriteModel, error) {
writeModel := NewIDPIntentWriteModel(id, resourceOwner)
writeModel := NewIDPIntentWriteModel(id, resourceOwner, c.maxIdPIntentLifetime)
err := c.eventstore.FilterToQueryReducer(ctx, writeModel)
if err != nil {
return nil, err

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/notification/senders"
"github.com/zitadel/zitadel/internal/repository/idpintent"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/zerrors"
@ -32,31 +33,33 @@ type SessionCommands struct {
eventstore *eventstore.Eventstore
eventCommands []eventstore.Command
hasher *crypto.Hasher
intentAlg crypto.EncryptionAlgorithm
totpAlg crypto.EncryptionAlgorithm
otpAlg crypto.EncryptionAlgorithm
createCode encryptedCodeWithDefaultFunc
createPhoneCode encryptedCodeGeneratorWithDefaultFunc
createToken func(sessionID string) (id string, token string, err error)
getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
now func() time.Time
hasher *crypto.Hasher
intentAlg crypto.EncryptionAlgorithm
totpAlg crypto.EncryptionAlgorithm
otpAlg crypto.EncryptionAlgorithm
createCode encryptedCodeWithDefaultFunc
createPhoneCode encryptedCodeGeneratorWithDefaultFunc
createToken func(sessionID string) (id string, token string, err error)
getCodeVerifier func(ctx context.Context, id string) (senders.CodeGenerator, error)
now func() time.Time
maxIdPIntentLifetime time.Duration
}
func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWriteModel) *SessionCommands {
return &SessionCommands{
sessionCommands: cmds,
sessionWriteModel: session,
eventstore: c.eventstore,
hasher: c.userPasswordHasher,
intentAlg: c.idpConfigEncryption,
totpAlg: c.multifactors.OTP.CryptoMFA,
otpAlg: c.userEncryption,
createCode: c.newEncryptedCodeWithDefault,
createPhoneCode: c.newPhoneCode,
createToken: c.sessionTokenCreator,
getCodeVerifier: c.phoneCodeVerifierFromConfig,
now: time.Now,
sessionCommands: cmds,
sessionWriteModel: session,
eventstore: c.eventstore,
hasher: c.userPasswordHasher,
intentAlg: c.idpConfigEncryption,
totpAlg: c.multifactors.OTP.CryptoMFA,
otpAlg: c.userEncryption,
createCode: c.newEncryptedCodeWithDefault,
createPhoneCode: c.newPhoneCode,
createToken: c.sessionTokenCreator,
getCodeVerifier: c.phoneCodeVerifierFromConfig,
now: time.Now,
maxIdPIntentLifetime: c.maxIdPIntentLifetime,
}
}
@ -92,7 +95,7 @@ func CheckIntent(intentID, token string) SessionCommand {
if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil {
return nil, err
}
cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "")
cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "", cmd.maxIdPIntentLifetime)
err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel)
if err != nil {
return nil, err
@ -100,6 +103,9 @@ func CheckIntent(intentID, token string) SessionCommand {
if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded")
}
if time.Now().After(cmd.intentWriteModel.ExpiresAt()) {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired")
}
if cmd.intentWriteModel.UserID != "" {
if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
@ -168,6 +174,7 @@ func (s *SessionCommands) PasswordChecked(ctx context.Context, checkedAt time.Ti
func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time) {
s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
s.eventCommands = append(s.eventCommands, idpintent.NewConsumedEvent(ctx, IDPIntentAggregateFromWriteModel(&s.intentWriteModel.WriteModel)))
}
func (s *SessionCommands) WebAuthNChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement, rpid string) {

View File

@ -695,6 +695,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID2",
nil,
"",
time.Now().Add(time.Hour),
),
),
),
@ -757,6 +758,111 @@ func TestCommands_updateSession(t *testing.T) {
err: zerrors.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken"),
},
},
{
"set user, intent token already consumed",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(),
&idpintent.NewAggregate("intent", "instance1").Aggregate,
nil,
"idpUserID",
"idpUsername",
"userID",
nil,
"",
time.Now().Add(time.Hour),
),
),
eventFromEventPusher(
idpintent.NewConsumedEvent(context.Background(),
&idpintent.NewAggregate("intent", "instance1").Aggregate,
),
),
),
),
},
args{
ctx: authz.NewMockContext("instance1", "", ""),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
sessionCommands: []SessionCommand{
CheckUser("userID", "org1", &language.Afrikaans),
CheckIntent("intent", "aW50ZW50"),
},
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
intentAlg: decryption(nil),
now: func() time.Time {
return testNow
},
},
metadata: map[string][]byte{
"key": []byte("value"),
},
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded"),
},
},
{
"set user, intent token already expired",
fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(),
&idpintent.NewAggregate("intent", "instance1").Aggregate,
nil,
"idpUserID",
"idpUsername",
"userID",
nil,
"",
time.Now().Add(-time.Hour),
),
),
),
),
},
args{
ctx: authz.NewMockContext("instance1", "", ""),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "instance1"),
sessionCommands: []SessionCommand{
CheckUser("userID", "org1", &language.Afrikaans),
CheckIntent("intent", "aW50ZW50"),
},
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
intentAlg: decryption(nil),
now: func() time.Time {
return testNow
},
},
metadata: map[string][]byte{
"key": []byte("value"),
},
},
res{
err: zerrors.ThrowPreconditionFailed(nil, "COMMAND-SAf42", "Errors.Intent.Expired"),
},
},
{
"set user, intent, metadata and token",
fields{
@ -768,13 +874,14 @@ func TestCommands_updateSession(t *testing.T) {
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(),
&idpintent.NewAggregate("id", "instance1").Aggregate,
&idpintent.NewAggregate("intent", "instance1").Aggregate,
nil,
"idpUserID",
"idpUsername",
"userID",
nil,
"",
time.Now().Add(time.Hour),
),
),
),
@ -783,6 +890,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID", "org1", testNow, &language.Afrikaans),
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate),
session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
map[string][]byte{"key": []byte("value")}),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
@ -842,13 +950,14 @@ func TestCommands_updateSession(t *testing.T) {
),
eventFromEventPusher(
idpintent.NewSucceededEvent(context.Background(),
&idpintent.NewAggregate("id", "instance1").Aggregate,
&idpintent.NewAggregate("intent", "instance1").Aggregate,
nil,
"idpUserID",
"idpUsername",
"",
nil,
"",
time.Now().Add(time.Hour),
),
),
),
@ -866,6 +975,7 @@ func TestCommands_updateSession(t *testing.T) {
"userID", "org1", testNow, &language.Afrikaans),
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
testNow),
idpintent.NewConsumedEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "instance1").Aggregate,
"tokenID"),
),

View File

@ -7,15 +7,16 @@ import (
)
type SystemDefaults struct {
SecretGenerators SecretGenerators
PasswordHasher crypto.HashConfig
SecretHasher crypto.HashConfig
Multifactors MultifactorConfig
DomainVerification DomainVerification
Notifications Notifications
KeyConfig KeyConfig
DefaultQueryLimit uint64
MaxQueryLimit uint64
SecretGenerators SecretGenerators
PasswordHasher crypto.HashConfig
SecretHasher crypto.HashConfig
Multifactors MultifactorConfig
DomainVerification DomainVerification
Notifications Notifications
KeyConfig KeyConfig
DefaultQueryLimit uint64
MaxQueryLimit uint64
MaxIdPIntentLifetime time.Duration
}
type SecretGenerators struct {

View File

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

View File

@ -10,6 +10,8 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
)
var _ idp.Session = (*Session)(nil)
// Session extends the [oidc.Session] with the formValues returned from the callback.
// This enables to parse the user (name and email), which Apple only returns as form params on registration
type Session struct {

View File

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

View File

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

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

View File

@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
"time"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
@ -69,6 +70,13 @@ func (s *Session) FetchUser(ctx context.Context) (_ idp.User, err error) {
return user, nil
}
func (s *Session) ExpiresAt() time.Time {
if s.Tokens == nil {
return time.Time{}
}
return s.Tokens.Expiry
}
func (s *Session) authorize(ctx context.Context) (err error) {
if s.Code == "" {
return ErrCodeMissing

View File

@ -3,6 +3,7 @@ package oidc
import (
"context"
"errors"
"time"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/oidc"
@ -72,6 +73,13 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
return u, nil
}
func (s *Session) ExpiresAt() time.Time {
if s.Tokens == nil {
return time.Time{}
}
return s.Tokens.Expiry
}
func (s *Session) Authorize(ctx context.Context) (err error) {
if s.Code == "" {
return ErrCodeMissing

View File

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

View File

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

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()
}
func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID, intentID, intentToken string) (id, token string, start, change time.Time) {
createResp, err := i.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{UserId: userID},
},
IdpIntent: &session.CheckIDPIntent{
IdpIntentId: intentID,
IdpIntentToken: intentToken,
},
},
})
require.NoError(t, err)
return createResp.GetSessionId(), createResp.GetSessionToken(),
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
}
func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse {
resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{
GrantedOrgId: grantedOrgID,

View File

@ -17,6 +17,7 @@ import (
crewjam_saml "github.com/crewjam/saml"
"github.com/go-chi/chi/v5"
goldap "github.com/go-ldap/ldap/v3"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/zitadel/logging"
@ -48,7 +49,7 @@ func CallURL(ch Channel) string {
return u.String()
}
func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
@ -59,6 +60,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID,
IDPUserID: idpUserID,
UserID: userID,
Expiry: expiry,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
@ -66,7 +68,7 @@ func SuccessfulOAuthIntent(instanceID, idpID, idpUserID, userID string) (string,
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
}
func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
@ -77,6 +79,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID,
IDPUserID: idpUserID,
UserID: userID,
Expiry: expiry,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
@ -84,7 +87,7 @@ func SuccessfulOIDCIntent(instanceID, idpID, idpUserID, userID string) (string,
return resp.IntentID, resp.Token, resp.ChangeDate, resp.Sequence, nil
}
func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string, string, time.Time, uint64, error) {
func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string, expiry time.Time) (string, string, time.Time, uint64, error) {
u := url.URL{
Scheme: "http",
Host: host,
@ -95,6 +98,7 @@ func SuccessfulSAMLIntent(instanceID, idpID, idpUserID, userID string) (string,
IDPID: idpID,
IDPUserID: idpUserID,
UserID: userID,
Expiry: expiry,
})
if err != nil {
return "", "", time.Time{}, uint64(0), err
@ -282,10 +286,11 @@ func readLoop(ws *websocket.Conn) (done chan error) {
}
type SuccessfulIntentRequest struct {
InstanceID string `json:"instance_id"`
IDPID string `json:"idp_id"`
IDPUserID string `json:"idp_user_id"`
UserID string `json:"user_id"`
InstanceID string `json:"instance_id"`
IDPID string `json:"idp_id"`
IDPUserID string `json:"idp_user_id"`
UserID string `json:"user_id"`
Expiry time.Time `json:"expiry"`
}
type SuccessfulIntentResponse struct {
IntentID string `json:"intent_id"`
@ -376,6 +381,7 @@ func createSuccessfulOAuthIntent(ctx context.Context, cmd *command.Commands, req
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
Expiry: req.Expiry,
},
IDToken: "idToken",
},
@ -407,6 +413,7 @@ func createSuccessfulOIDCIntent(ctx context.Context, cmd *command.Commands, req
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
Token: &oauth2.Token{
AccessToken: "accessToken",
Expiry: req.Expiry,
},
IDToken: "idToken",
},
@ -431,9 +438,16 @@ func createSuccessfulSAMLIntent(ctx context.Context, cmd *command.Commands, req
ID: req.IDPUserID,
Attributes: map[string][]string{"attribute1": {"value1"}},
}
assertion := &crewjam_saml.Assertion{ID: "id"}
session := &saml.Session{
Assertion: &crewjam_saml.Assertion{
ID: "id",
Conditions: &crewjam_saml.Conditions{
NotOnOrAfter: req.Expiry,
},
},
}
token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, assertion)
token, err := cmd.SucceedSAMLIDPIntent(ctx, writeModel, idpUser, req.UserID, session)
if err != nil {
return nil, err
}
@ -465,8 +479,14 @@ func createSuccessfulLDAPIntent(ctx context.Context, cmd *command.Commands, req
"",
"",
)
attributes := map[string][]string{"id": {req.IDPUserID}, "username": {username}, "language": {lang.String()}}
token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, attributes)
session := &ldap.Session{Entry: &goldap.Entry{
Attributes: []*goldap.EntryAttribute{
{Name: "id", Values: []string{req.IDPUserID}},
{Name: "username", Values: []string{username}},
{Name: "language", Values: []string{lang.String()}},
},
}}
token, err := cmd.SucceedLDAPIDPIntent(ctx, writeModel, idpUser, req.UserID, session)
if err != nil {
return nil, err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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