mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:47:33 +00:00
feat(api/v2): implement TOTP session check (#6362)
* feat(api/v2): implement TOTP session check * add integration test * correct typo in projection test * fix event type typos --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ const (
|
||||
SessionColumnIntentCheckedAt = "intent_checked_at"
|
||||
SessionColumnWebAuthNCheckedAt = "webauthn_checked_at"
|
||||
SessionColumnWebAuthNUserVerified = "webauthn_user_verified"
|
||||
SessionColumnTOTPCheckedAt = "totp_checked_at"
|
||||
SessionColumnMetadata = "metadata"
|
||||
SessionColumnTokenID = "token_id"
|
||||
)
|
||||
@@ -58,6 +59,7 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi
|
||||
crdb.NewColumn(SessionColumnIntentCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnWebAuthNCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnWebAuthNUserVerified, crdb.ColumnTypeBool, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnTOTPCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
|
||||
},
|
||||
@@ -93,6 +95,10 @@ func (p *sessionProjection) reducers() []handler.AggregateReducer {
|
||||
Event: session.WebAuthNCheckedType,
|
||||
Reduce: p.reduceWebAuthNChecked,
|
||||
},
|
||||
{
|
||||
Event: session.TOTPCheckedType,
|
||||
Reduce: p.reduceTOTPChecked,
|
||||
},
|
||||
{
|
||||
Event: session.TokenSetType,
|
||||
Reduce: p.reduceTokenSet,
|
||||
@@ -229,6 +235,26 @@ func (p *sessionProjection) reduceWebAuthNChecked(event eventstore.Event) (*hand
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceTOTPChecked(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.TOTPCheckedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-Oqu8i", "reduce.wrong.event.type %s", session.TOTPCheckedType)
|
||||
}
|
||||
|
||||
return crdb.NewUpdateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SessionColumnTOTPCheckedAt, e.CheckedAt),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceTokenSet(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.TokenSetEvent)
|
||||
if !ok {
|
||||
|
@@ -191,6 +191,38 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceOTPChecked",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.AddedType,
|
||||
session.AggregateType,
|
||||
[]byte(`{
|
||||
"checkedAt": "2023-05-04T00:00:00Z"
|
||||
}`),
|
||||
), eventstore.GenericEventMapper[session.TOTPCheckedEvent]),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reduceTOTPChecked,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions4 SET (change_date, sequence, totp_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
time.Date(2023, time.May, 4, 0, 0, 0, 0, time.UTC),
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceTokenSet",
|
||||
args: args{
|
||||
|
@@ -34,6 +34,7 @@ type Session struct {
|
||||
PasswordFactor SessionPasswordFactor
|
||||
IntentFactor SessionIntentFactor
|
||||
WebAuthNFactor SessionWebAuthNFactor
|
||||
TOTPFactor SessionTOTPFactor
|
||||
Metadata map[string][]byte
|
||||
}
|
||||
|
||||
@@ -58,6 +59,10 @@ type SessionWebAuthNFactor struct {
|
||||
UserVerified bool
|
||||
}
|
||||
|
||||
type SessionTOTPFactor struct {
|
||||
TOTPCheckedAt time.Time
|
||||
}
|
||||
|
||||
type SessionsSearchQueries struct {
|
||||
SearchRequest
|
||||
Queries []SearchQuery
|
||||
@@ -132,6 +137,10 @@ var (
|
||||
name: projection.SessionColumnWebAuthNUserVerified,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnTOTPCheckedAt = Column{
|
||||
name: projection.SessionColumnTOTPCheckedAt,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnMetadata = Column{
|
||||
name: projection.SessionColumnMetadata,
|
||||
table: sessionsTable,
|
||||
@@ -230,6 +239,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
SessionColumnIntentCheckedAt.identifier(),
|
||||
SessionColumnWebAuthNCheckedAt.identifier(),
|
||||
SessionColumnWebAuthNUserVerified.identifier(),
|
||||
SessionColumnTOTPCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
SessionColumnToken.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
@@ -249,6 +259,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
intentCheckedAt sql.NullTime
|
||||
webAuthNCheckedAt sql.NullTime
|
||||
webAuthNUserPresent sql.NullBool
|
||||
totpCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
token sql.NullString
|
||||
)
|
||||
@@ -270,6 +281,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
&intentCheckedAt,
|
||||
&webAuthNCheckedAt,
|
||||
&webAuthNUserPresent,
|
||||
&totpCheckedAt,
|
||||
&metadata,
|
||||
&token,
|
||||
)
|
||||
@@ -290,6 +302,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
|
||||
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
||||
session.TOTPFactor.TOTPCheckedAt = totpCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
return session, token.String, nil
|
||||
@@ -314,6 +327,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
SessionColumnIntentCheckedAt.identifier(),
|
||||
SessionColumnWebAuthNCheckedAt.identifier(),
|
||||
SessionColumnWebAuthNUserVerified.identifier(),
|
||||
SessionColumnTOTPCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
countColumn.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
@@ -336,6 +350,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
intentCheckedAt sql.NullTime
|
||||
webAuthNCheckedAt sql.NullTime
|
||||
webAuthNUserPresent sql.NullBool
|
||||
totpCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
)
|
||||
|
||||
@@ -356,6 +371,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
&intentCheckedAt,
|
||||
&webAuthNCheckedAt,
|
||||
&webAuthNUserPresent,
|
||||
&totpCheckedAt,
|
||||
&metadata,
|
||||
&sessions.Count,
|
||||
)
|
||||
@@ -372,6 +388,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
|
||||
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
||||
session.TOTPFactor.TOTPCheckedAt = totpCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
sessions.Sessions = append(sessions.Sessions, session)
|
||||
|
@@ -33,6 +33,7 @@ var (
|
||||
` projections.sessions4.intent_checked_at,` +
|
||||
` projections.sessions4.webauthn_checked_at,` +
|
||||
` projections.sessions4.webauthn_user_verified,` +
|
||||
` projections.sessions4.totp_checked_at,` +
|
||||
` projections.sessions4.metadata,` +
|
||||
` projections.sessions4.token_id` +
|
||||
` FROM projections.sessions4` +
|
||||
@@ -56,6 +57,7 @@ var (
|
||||
` projections.sessions4.intent_checked_at,` +
|
||||
` projections.sessions4.webauthn_checked_at,` +
|
||||
` projections.sessions4.webauthn_user_verified,` +
|
||||
` projections.sessions4.totp_checked_at,` +
|
||||
` projections.sessions4.metadata,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.sessions4` +
|
||||
@@ -81,6 +83,7 @@ var (
|
||||
"intent_checked_at",
|
||||
"webauthn_checked_at",
|
||||
"webauthn_user_verified",
|
||||
"totp_checked_at",
|
||||
"metadata",
|
||||
"token",
|
||||
}
|
||||
@@ -102,6 +105,7 @@ var (
|
||||
"intent_checked_at",
|
||||
"webauthn_checked_at",
|
||||
"webauthn_user_verified",
|
||||
"totp_checked_at",
|
||||
"metadata",
|
||||
"count",
|
||||
}
|
||||
@@ -155,6 +159,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
testNow,
|
||||
testNow,
|
||||
true,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
},
|
||||
@@ -190,6 +195,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
WebAuthNCheckedAt: testNow,
|
||||
UserVerified: true,
|
||||
},
|
||||
TOTPFactor: SessionTOTPFactor{
|
||||
TOTPCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
@@ -222,6 +230,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
testNow,
|
||||
testNow,
|
||||
true,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
{
|
||||
@@ -241,6 +250,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
testNow,
|
||||
testNow,
|
||||
false,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
},
|
||||
@@ -276,6 +286,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
WebAuthNCheckedAt: testNow,
|
||||
UserVerified: true,
|
||||
},
|
||||
TOTPFactor: SessionTOTPFactor{
|
||||
TOTPCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
@@ -305,6 +318,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
WebAuthNCheckedAt: testNow,
|
||||
UserVerified: false,
|
||||
},
|
||||
TOTPFactor: SessionTOTPFactor{
|
||||
TOTPCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
@@ -390,6 +406,7 @@ func Test_SessionPrepare(t *testing.T) {
|
||||
testNow,
|
||||
testNow,
|
||||
true,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
"tokenID",
|
||||
},
|
||||
@@ -420,6 +437,9 @@ func Test_SessionPrepare(t *testing.T) {
|
||||
WebAuthNCheckedAt: testNow,
|
||||
UserVerified: true,
|
||||
},
|
||||
TOTPFactor: SessionTOTPFactor{
|
||||
TOTPCheckedAt: testNow,
|
||||
},
|
||||
Metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
|
Reference in New Issue
Block a user