feat: session v2 passkey authentication (#5952)

This commit is contained in:
Tim Möhlmann
2023-06-07 17:28:42 +02:00
committed by GitHub
parent f7157b65f4
commit f456168a74
39 changed files with 1261 additions and 162 deletions

View File

@@ -13,7 +13,7 @@ import (
)
const (
SessionsProjectionTable = "projections.sessions"
SessionsProjectionTable = "projections.sessions1"
SessionColumnID = "id"
SessionColumnCreationDate = "creation_date"
@@ -26,6 +26,7 @@ const (
SessionColumnUserID = "user_id"
SessionColumnUserCheckedAt = "user_checked_at"
SessionColumnPasswordCheckedAt = "password_checked_at"
SessionColumnPasskeyCheckedAt = "passkey_checked_at"
SessionColumnMetadata = "metadata"
SessionColumnTokenID = "token_id"
)
@@ -51,6 +52,7 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi
crdb.NewColumn(SessionColumnUserID, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(SessionColumnUserCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnPasswordCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnPasskeyCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()),
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
},
@@ -78,6 +80,10 @@ func (p *sessionProjection) reducers() []handler.AggregateReducer {
Event: session.PasswordCheckedType,
Reduce: p.reducePasswordChecked,
},
{
Event: session.PasskeyCheckedType,
Reduce: p.reducePasskeyChecked,
},
{
Event: session.TokenSetType,
Reduce: p.reduceTokenSet,
@@ -165,6 +171,26 @@ func (p *sessionProjection) reducePasswordChecked(event eventstore.Event) (*hand
), nil
}
func (p *sessionProjection) reducePasskeyChecked(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*session.PasskeyCheckedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-WieM4", "reduce.wrong.event.type %s", session.PasskeyCheckedType)
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
handler.NewCol(SessionColumnSequence, e.Sequence()),
handler.NewCol(SessionColumnPasskeyCheckedAt, 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 {

View File

@@ -40,7 +40,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.sessions (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
expectedStmt: "INSERT INTO projections.sessions1 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -76,7 +76,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@@ -109,7 +109,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@@ -141,7 +141,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@@ -175,7 +175,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@@ -207,7 +207,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.sessions WHERE (id = $1) AND (instance_id = $2)",
expectedStmt: "DELETE FROM projections.sessions1 WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@@ -234,7 +234,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.sessions WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.sessions1 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},

View File

@@ -32,6 +32,7 @@ type Session struct {
Creator string
UserFactor SessionUserFactor
PasswordFactor SessionPasswordFactor
PasskeyFactor SessionPasskeyFactor
Metadata map[string][]byte
}
@@ -46,6 +47,10 @@ type SessionPasswordFactor struct {
PasswordCheckedAt time.Time
}
type SessionPasskeyFactor struct {
PasskeyCheckedAt time.Time
}
type SessionsSearchQueries struct {
SearchRequest
Queries []SearchQuery
@@ -108,6 +113,10 @@ var (
name: projection.SessionColumnPasswordCheckedAt,
table: sessionsTable,
}
SessionColumnPasskeyCheckedAt = Column{
name: projection.SessionColumnPasskeyCheckedAt,
table: sessionsTable,
}
SessionColumnMetadata = Column{
name: projection.SessionColumnMetadata,
table: sessionsTable,
@@ -198,6 +207,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
SessionColumnMetadata.identifier(),
SessionColumnToken.identifier(),
).From(sessionsTable.identifier()).
@@ -212,6 +222,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
loginName sql.NullString
displayName sql.NullString
passwordCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
metadata database.Map[[]byte]
token sql.NullString
)
@@ -229,6 +240,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&loginName,
&displayName,
&passwordCheckedAt,
&passkeyCheckedAt,
&metadata,
&token,
)
@@ -245,6 +257,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
session.Metadata = metadata
return session, token.String, nil
@@ -265,6 +278,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
SessionColumnMetadata.identifier(),
countColumn.identifier(),
).From(sessionsTable.identifier()).
@@ -282,6 +296,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
loginName sql.NullString
displayName sql.NullString
passwordCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
metadata database.Map[[]byte]
)
@@ -298,6 +313,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
&loginName,
&displayName,
&passwordCheckedAt,
&passkeyCheckedAt,
&metadata,
&sessions.Count,
)
@@ -310,6 +326,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
session.Metadata = metadata
sessions.Sessions = append(sessions.Sessions, session)

View File

@@ -17,41 +17,43 @@ import (
)
var (
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions.id,` +
` projections.sessions.creation_date,` +
` projections.sessions.change_date,` +
` projections.sessions.sequence,` +
` projections.sessions.state,` +
` projections.sessions.resource_owner,` +
` projections.sessions.creator,` +
` projections.sessions.user_id,` +
` projections.sessions.user_checked_at,` +
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` +
` projections.sessions1.creation_date,` +
` projections.sessions1.change_date,` +
` projections.sessions1.sequence,` +
` projections.sessions1.state,` +
` projections.sessions1.resource_owner,` +
` projections.sessions1.creator,` +
` projections.sessions1.user_id,` +
` projections.sessions1.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
` projections.sessions.password_checked_at,` +
` projections.sessions.metadata,` +
` projections.sessions.token_id` +
` FROM projections.sessions` +
` LEFT JOIN projections.login_names2 ON projections.sessions.user_id = projections.login_names2.user_id AND projections.sessions.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions.user_id = projections.users8_humans.user_id AND projections.sessions.instance_id = projections.users8_humans.instance_id` +
` projections.sessions1.password_checked_at,` +
` projections.sessions1.passkey_checked_at,` +
` projections.sessions1.metadata,` +
` projections.sessions1.token_id` +
` FROM projections.sessions1` +
` LEFT JOIN projections.login_names2 ON projections.sessions1.user_id = projections.login_names2.user_id AND projections.sessions1.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions1.user_id = projections.users8_humans.user_id AND projections.sessions1.instance_id = projections.users8_humans.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions.id,` +
` projections.sessions.creation_date,` +
` projections.sessions.change_date,` +
` projections.sessions.sequence,` +
` projections.sessions.state,` +
` projections.sessions.resource_owner,` +
` projections.sessions.creator,` +
` projections.sessions.user_id,` +
` projections.sessions.user_checked_at,` +
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` +
` projections.sessions1.creation_date,` +
` projections.sessions1.change_date,` +
` projections.sessions1.sequence,` +
` projections.sessions1.state,` +
` projections.sessions1.resource_owner,` +
` projections.sessions1.creator,` +
` projections.sessions1.user_id,` +
` projections.sessions1.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
` projections.sessions.password_checked_at,` +
` projections.sessions.metadata,` +
` projections.sessions1.password_checked_at,` +
` projections.sessions1.passkey_checked_at,` +
` projections.sessions1.metadata,` +
` COUNT(*) OVER ()` +
` FROM projections.sessions` +
` LEFT JOIN projections.login_names2 ON projections.sessions.user_id = projections.login_names2.user_id AND projections.sessions.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions.user_id = projections.users8_humans.user_id AND projections.sessions.instance_id = projections.users8_humans.instance_id` +
` FROM projections.sessions1` +
` LEFT JOIN projections.login_names2 ON projections.sessions1.user_id = projections.login_names2.user_id AND projections.sessions1.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions1.user_id = projections.users8_humans.user_id AND projections.sessions1.instance_id = projections.users8_humans.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
sessionCols = []string{
@@ -67,6 +69,7 @@ var (
"login_name",
"display_name",
"password_checked_at",
"passkey_checked_at",
"metadata",
"token",
}
@@ -84,6 +87,7 @@ var (
"login_name",
"display_name",
"password_checked_at",
"passkey_checked_at",
"metadata",
"count",
}
@@ -133,6 +137,7 @@ func Test_SessionsPrepare(t *testing.T) {
"login-name",
"display-name",
testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`),
},
},
@@ -160,6 +165,9 @@ func Test_SessionsPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
},
Metadata: map[string][]byte{
"key": []byte("value"),
},
@@ -188,6 +196,7 @@ func Test_SessionsPrepare(t *testing.T) {
"login-name",
"display-name",
testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`),
},
{
@@ -203,6 +212,7 @@ func Test_SessionsPrepare(t *testing.T) {
"login-name2",
"display-name2",
testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`),
},
},
@@ -230,6 +240,9 @@ func Test_SessionsPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
},
Metadata: map[string][]byte{
"key": []byte("value"),
},
@@ -251,6 +264,9 @@ func Test_SessionsPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
},
Metadata: map[string][]byte{
"key": []byte("value"),
},
@@ -332,6 +348,7 @@ func Test_SessionPrepare(t *testing.T) {
"login-name",
"display-name",
testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`),
"tokenID",
},
@@ -354,6 +371,9 @@ func Test_SessionPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
},
Metadata: map[string][]byte{
"key": []byte("value"),
},