diff --git a/internal/api/grpc/session/v2/session.go b/internal/api/grpc/session/v2/session.go index c766b3ef1..ec6c06a4b 100644 --- a/internal/api/grpc/session/v2/session.go +++ b/internal/api/grpc/session/v2/session.go @@ -4,6 +4,7 @@ import ( "context" "net" "net/http" + "time" "google.golang.org/protobuf/types/known/structpb" "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) { - checks, metadata, userAgent, err := s.createSessionRequestToCommand(ctx, req) + checks, metadata, userAgent, lifetime, err := s.createSessionRequestToCommand(ctx, req) if err != nil { return nil, err } @@ -55,7 +56,7 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe 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 { return nil, err } @@ -78,7 +79,7 @@ func (s *Server) SetSession(ctx context.Context, req *session.SetSessionRequest) 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 { return nil, err } @@ -113,13 +114,14 @@ func sessionsToPb(sessions []*query.Session) []*session.Session { func sessionToPb(s *query.Session) *session.Session { return &session.Session{ - Id: s.ID, - CreationDate: timestamppb.New(s.CreationDate), - ChangeDate: timestamppb.New(s.ChangeDate), - Sequence: s.Sequence, - Factors: factorsToPb(s), - Metadata: s.Metadata, - UserAgent: userAgentToPb(s.UserAgent), + Id: s.ID, + CreationDate: timestamppb.New(s.CreationDate), + ChangeDate: timestamppb.New(s.ChangeDate), + Sequence: s.Sequence, + Factors: factorsToPb(s), + Metadata: s.Metadata, + UserAgent: userAgentToPb(s.UserAgent), + ExpirationDate: expirationToPb(s.Expiration), } } @@ -147,6 +149,13 @@ func userAgentToPb(ua domain.UserAgent) *session.UserAgent { 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 { user := userFactorToPb(s.UserFactor) if user == nil { @@ -268,12 +277,12 @@ func idsQueryToQuery(q *session.IDsQuery) (query.SearchQuery, error) { 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) 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 { diff --git a/internal/api/grpc/session/v2/session_integration_test.go b/internal/api/grpc/session/v2/session_integration_test.go index 9aba59ee3..abd49ac8c 100644 --- a/internal/api/grpc/session/v2/session_integration_test.go +++ b/internal/api/grpc/session/v2/session_integration_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" "github.com/zitadel/zitadel/internal/integration" 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() require.NotEmpty(t, id) 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()) { 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) return s @@ -137,12 +143,13 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, func TestServer_CreateSession(t *testing.T) { tests := []struct { - name string - req *session.CreateSessionRequest - want *session.CreateSessionResponse - wantErr bool - wantFactors []wantFactor - wantUserAgent *session.UserAgent + name string + req *session.CreateSessionRequest + want *session.CreateSessionResponse + wantErr bool + wantFactors []wantFactor + wantUserAgent *session.UserAgent + wantExpirationWindow time.Duration }{ { 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", req: &session.CreateSessionRequest{ @@ -253,7 +281,7 @@ func TestServer_CreateSession(t *testing.T) { require.NoError(t, err) 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) - 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) require.NoError(t, err) @@ -292,7 +320,7 @@ func TestServer_CreateSession_webauthn(t *testing.T) { }, }) 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) { @@ -308,7 +336,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { }, }) require.NoError(t, err) - verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil) + verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, nil, 0) intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, User.GetUserId(), "id") updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{ @@ -322,7 +350,7 @@ func TestServer_CreateSession_successfulIntent(t *testing.T) { }, }) 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) { @@ -338,7 +366,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { }, }) 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" intentID, token, _, _ := Tester.CreateSuccessfulOAuthIntent(t, idpID, "", idpUserID) @@ -365,7 +393,7 @@ func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) { }, }) 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) { @@ -381,7 +409,7 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) { }, }) 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) _, err = Client.SetSession(CTX, &session.SetSessionRequest{ @@ -433,7 +461,7 @@ func TestServer_SetSession_flow(t *testing.T) { createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{}) require.NoError(t, err) 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) { resp, err := Client.SetSession(CTX, &session.SetSessionRequest{ @@ -449,7 +477,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) 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) { @@ -464,7 +492,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) 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() 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) 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) @@ -508,7 +536,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) 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() 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) 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) 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) { @@ -556,7 +584,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) 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() otp := resp.GetChallenges().GetOtpSms() @@ -573,7 +601,7 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) 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) { @@ -587,7 +615,7 @@ func TestServer_SetSession_flow(t *testing.T) { }, }) 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() otp := resp.GetChallenges().GetOtpEmail() @@ -604,10 +632,34 @@ func TestServer_SetSession_flow(t *testing.T) { }) require.NoError(t, err) 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) { // create new, empty session 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) { - id, token, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, User.GetUserId()) + id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId()) ctx := Tester.WithAuthorizationToken(context.Background(), token) 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) { - id, token, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, User.GetUserId()) + id, token, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, User.GetUserId()) // test session token works 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}) 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) +} diff --git a/internal/api/grpc/session/v2/session_test.go b/internal/api/grpc/session/v2/session_test.go index 8422b675b..0f243bab2 100644 --- a/internal/api/grpc/session/v2/session_test.go +++ b/internal/api/grpc/session/v2/session_test.go @@ -27,7 +27,7 @@ func Test_sessionsToPb(t *testing.T) { past := now.Add(-time.Hour) sessions := []*query.Session{ - { // no factor, with user agent + { // no factor, with user agent and expiration ID: "999", CreationDate: now, ChangeDate: now, @@ -42,6 +42,7 @@ func Test_sessionsToPb(t *testing.T) { IP: net.IPv4(1, 2, 3, 4), Header: http.Header{"foo": []string{"foo", "bar"}}, }, + Expiration: now, }, { // user factor ID: "999", @@ -124,7 +125,7 @@ func Test_sessionsToPb(t *testing.T) { } want := []*session.Session{ - { // no factor, with user agent + { // no factor, with user agent and expiration Id: "999", CreationDate: timestamppb.New(now), ChangeDate: timestamppb.New(now), @@ -139,6 +140,7 @@ func Test_sessionsToPb(t *testing.T) { "foo": {Values: []string{"foo", "bar"}}, }, }, + ExpirationDate: timestamppb.New(now), }, { // user factor Id: "999", diff --git a/internal/api/grpc/user/v2/otp_integration_test.go b/internal/api/grpc/user/v2/otp_integration_test.go index bf205ac87..cad0a2924 100644 --- a/internal/api/grpc/user/v2/otp_integration_test.go +++ b/internal/api/grpc/user/v2/otp_integration_test.go @@ -16,13 +16,13 @@ import ( func TestServer_AddOTPSMS(t *testing.T) { userID := Tester.CreateHumanUser(CTX).GetUserId() 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 /* userIDPhone := Tester.CreateHumanUser(CTX).GetUserId() Tester.RegisterUserPasskey(CTX, userIDPhone) - _, sessionTokenPhone, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userIDPhone) + _, sessionTokenPhone, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userIDPhone) */ type args struct { ctx context.Context @@ -99,7 +99,7 @@ func TestServer_RemoveOTPSMS(t *testing.T) { /* userID := Tester.CreateHumanUser(CTX).GetUserId() Tester.RegisterUserPasskey(CTX, userID) - _, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) */ type args struct { @@ -157,7 +157,7 @@ func TestServer_RemoveOTPSMS(t *testing.T) { func TestServer_AddOTPEmail(t *testing.T) { userID := Tester.CreateHumanUser(CTX).GetUserId() Tester.RegisterUserPasskey(CTX, userID) - _, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) userVerified := Tester.CreateHumanUser(CTX) _, err := Tester.Client.UserV2.VerifyEmail(CTX, &user.VerifyEmailRequest{ @@ -166,7 +166,7 @@ func TestServer_AddOTPEmail(t *testing.T) { }) require.NoError(t, err) Tester.RegisterUserPasskey(CTX, userVerified.GetUserId()) - _, sessionTokenVerified, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userVerified.GetUserId()) + _, sessionTokenVerified, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userVerified.GetUserId()) type args struct { ctx context.Context @@ -238,11 +238,11 @@ func TestServer_AddOTPEmail(t *testing.T) { func TestServer_RemoveOTPEmail(t *testing.T) { userID := Tester.CreateHumanUser(CTX).GetUserId() Tester.RegisterUserPasskey(CTX, userID) - _, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID) + _, sessionToken, _, _ := Tester.CreateVerifiedWebAuthNSession(t, CTX, userID) userVerified := Tester.CreateHumanUser(CTX) 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) _, err := Tester.Client.UserV2.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{ UserId: userVerified.GetUserId(), diff --git a/internal/api/oidc/auth_request_integration_test.go b/internal/api/oidc/auth_request_integration_test.go index 5b013fe86..83762e338 100644 --- a/internal/api/oidc/auth_request_integration_test.go +++ b/internal/api/oidc/auth_request_integration_test.go @@ -36,7 +36,7 @@ func TestOPStorage_CreateAuthRequest(t *testing.T) { func TestOPStorage_CreateAccessToken_code(t *testing.T) { clientID := createClient(t) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ @@ -75,7 +75,7 @@ func TestOPStorage_CreateAccessToken_code(t *testing.T) { func TestOPStorage_CreateAccessToken_implicit(t *testing.T) { clientID := createImplicitClient(t) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ @@ -125,7 +125,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) { func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) { clientID := createClient(t) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ @@ -150,7 +150,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) { provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ @@ -186,7 +186,7 @@ func TestOPStorage_RevokeToken_access_token(t *testing.T) { provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) 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{ AuthRequestId: authRequestID, 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) require.NoError(t, err) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ @@ -266,7 +266,7 @@ func TestOPStorage_RevokeToken_refresh_token(t *testing.T) { provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) 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{ AuthRequestId: authRequestID, 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) require.NoError(t, err) 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{ AuthRequestId: authRequestID, 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) { clientID := createClient(t) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ @@ -376,7 +376,7 @@ func TestOPStorage_TerminateSession(t *testing.T) { provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ @@ -413,7 +413,7 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) { provider, err := Tester.CreateRelyingParty(CTX, clientID, redirectURI) require.NoError(t, err) 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{ AuthRequestId: authRequestID, 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) require.NoError(t, err) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ diff --git a/internal/api/oidc/client_integration_test.go b/internal/api/oidc/client_integration_test.go index 8a7f58a44..0165051f0 100644 --- a/internal/api/oidc/client_integration_test.go +++ b/internal/api/oidc/client_integration_test.go @@ -21,7 +21,7 @@ import ( func TestOPStorage_SetUserinfoFromToken(t *testing.T) { clientID := createClient(t) 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{ AuthRequestId: authRequestID, 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} 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ diff --git a/internal/api/oidc/oidc_integration_test.go b/internal/api/oidc/oidc_integration_test.go index 7738e3d61..e1531fff3 100644 --- a/internal/api/oidc/oidc_integration_test.go +++ b/internal/api/oidc/oidc_integration_test.go @@ -57,7 +57,7 @@ func TestMain(m *testing.M) { func Test_ZITADEL_API_missing_audience_scope(t *testing.T) { clientID := createClient(t) 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{ AuthRequestId: authRequestID, 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) { clientID := createClient(t) 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{ AuthRequestId: authRequestID, 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) { clientID := createClient(t) 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{ AuthRequestId: authRequestID, 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) require.NoError(t, err) 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{ AuthRequestId: authRequestID, CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 42a9ba350..bcc4cfd34 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -153,6 +153,9 @@ func (repo *TokenVerifierRepo) verifySessionToken(ctx context.Context, sessionID if err != nil { 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 { return "", "", "", err } diff --git a/internal/command/auth_request.go b/internal/command/auth_request.go index bbcee503c..e54f0d65f 100644 --- a/internal/command/auth_request.go +++ b/internal/command/auth_request.go @@ -95,8 +95,8 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, if err != nil { return nil, nil, err } - if sessionWriteModel.State == domain.SessionStateUnspecified { - return nil, nil, errors.ThrowNotFound(nil, "COMMAND-x0099887", "Errors.Session.NotExisting") + if err = sessionWriteModel.CheckIsActive(); err != nil { + return nil, nil, err } if err := c.sessionPermission(ctx, sessionWriteModel, sessionToken, domain.PermissionSessionWrite); err != nil { return nil, nil, err diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 183e1b57c..2a390a922 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -327,7 +327,67 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { sessionID: "sessionID", }, 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, testNow), ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + 2*time.Minute), + ), ), expectPush( 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, testNow), ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, + 2*time.Minute), + ), ), expectPush( authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go index d990d0053..bcefbefd0 100644 --- a/internal/command/oidc_session.go +++ b/internal/command/oidc_session.go @@ -11,7 +11,6 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" - "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/id" @@ -159,8 +158,8 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st if err != nil { return nil, err } - if sessionWriteModel.State != domain.SessionStateActive { - return nil, caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated") + if err = sessionWriteModel.CheckIsActive(); err != nil { + return nil, err } resourceOwner, err := c.getResourceOwnerOfSessionUser(ctx, sessionWriteModel.UserID, sessionWriteModel.InstanceID) if err != nil { diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index a517814e0..7a8e7b402 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -119,7 +119,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) { authRequestID: "V2_authRequestID", }, 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", }, res{ - err: caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated"), + err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Flk38", "Errors.Session.NotExisting"), }, }, { diff --git a/internal/command/session.go b/internal/command/session.go index 045d7e957..377cc9917 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -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) { if s.sessionWriteModel.UserID == "" { 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 } -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() if err != nil { return nil, err @@ -292,10 +303,10 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, met } cmd := c.NewSessionCommands(cmds, sessionWriteModel) 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) err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel) if err != nil { @@ -305,7 +316,7 @@ func (c *Commands) UpdateSession(ctx context.Context, sessionID, sessionToken st return nil, err } 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) { @@ -326,7 +337,7 @@ func (c *Commands) terminateSession(ctx context.Context, sessionID, sessionToken return nil, err } } - if sessionWriteModel.State != domain.SessionStateActive { + if sessionWriteModel.CheckIsActive() != nil { return writeModelToObjectDetails(&sessionWriteModel.WriteModel), nil } 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) -func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, metadata map[string][]byte) (set *SessionChanged, err error) { - if checks.sessionWriteModel.State == domain.SessionStateTerminated { - return nil, caos_errs.ThrowPreconditionFailed(nil, "COMAND-SAjeh", "Errors.Session.Terminated") +func (c *Commands) updateSession(ctx context.Context, checks *SessionCommands, metadata map[string][]byte, lifetime time.Duration) (set *SessionChanged, err error) { + if err = checks.sessionWriteModel.CheckNotInvalidated(); err != nil { + return nil, err } if err := checks.Exec(ctx); err != nil { // TODO: how to handle failed checks (e.g. pw wrong) https://github.com/zitadel/zitadel/issues/5807 return nil, err } checks.ChangeMetadata(ctx, metadata) + err = checks.SetLifetime(ctx, lifetime) + if err != nil { + return nil, err + } sessionToken, cmds, err := checks.commands(ctx) if err != nil { return nil, err diff --git a/internal/command/session_model.go b/internal/command/session_model.go index 0ae23bac7..4df4b6b55 100644 --- a/internal/command/session_model.go +++ b/internal/command/session_model.go @@ -5,6 +5,7 @@ import ( "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/session" ) @@ -48,6 +49,7 @@ type SessionWriteModel struct { WebAuthNUserVerified bool Metadata map[string][]byte State domain.SessionState + Expiration time.Time WebAuthNChallenge *WebAuthNChallengeModel OTPSMSCodeChallenge *OTPCode @@ -94,6 +96,8 @@ func (wm *SessionWriteModel) Reduce() error { wm.reduceOTPEmailChecked(e) case *session.TokenSetEvent: wm.reduceTokenSet(e) + case *session.LifetimeSetEvent: + wm.reduceLifetimeSet(e) case *session.TerminateEvent: wm.reduceTerminate() } @@ -120,6 +124,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder { session.OTPEmailCheckedType, session.TokenSetType, session.MetadataSetType, + session.LifetimeSetType, session.TerminateType, ). Builder() @@ -196,6 +201,10 @@ func (wm *SessionWriteModel) reduceTokenSet(e *session.TokenSetEvent) { wm.TokenID = e.TokenID } +func (wm *SessionWriteModel) reduceLifetimeSet(e *session.LifetimeSetEvent) { + wm.Expiration = e.CreationDate().Add(e.Lifetime) +} + func (wm *SessionWriteModel) reduceTerminate() { wm.State = domain.SessionStateTerminated } @@ -245,3 +254,23 @@ func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType { } 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() +} diff --git a/internal/command/session_test.go b/internal/command/session_test.go index 8892c635b..429e37682 100644 --- a/internal/command/session_test.go +++ b/internal/command/session_test.go @@ -152,6 +152,7 @@ func TestCommands_CreateSession(t *testing.T) { checks []SessionCommand metadata map[string][]byte userAgent *domain.UserAgent + lifetime time.Duration } type res struct { want *SessionChanged @@ -192,6 +193,33 @@ func TestCommands_CreateSession(t *testing.T) { 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", fields{ @@ -210,6 +238,7 @@ func TestCommands_CreateSession(t *testing.T) { Description: gu.Ptr("firefox"), Header: http.Header{"foo": []string{"bar"}}, }, + lifetime: 10 * time.Minute, }, []expect{ expectFilter(), @@ -223,6 +252,7 @@ func TestCommands_CreateSession(t *testing.T) { 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, "tokenID", ), @@ -245,7 +275,7 @@ func TestCommands_CreateSession(t *testing.T) { idGenerator: tt.fields.idGenerator, 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) assert.Equal(t, tt.res.want, got) }) @@ -263,6 +293,7 @@ func TestCommands_UpdateSession(t *testing.T) { sessionToken string checks []SessionCommand metadata map[string][]byte + lifetime time.Duration } type res struct { want *SessionChanged @@ -368,7 +399,7 @@ func TestCommands_UpdateSession(t *testing.T) { eventstore: tt.fields.eventstore, 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) assert.Equal(t, tt.res.want, got) }) @@ -397,6 +428,7 @@ func TestCommands_updateSession(t *testing.T) { ctx context.Context checks *SessionCommands metadata map[string][]byte + lifetime time.Duration } type res struct { want *SessionChanged @@ -420,7 +452,7 @@ func TestCommands_updateSession(t *testing.T) { }, }, 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", fields{ @@ -721,7 +820,7 @@ func TestCommands_updateSession(t *testing.T) { c := &Commands{ 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) assert.Equal(t, tt.res.want, got) }) diff --git a/internal/integration/client.go b/internal/integration/client.go index 98a577be1..dadf60e7e 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -13,6 +13,7 @@ import ( "golang.org/x/oauth2" "golang.org/x/text/language" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/durationpb" "github.com/zitadel/zitadel/internal/api/authz" "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 } -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{ Checks: &session.Checks{ User: &session.CheckUser{ @@ -337,6 +346,7 @@ func (s *Tester) CreateVerfiedWebAuthNSession(t *testing.T, ctx context.Context, UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED, }, }, + Lifetime: sessionLifetime, }) require.NoError(t, err) diff --git a/internal/query/projection/session.go b/internal/query/projection/session.go index 5bfc5dd8c..f8c181ea1 100644 --- a/internal/query/projection/session.go +++ b/internal/query/projection/session.go @@ -14,7 +14,7 @@ import ( ) const ( - SessionsProjectionTable = "projections.sessions6" + SessionsProjectionTable = "projections.sessions7" SessionColumnID = "id" SessionColumnCreationDate = "creation_date" @@ -39,6 +39,7 @@ const ( SessionColumnUserAgentIP = "user_agent_ip" SessionColumnUserAgentDescription = "user_agent_description" SessionColumnUserAgentHeader = "user_agent_header" + SessionColumnExpiration = "expiration" ) type sessionProjection struct{} @@ -77,6 +78,7 @@ func (*sessionProjection) Init() *old_handler.Check { handler.NewColumn(SessionColumnUserAgentIP, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(SessionColumnUserAgentDescription, handler.ColumnTypeText, handler.Nullable()), handler.NewColumn(SessionColumnUserAgentHeader, handler.ColumnTypeJSONB, handler.Nullable()), + handler.NewColumn(SessionColumnExpiration, handler.ColumnTypeTimestamp, handler.Nullable()), }, handler.NewPrimaryKey(SessionColumnInstanceID, SessionColumnID), handler.WithIndex(handler.NewIndex( @@ -132,6 +134,10 @@ func (p *sessionProjection) Reducers() []handler.AggregateReducer { Event: session.MetadataSetType, Reduce: p.reduceMetadataSet, }, + { + Event: session.LifetimeSetType, + Reduce: p.reduceLifetimeSet, + }, { Event: session.TerminateType, Reduce: p.reduceSessionTerminated, @@ -376,6 +382,26 @@ func (p *sessionProjection) reduceMetadataSet(event eventstore.Event) (*handler. ), 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) { e, ok := event.(*session.TerminateEvent) if !ok { diff --git a/internal/query/projection/session_test.go b/internal/query/projection/session_test.go index b27b44399..7fcb5391b 100644 --- a/internal/query/projection/session_test.go +++ b/internal/query/projection/session_test.go @@ -51,7 +51,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ "agg-id", "instance-id", @@ -90,7 +90,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ anyArg{}, anyArg{}, @@ -122,7 +122,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ anyArg{}, anyArg{}, @@ -154,7 +154,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ anyArg{}, anyArg{}, @@ -186,7 +186,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ anyArg{}, anyArg{}, @@ -217,7 +217,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ anyArg{}, anyArg{}, @@ -248,7 +248,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ anyArg{}, anyArg{}, @@ -281,7 +281,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ 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", args: args{ @@ -312,7 +343,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ "agg-id", "instance-id", @@ -339,7 +370,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ executions: []execution{ { - expectedStmt: "DELETE FROM projections.sessions6 WHERE (instance_id = $1)", + expectedStmt: "DELETE FROM projections.sessions7 WHERE (instance_id = $1)", expectedArgs: []interface{}{ "agg-id", }, @@ -369,7 +400,7 @@ func TestSessionProjection_reduces(t *testing.T) { executer: &testExecuter{ 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{}{ nil, "agg-id", diff --git a/internal/query/session.go b/internal/query/session.go index 9f5bd15cd..75a31ab27 100644 --- a/internal/query/session.go +++ b/internal/query/session.go @@ -44,6 +44,7 @@ type Session struct { OTPEmailFactor SessionOTPFactor Metadata map[string][]byte UserAgent domain.UserAgent + Expiration time.Time } type SessionUserFactor struct { @@ -185,6 +186,10 @@ var ( name: projection.SessionColumnUserAgentHeader, 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) { @@ -290,6 +295,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil SessionColumnUserAgentIP.identifier(), SessionColumnUserAgentDescription.identifier(), SessionColumnUserAgentHeader.identifier(), + SessionColumnExpiration.identifier(), ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). LeftJoin(join(HumanUserIDCol, SessionColumnUserID)). @@ -314,6 +320,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil token sql.NullString userAgentIP sql.NullString userAgentHeader database.Map[[]string] + expiration sql.NullTime ) err := row.Scan( @@ -342,6 +349,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil &userAgentIP, &session.UserAgent.Description, &userAgentHeader, + &expiration, ) if err != nil { @@ -365,10 +373,10 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time session.Metadata = metadata session.UserAgent.Header = http.Header(userAgentHeader) - if userAgentIP.Valid { session.UserAgent.IP = net.ParseIP(userAgentIP.String) } + session.Expiration = expiration.Time return session, token.String, nil } } @@ -395,6 +403,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui SessionColumnOTPSMSCheckedAt.identifier(), SessionColumnOTPEmailCheckedAt.identifier(), SessionColumnMetadata.identifier(), + SessionColumnExpiration.identifier(), countColumn.identifier(), ).From(sessionsTable.identifier()). LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)). @@ -420,6 +429,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui otpSMSCheckedAt sql.NullTime otpEmailCheckedAt sql.NullTime metadata database.Map[[]byte] + expiration sql.NullTime ) err := rows.Scan( @@ -443,6 +453,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui &otpSMSCheckedAt, &otpEmailCheckedAt, &metadata, + &expiration, &sessions.Count, ) @@ -462,6 +473,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui session.OTPSMSFactor.OTPCheckedAt = otpSMSCheckedAt.Time session.OTPEmailFactor.OTPCheckedAt = otpEmailCheckedAt.Time session.Metadata = metadata + session.Expiration = expiration.Time sessions.Sessions = append(sessions.Sessions, session) } diff --git a/internal/query/sessions_test.go b/internal/query/sessions_test.go index 1bae095ec..d78bedbbd 100644 --- a/internal/query/sessions_test.go +++ b/internal/query/sessions_test.go @@ -20,61 +20,63 @@ import ( ) var ( - expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + - ` projections.sessions6.creation_date,` + - ` projections.sessions6.change_date,` + - ` projections.sessions6.sequence,` + - ` projections.sessions6.state,` + - ` projections.sessions6.resource_owner,` + - ` projections.sessions6.creator,` + - ` projections.sessions6.user_id,` + - ` projections.sessions6.user_checked_at,` + + expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions7.id,` + + ` projections.sessions7.creation_date,` + + ` projections.sessions7.change_date,` + + ` projections.sessions7.sequence,` + + ` projections.sessions7.state,` + + ` projections.sessions7.resource_owner,` + + ` projections.sessions7.creator,` + + ` projections.sessions7.user_id,` + + ` projections.sessions7.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + ` projections.users8.resource_owner,` + - ` projections.sessions6.password_checked_at,` + - ` projections.sessions6.intent_checked_at,` + - ` projections.sessions6.webauthn_checked_at,` + - ` projections.sessions6.webauthn_user_verified,` + - ` projections.sessions6.totp_checked_at,` + - ` projections.sessions6.otp_sms_checked_at,` + - ` projections.sessions6.otp_email_checked_at,` + - ` projections.sessions6.metadata,` + - ` projections.sessions6.token_id,` + - ` projections.sessions6.user_agent_fingerprint_id,` + - ` projections.sessions6.user_agent_ip,` + - ` projections.sessions6.user_agent_description,` + - ` projections.sessions6.user_agent_header` + - ` FROM projections.sessions6` + - ` 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.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 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + + ` projections.sessions7.password_checked_at,` + + ` projections.sessions7.intent_checked_at,` + + ` projections.sessions7.webauthn_checked_at,` + + ` projections.sessions7.webauthn_user_verified,` + + ` projections.sessions7.totp_checked_at,` + + ` projections.sessions7.otp_sms_checked_at,` + + ` projections.sessions7.otp_email_checked_at,` + + ` projections.sessions7.metadata,` + + ` projections.sessions7.token_id,` + + ` projections.sessions7.user_agent_fingerprint_id,` + + ` projections.sessions7.user_agent_ip,` + + ` projections.sessions7.user_agent_description,` + + ` projections.sessions7.user_agent_header,` + + ` projections.sessions7.expiration` + + ` FROM projections.sessions7` + + ` 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.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'`) - expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions6.id,` + - ` projections.sessions6.creation_date,` + - ` projections.sessions6.change_date,` + - ` projections.sessions6.sequence,` + - ` projections.sessions6.state,` + - ` projections.sessions6.resource_owner,` + - ` projections.sessions6.creator,` + - ` projections.sessions6.user_id,` + - ` projections.sessions6.user_checked_at,` + + expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions7.id,` + + ` projections.sessions7.creation_date,` + + ` projections.sessions7.change_date,` + + ` projections.sessions7.sequence,` + + ` projections.sessions7.state,` + + ` projections.sessions7.resource_owner,` + + ` projections.sessions7.creator,` + + ` projections.sessions7.user_id,` + + ` projections.sessions7.user_checked_at,` + ` projections.login_names2.login_name,` + ` projections.users8_humans.display_name,` + ` projections.users8.resource_owner,` + - ` projections.sessions6.password_checked_at,` + - ` projections.sessions6.intent_checked_at,` + - ` projections.sessions6.webauthn_checked_at,` + - ` projections.sessions6.webauthn_user_verified,` + - ` projections.sessions6.totp_checked_at,` + - ` projections.sessions6.otp_sms_checked_at,` + - ` projections.sessions6.otp_email_checked_at,` + - ` projections.sessions6.metadata,` + + ` projections.sessions7.password_checked_at,` + + ` projections.sessions7.intent_checked_at,` + + ` projections.sessions7.webauthn_checked_at,` + + ` projections.sessions7.webauthn_user_verified,` + + ` projections.sessions7.totp_checked_at,` + + ` projections.sessions7.otp_sms_checked_at,` + + ` projections.sessions7.otp_email_checked_at,` + + ` projections.sessions7.metadata,` + + ` projections.sessions7.expiration,` + ` COUNT(*) OVER ()` + - ` FROM projections.sessions6` + - ` 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.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 ON projections.sessions6.user_id = projections.users8.id AND projections.sessions6.instance_id = projections.users8.instance_id` + + ` FROM projections.sessions7` + + ` 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.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'`) sessionCols = []string{ @@ -103,6 +105,7 @@ var ( "user_agent_ip", "user_agent_description", "user_agent_header", + "expiration", } sessionsCols = []string{ @@ -126,6 +129,7 @@ var ( "otp_sms_checked_at", "otp_email_checked_at", "metadata", + "expiration", "count", } ) @@ -182,6 +186,7 @@ func Test_SessionsPrepare(t *testing.T) { testNow, testNow, []byte(`{"key": "dmFsdWU="}`), + testNow, }, }, ), @@ -228,6 +233,7 @@ func Test_SessionsPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + Expiration: testNow, }, }, }, @@ -261,6 +267,7 @@ func Test_SessionsPrepare(t *testing.T) { testNow, testNow, []byte(`{"key": "dmFsdWU="}`), + testNow, }, { "session-id2", @@ -283,6 +290,7 @@ func Test_SessionsPrepare(t *testing.T) { testNow, testNow, []byte(`{"key": "dmFsdWU="}`), + testNow, }, }, ), @@ -329,6 +337,7 @@ func Test_SessionsPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + Expiration: testNow, }, { ID: "session-id2", @@ -367,6 +376,7 @@ func Test_SessionsPrepare(t *testing.T) { Metadata: map[string][]byte{ "key": []byte("value"), }, + Expiration: testNow, }, }, }, @@ -458,6 +468,7 @@ func Test_SessionPrepare(t *testing.T) { "1.2.3.4", "agentDescription", []byte(`{"foo":["foo","bar"]}`), + testNow, }, ), }, @@ -504,6 +515,7 @@ func Test_SessionPrepare(t *testing.T) { Description: gu.Ptr("agentDescription"), Header: http.Header{"foo": []string{"foo", "bar"}}, }, + Expiration: testNow, }, }, { diff --git a/internal/repository/session/eventstore.go b/internal/repository/session/eventstore.go index 2923e5239..ccd95d666 100644 --- a/internal/repository/session/eventstore.go +++ b/internal/repository/session/eventstore.go @@ -18,5 +18,6 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, OTPEmailCheckedType, eventstore.GenericEventMapper[OTPEmailCheckedEvent]). RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper). RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper). + RegisterFilterEventMapper(AggregateType, LifetimeSetType, eventstore.GenericEventMapper[LifetimeSetEvent]). RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper) } diff --git a/internal/repository/session/session.go b/internal/repository/session/session.go index 83d905ea8..a63cb8cfb 100644 --- a/internal/repository/session/session.go +++ b/internal/repository/session/session.go @@ -28,6 +28,7 @@ const ( OTPEmailCheckedType = sessionEventPrefix + "otp.email.checked" TokenSetType = sessionEventPrefix + "token.set" MetadataSetType = sessionEventPrefix + "metadata.set" + LifetimeSetType = sessionEventPrefix + "lifetime.set" TerminateType = sessionEventPrefix + "terminated" ) @@ -607,6 +608,39 @@ func MetadataSetEventMapper(event eventstore.Event) (eventstore.Event, error) { 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 { eventstore.BaseEvent `json:"-"` } diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index a0b757ac2..46eda0c4d 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -509,6 +509,8 @@ Errors: Session: NotExisting: Сесията не съществува Terminated: Сесията вече е прекратена + Expired: Сесията е изтекла + PositiveLifetime: Животът на сесията не трябва да е по-малък от 0 Token: Invalid: Токенът на сесията е невалиден WebAuthN: diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index e3c357288..0b902da86 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -491,6 +491,8 @@ Errors: Session: NotExisting: Session existiert nicht Terminated: Session bereits beendet + Expired: Session ist abgelaufen + PositiveLifetime: Session Lebensdauer darf nicht kleiner als 0 sein Token: Invalid: Session Token ist ungültig WebAuthN: diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index cc9e53944..ed881e22a 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -491,6 +491,8 @@ Errors: Session: NotExisting: Session does not exist Terminated: Session already terminated + Expired: Session has expired + PositiveLifetime: Session lifetime must not be less than 0 Token: Invalid: Session Token is invalid WebAuthN: diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 066724cb7..ff4410cb0 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -490,7 +490,9 @@ Errors: ScanFailed: La consulta de uso de los segundos de ejecuciónde acciones ha fallado Session: 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: Invalid: El identificador de sesión no es válido WebAuthN: diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 43505067c..3a14c5226 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -491,6 +491,8 @@ Errors: Session: NotExisting: La session n'existe pas 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: Invalid: Le jeton de session n'est pas valide WebAuthN: diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 5c4787be0..fe56c424e 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -491,7 +491,9 @@ Errors: ScanFailed: La query dei secondi delle azioni utilizzate non è riuscita Session: 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: Invalid: Il token della sessione non è valido WebAuthN: diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 9752b68ea..9180e41d5 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -480,6 +480,8 @@ Errors: Session: NotExisting: セッションが存在しない Terminated: セッションはすでに終了しています + Expired: セッションの有効期限が切れました + PositiveLifetime: セッションの有効期間は 0 未満であってはなりません Token: Invalid: セッショントークンが無効です WebAuthN: diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index c2544d432..085daec0d 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -491,6 +491,8 @@ Errors: Session: NotExisting: Сесијата не постои Terminated: Сесијата е веќе завршена + Expired: Сесијата истече + PositiveLifetime: Времетраењето на сесијата не смее да биде помало од 0 Token: Invalid: Токенот за сесија е невалиден WebAuthN: diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 230600d05..3a54a6fcb 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -491,6 +491,8 @@ Errors: Session: NotExisting: Sesja nie istnieje Terminated: Sesja już zakończona + Expired: Sesja wygasła + PositiveLifetime: Czas życia sesji nie może być krótszy niż 0 Token: Invalid: Token sesji jest nieprawidłowy WebAuthN: diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 92a93fb5a..90ff628a1 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -489,6 +489,8 @@ Errors: Session: NotExisting: A sessão não existe 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: Invalid: O token da sessão é inválido WebAuthN: diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 8c3210925..a44de5485 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -491,6 +491,8 @@ Errors: Session: NotExisting: 会话不存在 Terminated: 会话已经终止 + Expired: 会话已过期 + PositiveLifetime: 会话生存期不得小于 0 Token: Invalid: 会话令牌是无效的 WebAuthN: diff --git a/proto/zitadel/session/v2beta/session.proto b/proto/zitadel/session/v2beta/session.proto index 08ef97e21..5de49c185 100644 --- a/proto/zitadel/session/v2beta/session.proto +++ b/proto/zitadel/session/v2beta/session.proto @@ -40,6 +40,11 @@ message Session { } ]; 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 { diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index 718a29381..714e7779d 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -10,6 +10,7 @@ import "zitadel/session/v2beta/session.proto"; import "google/api/annotations.proto"; import "google/api/field_behavior.proto"; import "google/protobuf/struct.proto"; +import "google/protobuf/duration.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; @@ -275,6 +276,12 @@ message CreateSessionRequest{ ]; RequestChallenges challenges = 3; 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{ @@ -322,6 +329,12 @@ message SetSessionRequest{ } ]; 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{