feat: add possibility to set an expiration to a session (#6851)

* add lifetime to session api

* extend session with lifetime

* check session token expiration

* fix typo

* integration test to check session token expiration

* integration test to check session token expiration

* i18n

* cleanup

* improve tests

* prevent negative lifetime

* fix error message

* fix lifetime check
This commit is contained in:
Livio Spring 2023-11-06 11:48:28 +02:00 committed by GitHub
parent ce322323aa
commit f3b8a3aece
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 608 additions and 151 deletions

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"net" "net"
"net/http" "net/http"
"time"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
@ -46,7 +47,7 @@ func (s *Server) ListSessions(ctx context.Context, req *session.ListSessionsRequ
} }
func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) { func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRequest) (*session.CreateSessionResponse, error) {
checks, metadata, userAgent, err := s.createSessionRequestToCommand(ctx, req) checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -55,7 +56,7 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe
return nil, err return nil, err
} }
set, err := s.command.CreateSession(ctx, cmds, metadata, userAgent) set, err := s.command.CreateSession(ctx, cmds, metadata, userAgent, lifetime)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -78,7 +79,7 @@ func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest)
return nil, err return nil, err
} }
set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), cmds, req.GetMetadata()) set, err := s.command.UpdateSession(ctx, req.GetSessionId(), req.GetSessionToken(), cmds, req.GetMetadata(), req.GetLifetime().AsDuration())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -113,13 +114,14 @@ func sessionsToPb(sessions []*query.Session) []*session.Session {
func sessionToPb(s *query.Session) *session.Session { func sessionToPb(s *query.Session) *session.Session {
return &session.Session{ return &session.Session{
Id: s.ID, Id: s.ID,
CreationDate: timestamppb.New(s.CreationDate), CreationDate: timestamppb.New(s.CreationDate),
ChangeDate: timestamppb.New(s.ChangeDate), ChangeDate: timestamppb.New(s.ChangeDate),
Sequence: s.Sequence, Sequence: s.Sequence,
Factors: factorsToPb(s), Factors: factorsToPb(s),
Metadata: s.Metadata, Metadata: s.Metadata,
UserAgent: userAgentToPb(s.UserAgent), UserAgent: userAgentToPb(s.UserAgent),
ExpirationDate: expirationToPb(s.Expiration),
} }
} }
@ -147,6 +149,13 @@ func userAgentToPb(ua domain.UserAgent) *session.UserAgent {
return out return out
} }
func expirationToPb(expiration time.Time) *timestamppb.Timestamp {
if expiration.IsZero() {
return nil
}
return timestamppb.New(expiration)
}
func factorsToPb(s *query.Session) *session.Factors { func factorsToPb(s *query.Session) *session.Factors {
user := userFactorToPb(s.UserFactor) user := userFactorToPb(s.UserFactor)
if user == nil { if user == nil {
@ -268,12 +277,12 @@ func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) {
return query.NewSessionIDsSearchQuery(q.Ids) return query.NewSessionIDsSearchQuery(q.Ids)
} }
func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, error) { func (s *Server) createSessionRequestToCommand(ctx context.Context, req *session.CreateSessionRequest) ([]command.SessionCommand, map[string][]byte, *domain.UserAgent, time.Duration, error) {
checks, err := s.checksToCommand(ctx, req.Checks) checks, err := s.checksToCommand(ctx, req.Checks)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, 0, err
} }
return checks, req.GetMetadata(), userAgentToCommand(req.GetUserAgent()), nil return checks, req.GetMetadata(), userAgentToCommand(req.GetUserAgent()), req.GetLifetime().AsDuration(), nil
} }
func userAgentToCommand(userAgent *session.UserAgent) *domain.UserAgent { func userAgentToCommand(userAgent *session.UserAgent) *domain.UserAgent {

View File

@ -15,6 +15,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta" object "github.com/zitadel/zitadel/pkg/grpc/object/v2beta"
@ -54,7 +55,7 @@ func TestMain(m *testing.M) {
}()) }())
} }
func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, factors ...wantFactor) *session.Session { func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, userAgent *session.UserAgent, expirationWindow time.Duration, factors ...wantFactor) *session.Session {
t.Helper() t.Helper()
require.NotEmpty(t, id) require.NotEmpty(t, id)
require.NotEmpty(t, token) require.NotEmpty(t, token)
@ -75,6 +76,11 @@ func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, windo
if !proto.Equal(userAgent, s.GetUserAgent()) { if !proto.Equal(userAgent, s.GetUserAgent()) {
t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent) t.Errorf("user agent =\n%v\nwant\n%v", s.GetUserAgent(), userAgent)
} }
if expirationWindow == 0 {
assert.Nil(t, s.GetExpirationDate())
} else {
assert.WithinRange(t, s.GetExpirationDate().AsTime(), time.Now().Add(-expirationWindow), time.Now().Add(expirationWindow))
}
verifyFactors(t, s.GetFactors(), window, factors) verifyFactors(t, s.GetFactors(), window, factors)
return s return s
@ -137,12 +143,13 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration,
func TestServer_CreateSession(t *testing.T) { func TestServer_CreateSession(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
req *session.CreateSessionRequest req *session.CreateSessionRequest
want *session.CreateSessionResponse want *session.CreateSessionResponse
wantErr bool wantErr bool
wantFactors []wantFactor wantFactors []wantFactor
wantUserAgent *session.UserAgent wantUserAgent *session.UserAgent
wantExpirationWindow time.Duration
}{ }{
{ {
name: "empty session", name: "empty session",
@ -182,6 +189,27 @@ func TestServer_CreateSession(t *testing.T) {
}, },
}, },
}, },
{
name: "negative lifetime",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
Lifetime: durationpb.New(-5 * time.Minute),
},
wantErr: true,
},
{
name: "lifetime",
req: &session.CreateSessionRequest{
Metadata: map[string][]byte{"foo": []byte("bar")},
Lifetime: durationpb.New(5 * time.Minute),
},
want: &session.CreateSessionResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
wantExpirationWindow: 5 * time.Minute,
},
{ {
name: "with user", name: "with user",
req: &session.CreateSessionRequest{ req: &session.CreateSessionRequest{
@ -253,7 +281,7 @@ func TestServer_CreateSession(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
integration.AssertDetails(t, tt.want, got) integration.AssertDetails(t, tt.want, got)
verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantFactors...) verifyCurrentSession(t, got.GetSessionId(), got.GetSessionToken(), got.GetDetails().GetSequence(), time.Minute, tt.req.GetMetadata(), tt.wantUserAgent, tt.wantExpirationWindow, tt.wantFactors...)
}) })
} }
} }
@ -276,7 +304,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err) require.NoError(t, err)
@ -292,7 +320,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactorUserVerified) verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantWebAuthNFactorUserVerified)
} }
func TestServer_CreateSession_successfulIntent(t *testing.T) { func TestServer_CreateSession_successfulIntent(t *testing.T) {
@ -308,7 +336,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) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id") intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id")
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
@ -322,7 +350,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantIntentFactor) verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantIntentFactor)
} }
func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
@ -338,7 +366,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
idpUserID := "id" idpUserID := "id"
intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", idpUserID) intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", idpUserID)
@ -365,7 +393,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantIntentFactor) verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantIntentFactor)
} }
func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
@ -381,7 +409,7 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
intentID := Tester.CreateIntent(t, idpID) intentID := Tester.CreateIntent(t, idpID)
_, err = Client.SetSession(CTX, &session.SetSessionRequest{ _, err = Client.SetSession(CTX, &session.SetSessionRequest{
@ -433,7 +461,7 @@ func TestServer_SetSession_flow(t *testing.T) {
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err) require.NoError(t, err)
sessionToken := createResp.GetSessionToken() sessionToken := createResp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
t.Run("check user", func(t *testing.T) { t.Run("check user", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
@ -449,7 +477,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor)
}) })
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) { t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
@ -464,7 +492,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true) assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
@ -481,7 +509,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactorUserVerified) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantWebAuthNFactorUserVerified)
}) })
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken) userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
@ -508,7 +536,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false) assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false)
@ -525,7 +553,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantWebAuthNFactor)
}) })
} }
}) })
@ -544,7 +572,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor)
}) })
t.Run("check OTP SMS", func(t *testing.T) { t.Run("check OTP SMS", func(t *testing.T) {
@ -556,7 +584,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpSms() otp := resp.GetChallenges().GetOtpSms()
@ -573,7 +601,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantWebAuthNFactor, wantOTPSMSFactor)
}) })
t.Run("check OTP Email", func(t *testing.T) { t.Run("check OTP Email", func(t *testing.T) {
@ -587,7 +615,7 @@ func TestServer_SetSession_flow(t *testing.T) {
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil) verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
otp := resp.GetChallenges().GetOtpEmail() otp := resp.GetChallenges().GetOtpEmail()
@ -604,10 +632,34 @@ func TestServer_SetSession_flow(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
sessionToken = resp.GetSessionToken() sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor) verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, nil, 0, wantUserFactor, wantWebAuthNFactor, wantOTPEmailFactor)
}) })
} }
func TestServer_SetSession_expired(t *testing.T) {
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Lifetime: durationpb.New(20 * time.Second),
})
require.NoError(t, err)
// test session token works
sessionResp, err := Tester.Client.SessionV2.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: createResp.GetSessionToken(),
Lifetime: durationpb.New(20 * time.Second),
})
require.NoError(t, err)
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)
_, err = Tester.Client.SessionV2.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionResp.GetSessionToken(),
Lifetime: durationpb.New(20 * time.Second),
})
require.Error(t, err)
}
func Test_ZITADEL_API_missing_authentication(t *testing.T) { func Test_ZITADEL_API_missing_authentication(t *testing.T) {
// create new, empty session // create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
@ -629,7 +681,7 @@ func Test_ZITADEL_API_missing_mfa(t *testing.T) {
} }
func Test_ZITADEL_API_success(t *testing.T) { func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, User.GetUserId()) id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
ctx := Tester.WithAuthorizationToken(context.Background(), token) ctx := Tester.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
@ -641,7 +693,7 @@ func Test_ZITADEL_API_success(t *testing.T) {
} }
func Test_ZITADEL_API_session_not_found(t *testing.T) { func Test_ZITADEL_API_session_not_found(t *testing.T) {
id, token, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, User.GetUserId()) id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId())
// test session token works // test session token works
ctx := Tester.WithAuthorizationToken(context.Background(), token) ctx := Tester.WithAuthorizationToken(context.Background(), token)
@ -658,3 +710,18 @@ func Test_ZITADEL_API_session_not_found(t *testing.T) {
_, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id}) _, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err) require.Error(t, err)
} }
func Test_ZITADEL_API_session_expired(t *testing.T) {
id, token, _, _ := Tester.CreateVerifiedWebAuthNSessionWithLifetime(t, CTX, User.GetUserId(), 20*time.Second)
// test session token works
ctx := Tester.WithAuthorizationToken(context.Background(), token)
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
// ensure session expires and does not work anymore
time.Sleep(20 * time.Second)
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.Error(t, err)
require.Nil(t, sessionResp)
}

View File

@ -27,7 +27,7 @@ func Test_sessionsToPb(t *testing.T) {
past := now.Add(-time.Hour) past := now.Add(-time.Hour)
sessions := []*query.Session{ sessions := []*query.Session{
{ // no factor, with user agent { // no factor, with user agent and expiration
ID: "999", ID: "999",
CreationDate: now, CreationDate: now,
ChangeDate: now, ChangeDate: now,
@ -42,6 +42,7 @@ func Test_sessionsToPb(t *testing.T) {
IP: net.IPv4(1, 2, 3, 4), IP: net.IPv4(1, 2, 3, 4),
Header: http.Header{"foo": []string{"foo", "bar"}}, Header: http.Header{"foo": []string{"foo", "bar"}},
}, },
Expiration: now,
}, },
{ // user factor { // user factor
ID: "999", ID: "999",
@ -124,7 +125,7 @@ func Test_sessionsToPb(t *testing.T) {
} }
want := []*session.Session{ want := []*session.Session{
{ // no factor, with user agent { // no factor, with user agent and expiration
Id: "999", Id: "999",
CreationDate: timestamppb.New(now), CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now), ChangeDate: timestamppb.New(now),
@ -139,6 +140,7 @@ func Test_sessionsToPb(t *testing.T) {
"foo": {Values: []string{"foo", "bar"}}, "foo": {Values: []string{"foo", "bar"}},
}, },
}, },
ExpirationDate: timestamppb.New(now),
}, },
{ // user factor { // user factor
Id: "999", Id: "999",

View File

@ -16,13 +16,13 @@ import (
func TestServer_AddOTPSMS(t *testing.T) { func TestServer_AddOTPSMS(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID) Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID) _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
// TODO: add when phone can be added to user // TODO: add when phone can be added to user
/* /*
userIDPhone := Tester.CreateHumanUser(CTX).GetUserId() userIDPhone := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userIDPhone) Tester.RegisterUserPasskey(CTX, userIDPhone)
_, sessionTokenPhone, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userIDPhone) _, sessionTokenPhone, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userIDPhone)
*/ */
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -99,7 +99,7 @@ func TestServer_RemoveOTPSMS(t *testing.T) {
/* /*
userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID) Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID) _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
*/ */
type args struct { type args struct {
@ -157,7 +157,7 @@ func TestServer_RemoveOTPSMS(t *testing.T) {
func TestServer_AddOTPEmail(t *testing.T) { func TestServer_AddOTPEmail(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID) Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID) _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX) userVerified := Tester.CreateHumanUser(CTX)
_, err := Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{ _, err := Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{
@ -166,7 +166,7 @@ func TestServer_AddOTPEmail(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userVerified.GetUserId()) _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
type args struct { type args struct {
ctx context.Context ctx context.Context
@ -238,11 +238,11 @@ func TestServer_AddOTPEmail(t *testing.T) {
func TestServer_RemoveOTPEmail(t *testing.T) { func TestServer_RemoveOTPEmail(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId() userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID) Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID) _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX) userVerified := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userVerified.GetUserId()) _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId())
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified) userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
_, err := Tester.Client.UserV2.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{ _, err := Tester.Client.UserV2.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{
UserId: userVerified.GetUserId(), UserId: userVerified.GetUserId(),

View File

@ -36,7 +36,7 @@ func TestOPStorage_CreateAuthRequest(t *testing.T) {
func TestOPStorage_CreateAccessToken_code(t *testing.T) { func TestOPStorage_CreateAccessToken_code(t *testing.T) {
clientID := createClient(t) clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI) authRequestID := createAuthRequest(t, clientID, redirectURI)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -75,7 +75,7 @@ func TestOPStorage_CreateAccessToken_code(t *testing.T) {
func TestOPStorage_CreateAccessToken_implicit(t *testing.T) { func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
clientID := createImplicitClient(t) clientID := createImplicitClient(t)
authRequestID := createAuthRequestImplicit(t, clientID, redirectURIImplicit) authRequestID := createAuthRequestImplicit(t, clientID, redirectURIImplicit)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -125,7 +125,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) { func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) {
clientID := createClient(t) clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -150,7 +150,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -186,7 +186,7 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) {
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -229,7 +229,7 @@ func TestOPStorage_RevokeToken_access_token_invalid_token_hint_type(t *testing.T
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -266,7 +266,7 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) {
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -309,7 +309,7 @@ func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -344,7 +344,7 @@ func TestOPStorage_RevokeToken_refresh_token_invalid_token_type_hint(t *testing.
func TestOPStorage_RevokeToken_invalid_client(t *testing.T) { func TestOPStorage_RevokeToken_invalid_client(t *testing.T) {
clientID := createClient(t) clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -376,7 +376,7 @@ func TestOPStorage_TerminateSession(t *testing.T) {
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI) authRequestID := createAuthRequest(t, clientID, redirectURI)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -413,7 +413,7 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -457,7 +457,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI) authRequestID := createAuthRequest(t, clientID, redirectURI)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{

View File

@ -21,7 +21,7 @@ import (
func TestOPStorage_SetUserinfoFromToken(t *testing.T) { func TestOPStorage_SetUserinfoFromToken(t *testing.T) {
clientID := createClient(t) clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -67,7 +67,7 @@ func TestOPStorage_SetIntrospectionFromToken(t *testing.T) {
scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess} scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}
authRequestID := createAuthRequest(t, app.GetClientId(), redirectURI, scope...) authRequestID := createAuthRequest(t, app.GetClientId(), redirectURI, scope...)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{

View File

@ -57,7 +57,7 @@ func TestMain(m *testing.M) {
func Test_ZITADEL_API_missing_audience_scope(t *testing.T) { func Test_ZITADEL_API_missing_audience_scope(t *testing.T) {
clientID := createClient(t) clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -148,7 +148,7 @@ func Test_ZITADEL_API_missing_mfa(t *testing.T) {
func Test_ZITADEL_API_success(t *testing.T) { func Test_ZITADEL_API_success(t *testing.T) {
clientID := createClient(t) clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -177,7 +177,7 @@ func Test_ZITADEL_API_success(t *testing.T) {
func Test_ZITADEL_API_inactive_access_token(t *testing.T) { func Test_ZITADEL_API_inactive_access_token(t *testing.T) {
clientID := createClient(t) clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -219,7 +219,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) {
provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI)
require.NoError(t, err) require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope) authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId()) sessionID, sessionToken, startTime, changeTime := Tester.CreateVerifiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{ linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID, AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ CallbackKind: &oidc_pb.CreateCallbackRequest_Session{

View File

@ -153,6 +153,9 @@ func (repo *TokenVerifierRepo) verifySessionToken(ctx context.Context, sessionID
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
if !session.Expiration.IsZero() && session.Expiration.Before(time.Now()) {
return "", "", "", caos_errs.ThrowPermissionDenied(nil, "AUTHZ-EGDo3", "session expired")
}
if err = repo.checkAuthentication(ctx, authMethodsFromSession(session), session.UserFactor.UserID); err != nil { if err = repo.checkAuthentication(ctx, authMethodsFromSession(session), session.UserFactor.UserID); err != nil {
return "", "", "", err return "", "", "", err
} }

View File

@ -95,8 +95,8 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID,
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if sessionWriteModel.State == domain.SessionStateUnspecified { if err = sessionWriteModel.CheckIsActive(); err != nil {
return nil, nil, errors.ThrowNotFound(nil, "COMMAND-x0099887", "Errors.Session.NotExisting") return nil, nil, err
} }
if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil { if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil {
return nil, nil, err return nil, nil, err

View File

@ -327,7 +327,67 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
sessionID: "sessionID", sessionID: "sessionID",
}, },
res{ res{
wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-x0099887", "Errors.Session.NotExisting"), wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
},
},
{
"session expired",
fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
"loginClient",
"clientID",
"redirectURI",
"state",
"nonce",
[]string{"openid"},
[]string{"audience"},
domain.OIDCResponseTypeCode,
nil,
nil,
nil,
nil,
nil,
nil,
),
),
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx,
&session.NewAggregate("sessionID", "org1").Aggregate,
&domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
)),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
"userID", testNow.Add(-5*time.Minute)),
),
eventFromEventPusher(
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
testNow.Add(-5*time.Minute)),
),
eventFromEventPusher(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
2*time.Minute),
),
),
),
},
args{
ctx: mockCtx,
id: "V2_id",
sessionID: "sessionID",
sessionToken: "token",
},
res{
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Hkl3d", "Errors.Session.Expired"),
}, },
}, },
{ {
@ -475,6 +535,10 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
testNow), testNow),
), ),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
2*time.Minute),
),
), ),
expectPush( expectPush(
authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,
@ -559,6 +623,10 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
testNow), testNow),
), ),
eventFromEventPusherWithCreationDateNow(
session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
2*time.Minute),
),
), ),
expectPush( expectPush(
authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate,

View File

@ -11,7 +11,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors" caos_errs "github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id"
@ -159,8 +158,8 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st
if err != nil { if err != nil {
return nil, err return nil, err
} }
if sessionWriteModel.State != domain.SessionStateActive { if err = sessionWriteModel.CheckIsActive(); err != nil {
return nil, caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated") return nil, err
} }
resourceOwner, err := c.getResourceOwnerOfSessionUser(ctx, sessionWriteModel.UserID, sessionWriteModel.InstanceID) resourceOwner, err := c.getResourceOwnerOfSessionUser(ctx, sessionWriteModel.UserID, sessionWriteModel.InstanceID)
if err != nil { if err != nil {

View File

@ -119,7 +119,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
authRequestID: "V2_authRequestID", authRequestID: "V2_authRequestID",
}, },
res{ res{
err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated"), err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
}, },
}, },
{ {
@ -320,7 +320,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
authRequestID: "V2_authRequestID", authRequestID: "V2_authRequestID",
}, },
res{ res{
err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated"), err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"),
}, },
}, },
{ {

View File

@ -252,6 +252,17 @@ func (s *SessionCommands) ChangeMetadata(ctx context.Context, metadata map[strin
} }
} }
func (s *SessionCommands) SetLifetime(ctx context.Context, lifetime time.Duration) error {
if lifetime < 0 {
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-asEG4", "Errors.Session.PositiveLifetime")
}
if lifetime == 0 {
return nil
}
s.eventCommands = append(s.eventCommands, session.NewLifetimeSetEvent(ctx, s.sessionWriteModel.aggregate, lifetime))
return nil
}
func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteModel, error) { func (s *SessionCommands) gethumanWriteModel(ctx context.Context) (*HumanWriteModel, error) {
if s.sessionWriteModel.UserID == "" { if s.sessionWriteModel.UserID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing") return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing")
@ -280,7 +291,7 @@ func (s *SessionCommands) commands(ctx context.Context) (string, []eventstore.Co
return token, s.eventCommands, nil return token, s.eventCommands, nil
} }
func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte, userAgent *domain.UserAgent) (set *SessionChanged, err error) { func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte, userAgent *domain.UserAgent, lifetime time.Duration) (set *SessionChanged, err error) {
sessionID, err := c.idGenerator.Next() sessionID, err := c.idGenerator.Next()
if err != nil { if err != nil {
return nil, err return nil, err
@ -292,10 +303,10 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, met
} }
cmd := c.NewSessionCommands(cmds, sessionWriteModel) cmd := c.NewSessionCommands(cmds, sessionWriteModel)
cmd.Start(ctx, userAgent) cmd.Start(ctx, userAgent)
return c.updateSession(ctx, cmd, metadata) return c.updateSession(ctx, cmd, metadata, lifetime)
} }
func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken string, cmds []SessionCommand, metadata map[string][]byte) (set *SessionChanged, err error) { func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken string, cmds []SessionCommand, metadata map[string][]byte, lifetime time.Duration) (set *SessionChanged, err error) {
sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID) sessionWriteModel := NewSessionWriteModel(sessionID, authz.GetCtxData(ctx).OrgID)
err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel)
if err != nil { if err != nil {
@ -305,7 +316,7 @@ func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken st
return nil, err return nil, err
} }
cmd := c.NewSessionCommands(cmds, sessionWriteModel) cmd := c.NewSessionCommands(cmds, sessionWriteModel)
return c.updateSession(ctx, cmd, metadata) return c.updateSession(ctx, cmd, metadata, lifetime)
} }
func (c *Commands) TerminateSession(ctx context.Context, sessionID string, sessionToken string) (*domain.ObjectDetails, error) { func (c *Commands) TerminateSession(ctx context.Context, sessionID string, sessionToken string) (*domain.ObjectDetails, error) {
@ -326,7 +337,7 @@ func (c *Commands) terminateSession(ctx context.Context, sessionID, sessionToken
return nil, err return nil, err
} }
} }
if sessionWriteModel.State != domain.SessionStateActive { if sessionWriteModel.CheckIsActive() != nil {
return writeModelToObjectDetails(&sessionWriteModel.WriteModel), nil return writeModelToObjectDetails(&sessionWriteModel.WriteModel), nil
} }
terminate := session.NewTerminateEvent(ctx, &session.NewAggregate(sessionWriteModel.AggregateID, sessionWriteModel.ResourceOwner).Aggregate) terminate := session.NewTerminateEvent(ctx, &session.NewAggregate(sessionWriteModel.AggregateID, sessionWriteModel.ResourceOwner).Aggregate)
@ -342,15 +353,19 @@ func (c *Commands) terminateSession(ctx context.Context, sessionID, sessionToken
} }
// updateSession execute the [SessionCommands] where new events will be created and as well as for metadata (changes) // updateSession execute the [SessionCommands] where new events will be created and as well as for metadata (changes)
func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, metadata map[string][]byte) (set *SessionChanged, err error) { func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, metadata map[string][]byte, lifetime time.Duration) (set *SessionChanged, err error) {
if checks.sessionWriteModel.State == domain.SessionStateTerminated { if err = checks.sessionWriteModel.CheckNotInvalidated(); err != nil {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMAND-SAjeh", "Errors.Session.Terminated") return nil, err
} }
if err := checks.Exec(ctx); err != nil { if err := checks.Exec(ctx); err != nil {
// TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807 // TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807
return nil, err return nil, err
} }
checks.ChangeMetadata(ctx, metadata) checks.ChangeMetadata(ctx, metadata)
err = checks.SetLifetime(ctx, lifetime)
if err != nil {
return nil, err
}
sessionToken, cmds, err := checks.commands(ctx) sessionToken, cmds, err := checks.commands(ctx)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -5,6 +5,7 @@ import (
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/session" "github.com/zitadel/zitadel/internal/repository/session"
) )
@ -48,6 +49,7 @@ type SessionWriteModel struct {
WebAuthNUserVerified bool WebAuthNUserVerified bool
Metadata map[string][]byte Metadata map[string][]byte
State domain.SessionState State domain.SessionState
Expiration time.Time
WebAuthNChallenge *WebAuthNChallengeModel WebAuthNChallenge *WebAuthNChallengeModel
OTPSMSCodeChallenge *OTPCode OTPSMSCodeChallenge *OTPCode
@ -94,6 +96,8 @@ func (wm *SessionWriteModel) Reduce() error {
wm.reduceOTPEmailChecked(e) wm.reduceOTPEmailChecked(e)
case *session.TokenSetEvent: case *session.TokenSetEvent:
wm.reduceTokenSet(e) wm.reduceTokenSet(e)
case *session.LifetimeSetEvent:
wm.reduceLifetimeSet(e)
case *session.TerminateEvent: case *session.TerminateEvent:
wm.reduceTerminate() wm.reduceTerminate()
} }
@ -120,6 +124,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
session.OTPEmailCheckedType, session.OTPEmailCheckedType,
session.TokenSetType, session.TokenSetType,
session.MetadataSetType, session.MetadataSetType,
session.LifetimeSetType,
session.TerminateType, session.TerminateType,
). ).
Builder() Builder()
@ -196,6 +201,10 @@ func (wm *SessionWriteModel) reduceTokenSet(e *session.TokenSetEvent) {
wm.TokenID = e.TokenID wm.TokenID = e.TokenID
} }
func (wm *SessionWriteModel) reduceLifetimeSet(e *session.LifetimeSetEvent) {
wm.Expiration = e.CreationDate().Add(e.Lifetime)
}
func (wm *SessionWriteModel) reduceTerminate() { func (wm *SessionWriteModel) reduceTerminate() {
wm.State = domain.SessionStateTerminated wm.State = domain.SessionStateTerminated
} }
@ -245,3 +254,23 @@ func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType {
} }
return types return types
} }
// CheckNotInvalidated checks that the session was not invalidated either manually ([session.TerminateType])
// or automatically (expired).
func (wm *SessionWriteModel) CheckNotInvalidated() error {
if wm.State == domain.SessionStateTerminated {
return errors.ThrowPreconditionFailed(nil, "COMMAND-Hewfq", "Errors.Session.Terminated")
}
if !wm.Expiration.IsZero() && wm.Expiration.Before(time.Now()) {
return errors.ThrowPreconditionFailed(nil, "COMMAND-Hkl3d", "Errors.Session.Expired")
}
return nil
}
// CheckIsActive checks that the session was not invalidated ([CheckNotInvalidated]) and actually already exists.
func (wm *SessionWriteModel) CheckIsActive() error {
if wm.State == domain.SessionStateUnspecified {
return errors.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting")
}
return wm.CheckNotInvalidated()
}

View File

@ -152,6 +152,7 @@ func TestCommands_CreateSession(t *testing.T) {
checks []SessionCommand checks []SessionCommand
metadata map[string][]byte metadata map[string][]byte
userAgent *domain.UserAgent userAgent *domain.UserAgent
lifetime time.Duration
} }
type res struct { type res struct {
want *SessionChanged want *SessionChanged
@ -192,6 +193,33 @@ func TestCommands_CreateSession(t *testing.T) {
err: caos_errs.ThrowInternal(nil, "id", "filter failed"), err: caos_errs.ThrowInternal(nil, "id", "filter failed"),
}, },
}, },
{
"negative lifetime",
fields{
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
tokenCreator: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
},
args{
ctx: authz.NewMockContext("", "org1", ""),
userAgent: &domain.UserAgent{
FingerprintID: gu.Ptr("fp1"),
IP: net.ParseIP("1.2.3.4"),
Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}},
},
lifetime: -10 * time.Minute,
},
[]expect{
expectFilter(),
},
res{
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-asEG4", "Errors.Session.PositiveLifetime"),
},
},
{ {
"empty session", "empty session",
fields{ fields{
@ -210,6 +238,7 @@ func TestCommands_CreateSession(t *testing.T) {
Description: gu.Ptr("firefox"), Description: gu.Ptr("firefox"),
Header: http.Header{"foo": []string{"bar"}}, Header: http.Header{"foo": []string{"bar"}},
}, },
lifetime: 10 * time.Minute,
}, },
[]expect{ []expect{
expectFilter(), expectFilter(),
@ -223,6 +252,7 @@ func TestCommands_CreateSession(t *testing.T) {
Header: http.Header{"foo": []string{"bar"}}, Header: http.Header{"foo": []string{"bar"}},
}, },
), ),
session.NewLifetimeSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, 10*time.Minute),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID", "tokenID",
), ),
@ -245,7 +275,7 @@ func TestCommands_CreateSession(t *testing.T) {
idGenerator: tt.fields.idGenerator, idGenerator: tt.fields.idGenerator,
sessionTokenCreator: tt.fields.tokenCreator, sessionTokenCreator: tt.fields.tokenCreator,
} }
got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata, tt.args.userAgent) got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata, tt.args.userAgent, tt.args.lifetime)
require.ErrorIs(t, err, tt.res.err) require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got) assert.Equal(t, tt.res.want, got)
}) })
@ -263,6 +293,7 @@ func TestCommands_UpdateSession(t *testing.T) {
sessionToken string sessionToken string
checks []SessionCommand checks []SessionCommand
metadata map[string][]byte metadata map[string][]byte
lifetime time.Duration
} }
type res struct { type res struct {
want *SessionChanged want *SessionChanged
@ -368,7 +399,7 @@ func TestCommands_UpdateSession(t *testing.T) {
eventstore: tt.fields.eventstore, eventstore: tt.fields.eventstore,
sessionTokenVerifier: tt.fields.tokenVerifier, sessionTokenVerifier: tt.fields.tokenVerifier,
} }
got, err := c.UpdateSession(tt.args.ctx, tt.args.sessionID, tt.args.sessionToken, tt.args.checks, tt.args.metadata) got, err := c.UpdateSession(tt.args.ctx, tt.args.sessionID, tt.args.sessionToken, tt.args.checks, tt.args.metadata, tt.args.lifetime)
require.ErrorIs(t, err, tt.res.err) require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got) assert.Equal(t, tt.res.want, got)
}) })
@ -397,6 +428,7 @@ func TestCommands_updateSession(t *testing.T) {
ctx context.Context ctx context.Context
checks *SessionCommands checks *SessionCommands
metadata map[string][]byte metadata map[string][]byte
lifetime time.Duration
} }
type res struct { type res struct {
want *SessionChanged want *SessionChanged
@ -420,7 +452,7 @@ func TestCommands_updateSession(t *testing.T) {
}, },
}, },
res{ res{
err: caos_errs.ThrowPreconditionFailed(nil, "COMAND-SAjeh", "Errors.Session.Terminated"), err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Hewfq", "Errors.Session.Terminated"),
}, },
}, },
{ {
@ -465,6 +497,73 @@ func TestCommands_updateSession(t *testing.T) {
}, },
}, },
}, },
{
"negative lifetime",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
sessionCommands: []SessionCommand{},
eventstore: eventstoreExpect(t),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
now: func() time.Time {
return testNow
},
},
lifetime: -10 * time.Minute,
},
res{
err: caos_errs.ThrowInvalidArgument(nil, "COMMAND-asEG4", "Errors.Session.PositiveLifetime"),
},
},
{
"lifetime set",
fields{
eventstore: eventstoreExpect(t,
expectPush(
session.NewLifetimeSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
10*time.Minute,
),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID",
),
),
),
},
args{
ctx: context.Background(),
checks: &SessionCommands{
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
sessionCommands: []SessionCommand{},
eventstore: eventstoreExpect(t),
createToken: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
now: func() time.Time {
return testNow
},
},
lifetime: 10 * time.Minute,
},
res{
want: &SessionChanged{
ObjectDetails: &domain.ObjectDetails{
ResourceOwner: "org1",
},
ID: "sessionID",
NewToken: "token",
},
},
},
{ {
"set user, password, metadata and token", "set user, password, metadata and token",
fields{ fields{
@ -721,7 +820,7 @@ func TestCommands_updateSession(t *testing.T) {
c := &Commands{ c := &Commands{
eventstore: tt.fields.eventstore, eventstore: tt.fields.eventstore,
} }
got, err := c.updateSession(tt.args.ctx, tt.args.checks, tt.args.metadata) got, err := c.updateSession(tt.args.ctx, tt.args.checks, tt.args.metadata, tt.args.lifetime)
require.ErrorIs(t, err, tt.res.err) require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got) assert.Equal(t, tt.res.want, got)
}) })

View File

@ -13,6 +13,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/text/language" "golang.org/x/text/language"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
@ -324,7 +325,15 @@ func (s *Tester) CreateSuccessfulSAMLIntent(t *testing.T, idpID, userID, idpUser
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
} }
func (s *Tester) CreateVerfiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) { func (s *Tester) CreateVerifiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) {
return s.CreateVerifiedWebAuthNSessionWithLifetime(t, ctx, userID, 0)
}
func (s *Tester) CreateVerifiedWebAuthNSessionWithLifetime(t *testing.T, ctx context.Context, userID string, lifetime time.Duration) (id, token string, start, change time.Time) {
var sessionLifetime *durationpb.Duration
if lifetime > 0 {
sessionLifetime = durationpb.New(lifetime)
}
createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{ createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{ Checks: &session.Checks{
User: &session.CheckUser{ User: &session.CheckUser{
@ -337,6 +346,7 @@ func (s *Tester) CreateVerfiedWebAuthNSession(t *testing.T, ctx context.Context,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
}, },
}, },
Lifetime: sessionLifetime,
}) })
require.NoError(t, err) require.NoError(t, err)

View File

@ -14,7 +14,7 @@ import (
) )
const ( const (
SessionsProjectionTable = "projections.sessions6" SessionsProjectionTable = "projections.sessions7"
SessionColumnID = "id" SessionColumnID = "id"
SessionColumnCreationDate = "creation_date" SessionColumnCreationDate = "creation_date"
@ -39,6 +39,7 @@ const (
SessionColumnUserAgentIP = "user_agent_ip" SessionColumnUserAgentIP = "user_agent_ip"
SessionColumnUserAgentDescription = "user_agent_description" SessionColumnUserAgentDescription = "user_agent_description"
SessionColumnUserAgentHeader = "user_agent_header" SessionColumnUserAgentHeader = "user_agent_header"
SessionColumnExpiration = "expiration"
) )
type sessionProjection struct{} type sessionProjection struct{}
@ -77,6 +78,7 @@ func (*sessionProjection) Init() *old_handler.Check {
handler.NewColumn(SessionColumnUserAgentIP, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(SessionColumnUserAgentIP, handler.ColumnTypeText, handler.Nullable()),
handler.NewColumn(SessionColumnUserAgentDescription, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(SessionColumnUserAgentDescription, handler.ColumnTypeText, handler.Nullable()),
handler.NewColumn(SessionColumnUserAgentHeader, handler.ColumnTypeJSONB, handler.Nullable()), handler.NewColumn(SessionColumnUserAgentHeader, handler.ColumnTypeJSONB, handler.Nullable()),
handler.NewColumn(SessionColumnExpiration, handler.ColumnTypeTimestamp, handler.Nullable()),
}, },
handler.NewPrimaryKey(SessionColumnInstanceID, SessionColumnID), handler.NewPrimaryKey(SessionColumnInstanceID, SessionColumnID),
handler.WithIndex(handler.NewIndex( handler.WithIndex(handler.NewIndex(
@ -132,6 +134,10 @@ func (p *sessionProjection) Reducers() []handler.AggregateReducer {
Event: session.MetadataSetType, Event: session.MetadataSetType,
Reduce: p.reduceMetadataSet, Reduce: p.reduceMetadataSet,
}, },
{
Event: session.LifetimeSetType,
Reduce: p.reduceLifetimeSet,
},
{ {
Event: session.TerminateType, Event: session.TerminateType,
Reduce: p.reduceSessionTerminated, Reduce: p.reduceSessionTerminated,
@ -376,6 +382,26 @@ func (p *sessionProjection) reduceMetadataSet(event eventstore.Event) (*handler.
), nil ), nil
} }
func (p *sessionProjection) reduceLifetimeSet(event eventstore.Event) (*handler.Statement, error) {
e, err := assertEvent[*session.LifetimeSetEvent](event)
if err != nil {
return nil, err
}
return handler.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
handler.NewCol(SessionColumnSequence, e.Sequence()),
handler.NewCol(SessionColumnExpiration, e.CreationDate().Add(e.Lifetime)),
},
[]handler.Condition{
handler.NewCond(SessionColumnID, e.Aggregate().ID),
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
},
), nil
}
func (p *sessionProjection) reduceSessionTerminated(event eventstore.Event) (*handler.Statement, error) { func (p *sessionProjection) reduceSessionTerminated(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*session.TerminateEvent) e, ok := event.(*session.TerminateEvent)
if !ok { if !ok {

View File

@ -51,7 +51,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "INSERT INTO projections.sessions6 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator, user_agent_fingerprint_id, user_agent_description, user_agent_ip, user_agent_header) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", expectedStmt: "INSERT INTO projections.sessions7 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator, user_agent_fingerprint_id, user_agent_description, user_agent_ip, user_agent_header) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -90,7 +90,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -122,7 +122,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -154,7 +154,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)", expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, webauthn_checked_at, webauthn_user_verified) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -186,7 +186,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -217,7 +217,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -248,7 +248,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -281,7 +281,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)", expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -296,6 +296,37 @@ func TestSessionProjection_reduces(t *testing.T) {
}, },
}, },
}, },
{
name: "instance reduceLifetimeSet",
args: args{
event: getEvent(testEvent(
session.MetadataSetType,
session.AggregateType,
[]byte(`{
"lifetime": 600000000000
}`),
), eventstore.GenericEventMapper[session.LifetimeSetEvent]),
},
reduce: (&sessionProjection{}).reduceLifetimeSet,
want: wantReduce{
aggregateType: eventstore.AggregateType("session"),
sequence: 15,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions7 SET (change_date, sequence, expiration) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
anyArg{},
"agg-id",
"instance-id",
},
},
},
},
},
},
{ {
name: "instance reduceSessionTerminated", name: "instance reduceSessionTerminated",
args: args{ args: args{
@ -312,7 +343,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "DELETE FROM projections.sessions6 WHERE (id = $1) AND (instance_id = $2)", expectedStmt: "DELETE FROM projections.sessions7 WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -339,7 +370,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "DELETE FROM projections.sessions6 WHERE (instance_id = $1)", expectedStmt: "DELETE FROM projections.sessions7 WHERE (instance_id = $1)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
}, },
@ -369,7 +400,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "UPDATE projections.sessions6 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)", expectedStmt: "UPDATE projections.sessions7 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
nil, nil,
"agg-id", "agg-id",

View File

@ -44,6 +44,7 @@ type Session struct {
OTPEmailFactor SessionOTPFactor OTPEmailFactor SessionOTPFactor
Metadata map[string][]byte Metadata map[string][]byte
UserAgent domain.UserAgent UserAgent domain.UserAgent
Expiration time.Time
} }
type SessionUserFactor struct { type SessionUserFactor struct {
@ -185,6 +186,10 @@ var (
name: projection.SessionColumnUserAgentHeader, name: projection.SessionColumnUserAgentHeader,
table: sessionsTable, table: sessionsTable,
} }
SessionColumnExpiration = Column{
name: projection.SessionColumnExpiration,
table: sessionsTable,
}
) )
func (q *Queries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (session *Session, err error) { func (q *Queries) SessionByID(ctx context.Context, shouldTriggerBulk bool, id, sessionToken string) (session *Session, err error) {
@ -290,6 +295,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
SessionColumnUserAgentIP.identifier(), SessionColumnUserAgentIP.identifier(),
SessionColumnUserAgentDescription.identifier(), SessionColumnUserAgentDescription.identifier(),
SessionColumnUserAgentHeader.identifier(), SessionColumnUserAgentHeader.identifier(),
SessionColumnExpiration.identifier(),
).From(sessionsTable.identifier()). ).From(sessionsTable.identifier()).
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
@ -314,6 +320,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
token sql.NullString token sql.NullString
userAgentIP sql.NullString userAgentIP sql.NullString
userAgentHeader database.Map[[]string] userAgentHeader database.Map[[]string]
expiration sql.NullTime
) )
err := row.Scan( err := row.Scan(
@ -342,6 +349,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&userAgentIP, &userAgentIP,
&session.UserAgent.Description, &session.UserAgent.Description,
&userAgentHeader, &userAgentHeader,
&expiration,
) )
if err != nil { if err != nil {
@ -365,10 +373,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time
session.Metadata = metadata session.Metadata = metadata
session.UserAgent.Header = http.Header(userAgentHeader) session.UserAgent.Header = http.Header(userAgentHeader)
if userAgentIP.Valid { if userAgentIP.Valid {
session.UserAgent.IP = net.ParseIP(userAgentIP.String) session.UserAgent.IP = net.ParseIP(userAgentIP.String)
} }
session.Expiration = expiration.Time
return session, token.String, nil return session, token.String, nil
} }
} }
@ -395,6 +403,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
SessionColumnOTPSMSCheckedAt.identifier(), SessionColumnOTPSMSCheckedAt.identifier(),
SessionColumnOTPEmailCheckedAt.identifier(), SessionColumnOTPEmailCheckedAt.identifier(),
SessionColumnMetadata.identifier(), SessionColumnMetadata.identifier(),
SessionColumnExpiration.identifier(),
countColumn.identifier(), countColumn.identifier(),
).From(sessionsTable.identifier()). ).From(sessionsTable.identifier()).
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
@ -420,6 +429,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
otpSMSCheckedAt sql.NullTime otpSMSCheckedAt sql.NullTime
otpEmailCheckedAt sql.NullTime otpEmailCheckedAt sql.NullTime
metadata database.Map[[]byte] metadata database.Map[[]byte]
expiration sql.NullTime
) )
err := rows.Scan( err := rows.Scan(
@ -443,6 +453,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
&otpSMSCheckedAt, &otpSMSCheckedAt,
&otpEmailCheckedAt, &otpEmailCheckedAt,
&metadata, &metadata,
&expiration,
&sessions.Count, &sessions.Count,
) )
@ -462,6 +473,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time
session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time
session.Metadata = metadata session.Metadata = metadata
session.Expiration = expiration.Time
sessions.Sessions = append(sessions.Sessions, session) sessions.Sessions = append(sessions.Sessions, session)
} }

View File

@ -20,61 +20,63 @@ import (
) )
var ( var (
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions7.id,` +
` projections.sessions6.creation_date,` + ` projections.sessions7.creation_date,` +
` projections.sessions6.change_date,` + ` projections.sessions7.change_date,` +
` projections.sessions6.sequence,` + ` projections.sessions7.sequence,` +
` projections.sessions6.state,` + ` projections.sessions7.state,` +
` projections.sessions6.resource_owner,` + ` projections.sessions7.resource_owner,` +
` projections.sessions6.creator,` + ` projections.sessions7.creator,` +
` projections.sessions6.user_id,` + ` projections.sessions7.user_id,` +
` projections.sessions6.user_checked_at,` + ` projections.sessions7.user_checked_at,` +
` projections.login_names2.login_name,` + ` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` + ` projections.users8_humans.display_name,` +
` projections.users8.resource_owner,` + ` projections.users8.resource_owner,` +
` projections.sessions6.password_checked_at,` + ` projections.sessions7.password_checked_at,` +
` projections.sessions6.intent_checked_at,` + ` projections.sessions7.intent_checked_at,` +
` projections.sessions6.webauthn_checked_at,` + ` projections.sessions7.webauthn_checked_at,` +
` projections.sessions6.webauthn_user_verified,` + ` projections.sessions7.webauthn_user_verified,` +
` projections.sessions6.totp_checked_at,` + ` projections.sessions7.totp_checked_at,` +
` projections.sessions6.otp_sms_checked_at,` + ` projections.sessions7.otp_sms_checked_at,` +
` projections.sessions6.otp_email_checked_at,` + ` projections.sessions7.otp_email_checked_at,` +
` projections.sessions6.metadata,` + ` projections.sessions7.metadata,` +
` projections.sessions6.token_id,` + ` projections.sessions7.token_id,` +
` projections.sessions6.user_agent_fingerprint_id,` + ` projections.sessions7.user_agent_fingerprint_id,` +
` projections.sessions6.user_agent_ip,` + ` projections.sessions7.user_agent_ip,` +
` projections.sessions6.user_agent_description,` + ` projections.sessions7.user_agent_description,` +
` projections.sessions6.user_agent_header` + ` projections.sessions7.user_agent_header,` +
` FROM projections.sessions6` + ` projections.sessions7.expiration` +
` LEFT JOIN projections.login_names2 ON projections.sessions6.user_id = projections.login_names2.user_id AND projections.sessions6.instance_id = projections.login_names2.instance_id` + ` FROM projections.sessions7` +
` LEFT JOIN projections.users8_humans ON projections.sessions6.user_id = projections.users8_humans.user_id AND projections.sessions6.instance_id = projections.users8_humans.instance_id` + ` LEFT JOIN projections.login_names2 ON projections.sessions7.user_id = projections.login_names2.user_id AND projections.sessions7.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + ` LEFT JOIN projections.users8_humans ON projections.sessions7.user_id = projections.users8_humans.user_id AND projections.sessions7.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions7.user_id = projections.users8.id AND projections.sessions7.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`) ` AS OF SYSTEM TIME '-1 ms'`)
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions7.id,` +
` projections.sessions6.creation_date,` + ` projections.sessions7.creation_date,` +
` projections.sessions6.change_date,` + ` projections.sessions7.change_date,` +
` projections.sessions6.sequence,` + ` projections.sessions7.sequence,` +
` projections.sessions6.state,` + ` projections.sessions7.state,` +
` projections.sessions6.resource_owner,` + ` projections.sessions7.resource_owner,` +
` projections.sessions6.creator,` + ` projections.sessions7.creator,` +
` projections.sessions6.user_id,` + ` projections.sessions7.user_id,` +
` projections.sessions6.user_checked_at,` + ` projections.sessions7.user_checked_at,` +
` projections.login_names2.login_name,` + ` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` + ` projections.users8_humans.display_name,` +
` projections.users8.resource_owner,` + ` projections.users8.resource_owner,` +
` projections.sessions6.password_checked_at,` + ` projections.sessions7.password_checked_at,` +
` projections.sessions6.intent_checked_at,` + ` projections.sessions7.intent_checked_at,` +
` projections.sessions6.webauthn_checked_at,` + ` projections.sessions7.webauthn_checked_at,` +
` projections.sessions6.webauthn_user_verified,` + ` projections.sessions7.webauthn_user_verified,` +
` projections.sessions6.totp_checked_at,` + ` projections.sessions7.totp_checked_at,` +
` projections.sessions6.otp_sms_checked_at,` + ` projections.sessions7.otp_sms_checked_at,` +
` projections.sessions6.otp_email_checked_at,` + ` projections.sessions7.otp_email_checked_at,` +
` projections.sessions6.metadata,` + ` projections.sessions7.metadata,` +
` projections.sessions7.expiration,` +
` COUNT(*) OVER ()` + ` COUNT(*) OVER ()` +
` FROM projections.sessions6` + ` FROM projections.sessions7` +
` LEFT JOIN projections.login_names2 ON projections.sessions6.user_id = projections.login_names2.user_id AND projections.sessions6.instance_id = projections.login_names2.instance_id` + ` LEFT JOIN projections.login_names2 ON projections.sessions7.user_id = projections.login_names2.user_id AND projections.sessions7.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions6.user_id = projections.users8_humans.user_id AND projections.sessions6.instance_id = projections.users8_humans.instance_id` + ` LEFT JOIN projections.users8_humans ON projections.sessions7.user_id = projections.users8_humans.user_id AND projections.sessions7.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + ` LEFT JOIN projections.users8 ON projections.sessions7.user_id = projections.users8.id AND projections.sessions7.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`) ` AS OF SYSTEM TIME '-1 ms'`)
sessionCols = []string{ sessionCols = []string{
@ -103,6 +105,7 @@ var (
"user_agent_ip", "user_agent_ip",
"user_agent_description", "user_agent_description",
"user_agent_header", "user_agent_header",
"expiration",
} }
sessionsCols = []string{ sessionsCols = []string{
@ -126,6 +129,7 @@ var (
"otp_sms_checked_at", "otp_sms_checked_at",
"otp_email_checked_at", "otp_email_checked_at",
"metadata", "metadata",
"expiration",
"count", "count",
} }
) )
@ -182,6 +186,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow, testNow,
testNow, testNow,
[]byte(`{"key": "dmFsdWU="}`), []byte(`{"key": "dmFsdWU="}`),
testNow,
}, },
}, },
), ),
@ -228,6 +233,7 @@ func Test_SessionsPrepare(t *testing.T) {
Metadata: map[string][]byte{ Metadata: map[string][]byte{
"key": []byte("value"), "key": []byte("value"),
}, },
Expiration: testNow,
}, },
}, },
}, },
@ -261,6 +267,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow, testNow,
testNow, testNow,
[]byte(`{"key": "dmFsdWU="}`), []byte(`{"key": "dmFsdWU="}`),
testNow,
}, },
{ {
"session-id2", "session-id2",
@ -283,6 +290,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow, testNow,
testNow, testNow,
[]byte(`{"key": "dmFsdWU="}`), []byte(`{"key": "dmFsdWU="}`),
testNow,
}, },
}, },
), ),
@ -329,6 +337,7 @@ func Test_SessionsPrepare(t *testing.T) {
Metadata: map[string][]byte{ Metadata: map[string][]byte{
"key": []byte("value"), "key": []byte("value"),
}, },
Expiration: testNow,
}, },
{ {
ID: "session-id2", ID: "session-id2",
@ -367,6 +376,7 @@ func Test_SessionsPrepare(t *testing.T) {
Metadata: map[string][]byte{ Metadata: map[string][]byte{
"key": []byte("value"), "key": []byte("value"),
}, },
Expiration: testNow,
}, },
}, },
}, },
@ -458,6 +468,7 @@ func Test_SessionPrepare(t *testing.T) {
"1.2.3.4", "1.2.3.4",
"agentDescription", "agentDescription",
[]byte(`{"foo":["foo","bar"]}`), []byte(`{"foo":["foo","bar"]}`),
testNow,
}, },
), ),
}, },
@ -504,6 +515,7 @@ func Test_SessionPrepare(t *testing.T) {
Description: gu.Ptr("agentDescription"), Description: gu.Ptr("agentDescription"),
Header: http.Header{"foo": []string{"foo", "bar"}}, Header: http.Header{"foo": []string{"foo", "bar"}},
}, },
Expiration: testNow,
}, },
}, },
{ {

View File

@ -18,5 +18,6 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, OTPEmailCheckedType, eventstore.GenericEventMapper[OTPEmailCheckedEvent]). RegisterFilterEventMapper(AggregateType, OTPEmailCheckedType, eventstore.GenericEventMapper[OTPEmailCheckedEvent]).
RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper). RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper). RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper).
RegisterFilterEventMapper(AggregateType, LifetimeSetType, eventstore.GenericEventMapper[LifetimeSetEvent]).
RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper) RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper)
} }

View File

@ -28,6 +28,7 @@ const (
OTPEmailCheckedType = sessionEventPrefix + "otp.email.checked" OTPEmailCheckedType = sessionEventPrefix + "otp.email.checked"
TokenSetType = sessionEventPrefix + "token.set" TokenSetType = sessionEventPrefix + "token.set"
MetadataSetType = sessionEventPrefix + "metadata.set" MetadataSetType = sessionEventPrefix + "metadata.set"
LifetimeSetType = sessionEventPrefix + "lifetime.set"
TerminateType = sessionEventPrefix + "terminated" TerminateType = sessionEventPrefix + "terminated"
) )
@ -607,6 +608,39 @@ func MetadataSetEventMapper(event eventstore.Event) (eventstore.Event, error) {
return added, nil return added, nil
} }
type LifetimeSetEvent struct {
eventstore.BaseEvent `json:"-"`
Lifetime time.Duration `json:"lifetime"`
}
func (e *LifetimeSetEvent) Payload() interface{} {
return e
}
func (e *LifetimeSetEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *LifetimeSetEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = *base
}
func NewLifetimeSetEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
lifetime time.Duration,
) *LifetimeSetEvent {
return &LifetimeSetEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
LifetimeSetType,
),
Lifetime: lifetime,
}
}
type TerminateEvent struct { type TerminateEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`
} }

View File

@ -509,6 +509,8 @@ Errors:
Session: Session:
NotExisting: Сесията не съществува NotExisting: Сесията не съществува
Terminated: Сесията вече е прекратена Terminated: Сесията вече е прекратена
Expired: Сесията е изтекла
PositiveLifetime: Животът на сесията не трябва да е по-малък от 0
Token: Token:
Invalid: Токенът на сесията е невалиден Invalid: Токенът на сесията е невалиден
WebAuthN: WebAuthN:

View File

@ -491,6 +491,8 @@ Errors:
Session: Session:
NotExisting: Session existiert nicht NotExisting: Session existiert nicht
Terminated: Session bereits beendet Terminated: Session bereits beendet
Expired: Session ist abgelaufen
PositiveLifetime: Session Lebensdauer darf nicht kleiner als 0 sein
Token: Token:
Invalid: Session Token ist ungültig Invalid: Session Token ist ungültig
WebAuthN: WebAuthN:

View File

@ -491,6 +491,8 @@ Errors:
Session: Session:
NotExisting: Session does not exist NotExisting: Session does not exist
Terminated: Session already terminated Terminated: Session already terminated
Expired: Session has expired
PositiveLifetime: Session lifetime must not be less than 0
Token: Token:
Invalid: Session Token is invalid Invalid: Session Token is invalid
WebAuthN: WebAuthN:

View File

@ -490,7 +490,9 @@ Errors:
ScanFailed: La consulta de uso de los segundos de ejecuciónde acciones ha fallado ScanFailed: La consulta de uso de los segundos de ejecuciónde acciones ha fallado
Session: Session:
NotExisting: La sesión no existe NotExisting: La sesión no existe
Terminated: Sesión ya terminada Terminated: La Sesión ya terminada
Expired: La sesión ha expirado
PositiveLifetime: La duración de la sesión no debe ser inferior a 0
Token: Token:
Invalid: El identificador de sesión no es válido Invalid: El identificador de sesión no es válido
WebAuthN: WebAuthN:

View File

@ -491,6 +491,8 @@ Errors:
Session: Session:
NotExisting: La session n'existe pas NotExisting: La session n'existe pas
Terminated: La session est déjà terminée Terminated: La session est déjà terminée
Expired: La session a expiré
PositiveLifetime: La durée de vie de la session ne doit pas être inférieure à 0
Token: Token:
Invalid: Le jeton de session n'est pas valide Invalid: Le jeton de session n'est pas valide
WebAuthN: WebAuthN:

View File

@ -491,7 +491,9 @@ Errors:
ScanFailed: La query dei secondi delle azioni utilizzate non è riuscita ScanFailed: La query dei secondi delle azioni utilizzate non è riuscita
Session: Session:
NotExisting: La sessione non esiste NotExisting: La sessione non esiste
Terminated: Sessione già terminata Terminated: La Sessione già terminata
Expired: La sessione è scaduta
PositiveLifetime: La durata della sessione non deve essere inferiore a 0
Token: Token:
Invalid: Il token della sessione non è valido Invalid: Il token della sessione non è valido
WebAuthN: WebAuthN:

View File

@ -480,6 +480,8 @@ Errors:
Session: Session:
NotExisting: セッションが存在しない NotExisting: セッションが存在しない
Terminated: セッションはすでに終了しています Terminated: セッションはすでに終了しています
Expired: セッションの有効期限が切れました
PositiveLifetime: セッションの有効期間は 0 未満であってはなりません
Token: Token:
Invalid: セッショントークンが無効です Invalid: セッショントークンが無効です
WebAuthN: WebAuthN:

View File

@ -491,6 +491,8 @@ Errors:
Session: Session:
NotExisting: Сесијата не постои NotExisting: Сесијата не постои
Terminated: Сесијата е веќе завршена Terminated: Сесијата е веќе завршена
Expired: Сесијата истече
PositiveLifetime: Времетраењето на сесијата не смее да биде помало од 0
Token: Token:
Invalid: Токенот за сесија е невалиден Invalid: Токенот за сесија е невалиден
WebAuthN: WebAuthN:

View File

@ -491,6 +491,8 @@ Errors:
Session: Session:
NotExisting: Sesja nie istnieje NotExisting: Sesja nie istnieje
Terminated: Sesja już zakończona Terminated: Sesja już zakończona
Expired: Sesja wygasła
PositiveLifetime: Czas życia sesji nie może być krótszy niż 0
Token: Token:
Invalid: Token sesji jest nieprawidłowy Invalid: Token sesji jest nieprawidłowy
WebAuthN: WebAuthN:

View File

@ -489,6 +489,8 @@ Errors:
Session: Session:
NotExisting: A sessão não existe NotExisting: A sessão não existe
Terminated: A sessão já foi encerrada Terminated: A sessão já foi encerrada
Expired: A Sessão expirou
PositiveLifetime: O tempo de vida da sessão não deve ser inferior a 0
Token: Token:
Invalid: O token da sessão é inválido Invalid: O token da sessão é inválido
WebAuthN: WebAuthN:

View File

@ -491,6 +491,8 @@ Errors:
Session: Session:
NotExisting: 会话不存在 NotExisting: 会话不存在
Terminated: 会话已经终止 Terminated: 会话已经终止
Expired: 会话已过期
PositiveLifetime: 会话生存期不得小于 0
Token: Token:
Invalid: 会话令牌是无效的 Invalid: 会话令牌是无效的
WebAuthN: WebAuthN:

View File

@ -40,6 +40,11 @@ message Session {
} }
]; ];
UserAgent user_agent = 7; UserAgent user_agent = 7;
optional google.protobuf.Timestamp expiration_date = 8 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"time the session will be automatically invalidated\"";
}
];
} }
message Factors { message Factors {

View File

@ -10,6 +10,7 @@ import "zitadel/session/v2beta/session.proto";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/api/field_behavior.proto"; import "google/api/field_behavior.proto";
import "google/protobuf/struct.proto"; import "google/protobuf/struct.proto";
import "google/protobuf/duration.proto";
import "protoc-gen-openapiv2/options/annotations.proto"; import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto"; import "validate/validate.proto";
@ -275,6 +276,12 @@ message CreateSessionRequest{
]; ];
RequestChallenges challenges = 3; RequestChallenges challenges = 3;
UserAgent user_agent = 4; UserAgent user_agent = 4;
optional google.protobuf.Duration lifetime = 5 [
(validate.rules).duration = {gt: {seconds: 0}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"duration after which the session will be automatically invalidated\"";
}
];
} }
message CreateSessionResponse{ message CreateSessionResponse{
@ -322,6 +329,12 @@ message SetSessionRequest{
} }
]; ];
RequestChallenges challenges = 5; RequestChallenges challenges = 5;
optional google.protobuf.Duration lifetime = 6 [
(validate.rules).duration = {gt: {seconds: 0}},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"duration after which the session will be automatically invalidated\"";
}
];
} }
message SetSessionResponse{ message SetSessionResponse{