mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-08 18:27:40 +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:
parent
8953353210
commit
0017542aa2
@ -120,6 +120,7 @@ func factorsToPb(s *query.Session) *session.Factors {
|
|||||||
Password: passwordFactorToPb(s.PasswordFactor),
|
Password: passwordFactorToPb(s.PasswordFactor),
|
||||||
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
|
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
|
||||||
Intent: intentFactorToPb(s.IntentFactor),
|
Intent: intentFactorToPb(s.IntentFactor),
|
||||||
|
Totp: totpFactorToPb(s.TOTPFactor),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +152,15 @@ func webAuthNFactorToPb(factor query.SessionWebAuthNFactor) *session.WebAuthNFac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func totpFactorToPb(factor query.SessionTOTPFactor) *session.TOTPFactor {
|
||||||
|
if factor.TOTPCheckedAt.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &session.TOTPFactor{
|
||||||
|
VerifiedAt: timestamppb.New(factor.TOTPCheckedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
|
func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
|
||||||
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
|
if factor.UserID == "" || factor.UserCheckedAt.IsZero() {
|
||||||
return nil
|
return nil
|
||||||
@ -247,7 +257,9 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
|
|||||||
if passkey := checks.GetWebAuthN(); passkey != nil {
|
if passkey := checks.GetWebAuthN(); passkey != nil {
|
||||||
sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData()))
|
sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData()))
|
||||||
}
|
}
|
||||||
|
if totp := checks.GetTotp(); totp != nil {
|
||||||
|
sessionChecks = append(sessionChecks, command.CheckTOTP(totp.GetTotp()))
|
||||||
|
}
|
||||||
return sessionChecks, nil
|
return sessionChecks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/muhlemmer/gu"
|
"github.com/muhlemmer/gu"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
@ -45,6 +46,7 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, factors ...wantFactor) *session.Session {
|
func verifyCurrentSession(t testing.TB, id, token string, sequence uint64, window time.Duration, metadata map[string][]byte, factors ...wantFactor) *session.Session {
|
||||||
|
t.Helper()
|
||||||
require.NotEmpty(t, id)
|
require.NotEmpty(t, id)
|
||||||
require.NotEmpty(t, token)
|
require.NotEmpty(t, token)
|
||||||
|
|
||||||
@ -71,6 +73,7 @@ const (
|
|||||||
wantPasswordFactor
|
wantPasswordFactor
|
||||||
wantWebAuthNFactor
|
wantWebAuthNFactor
|
||||||
wantWebAuthNFactorUserVerified
|
wantWebAuthNFactorUserVerified
|
||||||
|
wantTOTPFactor
|
||||||
wantIntentFactor
|
wantIntentFactor
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -90,12 +93,16 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration,
|
|||||||
pf := factors.GetWebAuthN()
|
pf := factors.GetWebAuthN()
|
||||||
assert.NotNil(t, pf)
|
assert.NotNil(t, pf)
|
||||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||||
assert.False(t, pf.UserVerified)
|
assert.False(t, pf.GetUserVerified())
|
||||||
case wantWebAuthNFactorUserVerified:
|
case wantWebAuthNFactorUserVerified:
|
||||||
pf := factors.GetWebAuthN()
|
pf := factors.GetWebAuthN()
|
||||||
assert.NotNil(t, pf)
|
assert.NotNil(t, pf)
|
||||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||||
assert.True(t, pf.UserVerified)
|
assert.True(t, pf.GetUserVerified())
|
||||||
|
case wantTOTPFactor:
|
||||||
|
pf := factors.GetTotp()
|
||||||
|
assert.NotNil(t, pf)
|
||||||
|
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||||
case wantIntentFactor:
|
case wantIntentFactor:
|
||||||
pf := factors.GetIntent()
|
pf := factors.GetIntent()
|
||||||
assert.NotNil(t, pf)
|
assert.NotNil(t, pf)
|
||||||
@ -338,6 +345,23 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
|
|||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerTOTP(ctx context.Context, t *testing.T, userID string) (secret string) {
|
||||||
|
resp, err := Tester.Client.UserV2.RegisterTOTP(ctx, &user.RegisterTOTPRequest{
|
||||||
|
UserId: userID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
secret = resp.GetSecret()
|
||||||
|
code, err := totp.GenerateCode(secret, time.Now())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = Tester.Client.UserV2.VerifyTOTPRegistration(ctx, &user.VerifyTOTPRegistrationRequest{
|
||||||
|
UserId: userID,
|
||||||
|
Code: code,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return secret
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_SetSession_flow(t *testing.T) {
|
func TestServer_SetSession_flow(t *testing.T) {
|
||||||
// create new, empty session
|
// create new, empty session
|
||||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
|
||||||
@ -346,7 +370,6 @@ func TestServer_SetSession_flow(t *testing.T) {
|
|||||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil)
|
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil)
|
||||||
|
|
||||||
t.Run("check user", func(t *testing.T) {
|
t.Run("check user", func(t *testing.T) {
|
||||||
wantFactors := []wantFactor{wantUserFactor}
|
|
||||||
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||||
SessionId: createResp.GetSessionId(),
|
SessionId: createResp.GetSessionId(),
|
||||||
SessionToken: sessionToken,
|
SessionToken: sessionToken,
|
||||||
@ -360,7 +383,7 @@ func TestServer_SetSession_flow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sessionToken = resp.GetSessionToken()
|
sessionToken = resp.GetSessionToken()
|
||||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
|
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
|
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
|
||||||
@ -378,7 +401,6 @@ func TestServer_SetSession_flow(t *testing.T) {
|
|||||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
|
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
|
||||||
sessionToken = resp.GetSessionToken()
|
sessionToken = resp.GetSessionToken()
|
||||||
|
|
||||||
wantFactors := []wantFactor{wantUserFactor, wantWebAuthNFactorUserVerified}
|
|
||||||
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
|
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -393,14 +415,14 @@ func TestServer_SetSession_flow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sessionToken = resp.GetSessionToken()
|
sessionToken = resp.GetSessionToken()
|
||||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
|
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactorUserVerified)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
userAuthCtx := Tester.WithAuthorizationToken(CTX, sessionToken)
|
||||||
|
Tester.RegisterUserU2F(userAuthCtx, User.GetUserId())
|
||||||
|
totpSecret := registerTOTP(userAuthCtx, t, User.GetUserId())
|
||||||
|
|
||||||
t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) {
|
t.Run("check webauthn, user not verified (U2F)", func(t *testing.T) {
|
||||||
Tester.RegisterUserU2F(
|
|
||||||
Tester.WithAuthorizationToken(context.Background(), sessionToken),
|
|
||||||
User.GetUserId(),
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, userVerificationRequirement := range []session.UserVerificationRequirement{
|
for _, userVerificationRequirement := range []session.UserVerificationRequirement{
|
||||||
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED,
|
||||||
@ -421,7 +443,6 @@ func TestServer_SetSession_flow(t *testing.T) {
|
|||||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
|
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
|
||||||
sessionToken = resp.GetSessionToken()
|
sessionToken = resp.GetSessionToken()
|
||||||
|
|
||||||
wantFactors := []wantFactor{wantUserFactor, wantWebAuthNFactor}
|
|
||||||
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false)
|
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -436,10 +457,27 @@ func TestServer_SetSession_flow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
sessionToken = resp.GetSessionToken()
|
sessionToken = resp.GetSessionToken()
|
||||||
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
|
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("check TOTP", func(t *testing.T) {
|
||||||
|
code, err := totp.GenerateCode(totpSecret, time.Now())
|
||||||
|
require.NoError(t, err)
|
||||||
|
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||||
|
SessionId: createResp.GetSessionId(),
|
||||||
|
SessionToken: sessionToken,
|
||||||
|
Checks: &session.Checks{
|
||||||
|
Totp: &session.CheckTOTP{
|
||||||
|
Totp: code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
sessionToken = resp.GetSessionToken()
|
||||||
|
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactor, wantTOTPFactor)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
||||||
|
@ -91,6 +91,26 @@ func Test_sessionsToPb(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||||
},
|
},
|
||||||
|
{ // totp factor
|
||||||
|
ID: "999",
|
||||||
|
CreationDate: now,
|
||||||
|
ChangeDate: now,
|
||||||
|
Sequence: 123,
|
||||||
|
State: domain.SessionStateActive,
|
||||||
|
ResourceOwner: "me",
|
||||||
|
Creator: "he",
|
||||||
|
UserFactor: query.SessionUserFactor{
|
||||||
|
UserID: "345",
|
||||||
|
UserCheckedAt: past,
|
||||||
|
LoginName: "donald",
|
||||||
|
DisplayName: "donald duck",
|
||||||
|
ResourceOwner: "org1",
|
||||||
|
},
|
||||||
|
TOTPFactor: query.SessionTOTPFactor{
|
||||||
|
TOTPCheckedAt: past,
|
||||||
|
},
|
||||||
|
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
want := []*session.Session{
|
want := []*session.Session{
|
||||||
@ -157,6 +177,25 @@ func Test_sessionsToPb(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||||
},
|
},
|
||||||
|
{ // totp factor
|
||||||
|
Id: "999",
|
||||||
|
CreationDate: timestamppb.New(now),
|
||||||
|
ChangeDate: timestamppb.New(now),
|
||||||
|
Sequence: 123,
|
||||||
|
Factors: &session.Factors{
|
||||||
|
User: &session.UserFactor{
|
||||||
|
VerifiedAt: timestamppb.New(past),
|
||||||
|
Id: "345",
|
||||||
|
LoginName: "donald",
|
||||||
|
DisplayName: "donald duck",
|
||||||
|
OrganisationId: "org1",
|
||||||
|
},
|
||||||
|
Totp: &session.TOTPFactor{
|
||||||
|
VerifiedAt: timestamppb.New(past),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := sessionsToPb(sessions)
|
out := sessionsToPb(sessions)
|
||||||
|
@ -26,11 +26,13 @@ type SessionCommands struct {
|
|||||||
sessionWriteModel *SessionWriteModel
|
sessionWriteModel *SessionWriteModel
|
||||||
passwordWriteModel *HumanPasswordWriteModel
|
passwordWriteModel *HumanPasswordWriteModel
|
||||||
intentWriteModel *IDPIntentWriteModel
|
intentWriteModel *IDPIntentWriteModel
|
||||||
|
totpWriteModel *HumanTOTPWriteModel
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
eventCommands []eventstore.Command
|
eventCommands []eventstore.Command
|
||||||
|
|
||||||
hasher *crypto.PasswordHasher
|
hasher *crypto.PasswordHasher
|
||||||
intentAlg crypto.EncryptionAlgorithm
|
intentAlg crypto.EncryptionAlgorithm
|
||||||
|
totpAlg crypto.EncryptionAlgorithm
|
||||||
createToken func(sessionID string) (id string, token string, err error)
|
createToken func(sessionID string) (id string, token string, err error)
|
||||||
now func() time.Time
|
now func() time.Time
|
||||||
}
|
}
|
||||||
@ -42,6 +44,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
|
|||||||
eventstore: c.eventstore,
|
eventstore: c.eventstore,
|
||||||
hasher: c.userPasswordHasher,
|
hasher: c.userPasswordHasher,
|
||||||
intentAlg: c.idpConfigEncryption,
|
intentAlg: c.idpConfigEncryption,
|
||||||
|
totpAlg: c.multifactors.OTP.CryptoMFA,
|
||||||
createToken: c.sessionTokenCreator,
|
createToken: c.sessionTokenCreator,
|
||||||
now: time.Now,
|
now: time.Now,
|
||||||
}
|
}
|
||||||
@ -127,6 +130,28 @@ func CheckIntent(intentID, token string) SessionCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckTOTP(code string) SessionCommand {
|
||||||
|
return func(ctx context.Context, cmd *SessionCommands) (err error) {
|
||||||
|
if cmd.sessionWriteModel.UserID == "" {
|
||||||
|
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Neil7", "Errors.User.UserIDMissing")
|
||||||
|
}
|
||||||
|
cmd.totpWriteModel = NewHumanTOTPWriteModel(cmd.sessionWriteModel.UserID, "")
|
||||||
|
err = cmd.eventstore.FilterToQueryReducer(ctx, cmd.totpWriteModel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cmd.totpWriteModel.State != domain.MFAStateReady {
|
||||||
|
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eej1U", "Errors.User.MFA.OTP.NotReady")
|
||||||
|
}
|
||||||
|
err = domain.VerifyTOTP(code, cmd.totpWriteModel.Secret, cmd.totpAlg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.TOTPChecked(ctx, cmd.now())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Exec will execute the commands specified and returns an error on the first occurrence
|
// Exec will execute the commands specified and returns an error on the first occurrence
|
||||||
func (s *SessionCommands) Exec(ctx context.Context) error {
|
func (s *SessionCommands) Exec(ctx context.Context) error {
|
||||||
for _, cmd := range s.sessionCommands {
|
for _, cmd := range s.sessionCommands {
|
||||||
@ -175,6 +200,10 @@ func (s *SessionCommands) WebAuthNChecked(ctx context.Context, checkedAt time.Ti
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SessionCommands) TOTPChecked(ctx context.Context, checkedAt time.Time) {
|
||||||
|
s.eventCommands = append(s.eventCommands, session.NewTOTPCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SessionCommands) SetToken(ctx context.Context, tokenID string) {
|
func (s *SessionCommands) SetToken(ctx context.Context, tokenID string) {
|
||||||
s.eventCommands = append(s.eventCommands, session.NewTokenSetEvent(ctx, s.sessionWriteModel.aggregate, tokenID))
|
s.eventCommands = append(s.eventCommands, session.NewTokenSetEvent(ctx, s.sessionWriteModel.aggregate, tokenID))
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ type SessionWriteModel struct {
|
|||||||
PasswordCheckedAt time.Time
|
PasswordCheckedAt time.Time
|
||||||
IntentCheckedAt time.Time
|
IntentCheckedAt time.Time
|
||||||
WebAuthNCheckedAt time.Time
|
WebAuthNCheckedAt time.Time
|
||||||
|
TOTPCheckedAt time.Time
|
||||||
WebAuthNUserVerified bool
|
WebAuthNUserVerified bool
|
||||||
Metadata map[string][]byte
|
Metadata map[string][]byte
|
||||||
State domain.SessionState
|
State domain.SessionState
|
||||||
@ -70,6 +71,8 @@ func (wm *SessionWriteModel) Reduce() error {
|
|||||||
wm.reduceWebAuthNChallenged(e)
|
wm.reduceWebAuthNChallenged(e)
|
||||||
case *session.WebAuthNCheckedEvent:
|
case *session.WebAuthNCheckedEvent:
|
||||||
wm.reduceWebAuthNChecked(e)
|
wm.reduceWebAuthNChecked(e)
|
||||||
|
case *session.TOTPCheckedEvent:
|
||||||
|
wm.reduceTOTPChecked(e)
|
||||||
case *session.TokenSetEvent:
|
case *session.TokenSetEvent:
|
||||||
wm.reduceTokenSet(e)
|
wm.reduceTokenSet(e)
|
||||||
case *session.TerminateEvent:
|
case *session.TerminateEvent:
|
||||||
@ -91,6 +94,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
|
|||||||
session.IntentCheckedType,
|
session.IntentCheckedType,
|
||||||
session.WebAuthNChallengedType,
|
session.WebAuthNChallengedType,
|
||||||
session.WebAuthNCheckedType,
|
session.WebAuthNCheckedType,
|
||||||
|
session.TOTPCheckedType,
|
||||||
session.TokenSetType,
|
session.TokenSetType,
|
||||||
session.MetadataSetType,
|
session.MetadataSetType,
|
||||||
session.TerminateType,
|
session.TerminateType,
|
||||||
@ -135,6 +139,10 @@ func (wm *SessionWriteModel) reduceWebAuthNChecked(e *session.WebAuthNCheckedEve
|
|||||||
wm.WebAuthNUserVerified = e.UserVerified
|
wm.WebAuthNUserVerified = e.UserVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wm *SessionWriteModel) reduceTOTPChecked(e *session.TOTPCheckedEvent) {
|
||||||
|
wm.TOTPCheckedAt = e.CheckedAt
|
||||||
|
}
|
||||||
|
|
||||||
func (wm *SessionWriteModel) reduceTokenSet(e *session.TokenSetEvent) {
|
func (wm *SessionWriteModel) reduceTokenSet(e *session.TokenSetEvent) {
|
||||||
wm.TokenID = e.TokenID
|
wm.TokenID = e.TokenID
|
||||||
}
|
}
|
||||||
@ -149,8 +157,8 @@ func (wm *SessionWriteModel) AuthenticationTime() time.Time {
|
|||||||
for _, check := range []time.Time{
|
for _, check := range []time.Time{
|
||||||
wm.PasswordCheckedAt,
|
wm.PasswordCheckedAt,
|
||||||
wm.WebAuthNCheckedAt,
|
wm.WebAuthNCheckedAt,
|
||||||
|
wm.TOTPCheckedAt,
|
||||||
wm.IntentCheckedAt,
|
wm.IntentCheckedAt,
|
||||||
// TODO: add OTP check https://github.com/zitadel/zitadel/issues/5477
|
|
||||||
// TODO: add OTP (sms and email) check https://github.com/zitadel/zitadel/issues/6224
|
// TODO: add OTP (sms and email) check https://github.com/zitadel/zitadel/issues/6224
|
||||||
} {
|
} {
|
||||||
if check.After(authTime) {
|
if check.After(authTime) {
|
||||||
@ -176,12 +184,9 @@ func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType {
|
|||||||
if !wm.IntentCheckedAt.IsZero() {
|
if !wm.IntentCheckedAt.IsZero() {
|
||||||
types = append(types, domain.UserAuthMethodTypeIDP)
|
types = append(types, domain.UserAuthMethodTypeIDP)
|
||||||
}
|
}
|
||||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
|
|
||||||
/*
|
|
||||||
if !wm.TOTPCheckedAt.IsZero() {
|
if !wm.TOTPCheckedAt.IsZero() {
|
||||||
types = append(types, domain.UserAuthMethodTypeTOTP)
|
types = append(types, domain.UserAuthMethodTypeTOTP)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
|
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
|
||||||
/*
|
/*
|
||||||
if !wm.TOTPFactor.OTPSMSCheckedAt.IsZero() {
|
if !wm.TOTPFactor.OTPSMSCheckedAt.IsZero() {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
@ -695,6 +696,138 @@ func TestCommands_updateSession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCheckTOTP(t *testing.T) {
|
||||||
|
ctx := authz.NewMockContext("", "org1", "user1")
|
||||||
|
|
||||||
|
cryptoAlg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||||
|
key, secret, err := domain.NewTOTPKey("example.com", "user1", cryptoAlg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sessAgg := &session.NewAggregate("session1", "org1").Aggregate
|
||||||
|
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||||
|
|
||||||
|
code, err := totp.GenerateCode(key.Secret(), testNow)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
sessionWriteModel *SessionWriteModel
|
||||||
|
eventstore func(*testing.T) *eventstore.Eventstore
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
code string
|
||||||
|
fields fields
|
||||||
|
wantEventCommands []eventstore.Command
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing userID",
|
||||||
|
code: code,
|
||||||
|
fields: fields{
|
||||||
|
sessionWriteModel: &SessionWriteModel{
|
||||||
|
aggregate: sessAgg,
|
||||||
|
},
|
||||||
|
eventstore: expectEventstore(),
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Neil7", "Errors.User.UserIDMissing"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "filter error",
|
||||||
|
code: code,
|
||||||
|
fields: fields{
|
||||||
|
sessionWriteModel: &SessionWriteModel{
|
||||||
|
UserID: "user1",
|
||||||
|
UserCheckedAt: testNow,
|
||||||
|
aggregate: sessAgg,
|
||||||
|
},
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilterError(io.ErrClosedPipe),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wantErr: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otp not ready error",
|
||||||
|
code: code,
|
||||||
|
fields: fields{
|
||||||
|
sessionWriteModel: &SessionWriteModel{
|
||||||
|
UserID: "user1",
|
||||||
|
UserCheckedAt: testNow,
|
||||||
|
aggregate: sessAgg,
|
||||||
|
},
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eej1U", "Errors.User.MFA.OTP.NotReady"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "otp verify error",
|
||||||
|
code: "foobar",
|
||||||
|
fields: fields{
|
||||||
|
sessionWriteModel: &SessionWriteModel{
|
||||||
|
UserID: "user1",
|
||||||
|
UserCheckedAt: testNow,
|
||||||
|
aggregate: sessAgg,
|
||||||
|
},
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
code: code,
|
||||||
|
fields: fields{
|
||||||
|
sessionWriteModel: &SessionWriteModel{
|
||||||
|
UserID: "user1",
|
||||||
|
UserCheckedAt: testNow,
|
||||||
|
aggregate: sessAgg,
|
||||||
|
},
|
||||||
|
eventstore: expectEventstore(
|
||||||
|
expectFilter(
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPAddedEvent(ctx, userAgg, secret),
|
||||||
|
),
|
||||||
|
eventFromEventPusher(
|
||||||
|
user.NewHumanOTPVerifiedEvent(ctx, userAgg, "agent1"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
wantEventCommands: []eventstore.Command{
|
||||||
|
session.NewTOTPCheckedEvent(ctx, sessAgg, testNow),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cmd := &SessionCommands{
|
||||||
|
sessionWriteModel: tt.fields.sessionWriteModel,
|
||||||
|
eventstore: tt.fields.eventstore(t),
|
||||||
|
totpAlg: cryptoAlg,
|
||||||
|
now: func() time.Time { return testNow },
|
||||||
|
}
|
||||||
|
err := CheckTOTP(tt.code)(ctx, cmd)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
assert.Equal(t, tt.wantEventCommands, cmd.eventCommands)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCommands_TerminateSession(t *testing.T) {
|
func TestCommands_TerminateSession(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
eventstore *eventstore.Eventstore
|
eventstore *eventstore.Eventstore
|
||||||
|
@ -239,7 +239,12 @@ func (s *Tester) WithInstanceAuthorization(ctx context.Context, u UserType, inst
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Tester) WithAuthorizationToken(ctx context.Context, token string) context.Context {
|
func (s *Tester) WithAuthorizationToken(ctx context.Context, token string) context.Context {
|
||||||
return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", token))
|
md, ok := metadata.FromOutgoingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
md = make(metadata.MD)
|
||||||
|
}
|
||||||
|
md.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
return metadata.NewOutgoingContext(ctx, md)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Tester) ensureSystemUser() {
|
func (s *Tester) ensureSystemUser() {
|
||||||
|
@ -30,6 +30,7 @@ const (
|
|||||||
SessionColumnIntentCheckedAt = "intent_checked_at"
|
SessionColumnIntentCheckedAt = "intent_checked_at"
|
||||||
SessionColumnWebAuthNCheckedAt = "webauthn_checked_at"
|
SessionColumnWebAuthNCheckedAt = "webauthn_checked_at"
|
||||||
SessionColumnWebAuthNUserVerified = "webauthn_user_verified"
|
SessionColumnWebAuthNUserVerified = "webauthn_user_verified"
|
||||||
|
SessionColumnTOTPCheckedAt = "totp_checked_at"
|
||||||
SessionColumnMetadata = "metadata"
|
SessionColumnMetadata = "metadata"
|
||||||
SessionColumnTokenID = "token_id"
|
SessionColumnTokenID = "token_id"
|
||||||
)
|
)
|
||||||
@ -58,6 +59,7 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi
|
|||||||
crdb.NewColumn(SessionColumnIntentCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
crdb.NewColumn(SessionColumnIntentCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||||
crdb.NewColumn(SessionColumnWebAuthNCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
crdb.NewColumn(SessionColumnWebAuthNCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||||
crdb.NewColumn(SessionColumnWebAuthNUserVerified, crdb.ColumnTypeBool, 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(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()),
|
||||||
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
|
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
|
||||||
},
|
},
|
||||||
@ -93,6 +95,10 @@ func (p *sessionProjection) reducers() []handler.AggregateReducer {
|
|||||||
Event: session.WebAuthNCheckedType,
|
Event: session.WebAuthNCheckedType,
|
||||||
Reduce: p.reduceWebAuthNChecked,
|
Reduce: p.reduceWebAuthNChecked,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Event: session.TOTPCheckedType,
|
||||||
|
Reduce: p.reduceTOTPChecked,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Event: session.TokenSetType,
|
Event: session.TokenSetType,
|
||||||
Reduce: p.reduceTokenSet,
|
Reduce: p.reduceTokenSet,
|
||||||
@ -229,6 +235,26 @@ func (p *sessionProjection) reduceWebAuthNChecked(event eventstore.Event) (*hand
|
|||||||
), nil
|
), 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) {
|
func (p *sessionProjection) reduceTokenSet(event eventstore.Event) (*handler.Statement, error) {
|
||||||
e, ok := event.(*session.TokenSetEvent)
|
e, ok := event.(*session.TokenSetEvent)
|
||||||
if !ok {
|
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",
|
name: "instance reduceTokenSet",
|
||||||
args: args{
|
args: args{
|
||||||
|
@ -34,6 +34,7 @@ type Session struct {
|
|||||||
PasswordFactor SessionPasswordFactor
|
PasswordFactor SessionPasswordFactor
|
||||||
IntentFactor SessionIntentFactor
|
IntentFactor SessionIntentFactor
|
||||||
WebAuthNFactor SessionWebAuthNFactor
|
WebAuthNFactor SessionWebAuthNFactor
|
||||||
|
TOTPFactor SessionTOTPFactor
|
||||||
Metadata map[string][]byte
|
Metadata map[string][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +59,10 @@ type SessionWebAuthNFactor struct {
|
|||||||
UserVerified bool
|
UserVerified bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SessionTOTPFactor struct {
|
||||||
|
TOTPCheckedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
type SessionsSearchQueries struct {
|
type SessionsSearchQueries struct {
|
||||||
SearchRequest
|
SearchRequest
|
||||||
Queries []SearchQuery
|
Queries []SearchQuery
|
||||||
@ -132,6 +137,10 @@ var (
|
|||||||
name: projection.SessionColumnWebAuthNUserVerified,
|
name: projection.SessionColumnWebAuthNUserVerified,
|
||||||
table: sessionsTable,
|
table: sessionsTable,
|
||||||
}
|
}
|
||||||
|
SessionColumnTOTPCheckedAt = Column{
|
||||||
|
name: projection.SessionColumnTOTPCheckedAt,
|
||||||
|
table: sessionsTable,
|
||||||
|
}
|
||||||
SessionColumnMetadata = Column{
|
SessionColumnMetadata = Column{
|
||||||
name: projection.SessionColumnMetadata,
|
name: projection.SessionColumnMetadata,
|
||||||
table: sessionsTable,
|
table: sessionsTable,
|
||||||
@ -230,6 +239,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
|||||||
SessionColumnIntentCheckedAt.identifier(),
|
SessionColumnIntentCheckedAt.identifier(),
|
||||||
SessionColumnWebAuthNCheckedAt.identifier(),
|
SessionColumnWebAuthNCheckedAt.identifier(),
|
||||||
SessionColumnWebAuthNUserVerified.identifier(),
|
SessionColumnWebAuthNUserVerified.identifier(),
|
||||||
|
SessionColumnTOTPCheckedAt.identifier(),
|
||||||
SessionColumnMetadata.identifier(),
|
SessionColumnMetadata.identifier(),
|
||||||
SessionColumnToken.identifier(),
|
SessionColumnToken.identifier(),
|
||||||
).From(sessionsTable.identifier()).
|
).From(sessionsTable.identifier()).
|
||||||
@ -249,6 +259,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
|||||||
intentCheckedAt sql.NullTime
|
intentCheckedAt sql.NullTime
|
||||||
webAuthNCheckedAt sql.NullTime
|
webAuthNCheckedAt sql.NullTime
|
||||||
webAuthNUserPresent sql.NullBool
|
webAuthNUserPresent sql.NullBool
|
||||||
|
totpCheckedAt sql.NullTime
|
||||||
metadata database.Map[[]byte]
|
metadata database.Map[[]byte]
|
||||||
token sql.NullString
|
token sql.NullString
|
||||||
)
|
)
|
||||||
@ -270,6 +281,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
|||||||
&intentCheckedAt,
|
&intentCheckedAt,
|
||||||
&webAuthNCheckedAt,
|
&webAuthNCheckedAt,
|
||||||
&webAuthNUserPresent,
|
&webAuthNUserPresent,
|
||||||
|
&totpCheckedAt,
|
||||||
&metadata,
|
&metadata,
|
||||||
&token,
|
&token,
|
||||||
)
|
)
|
||||||
@ -290,6 +302,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
|||||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||||
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
|
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
|
||||||
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
||||||
|
session.TOTPFactor.TOTPCheckedAt = totpCheckedAt.Time
|
||||||
session.Metadata = metadata
|
session.Metadata = metadata
|
||||||
|
|
||||||
return session, token.String, nil
|
return session, token.String, nil
|
||||||
@ -314,6 +327,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
|||||||
SessionColumnIntentCheckedAt.identifier(),
|
SessionColumnIntentCheckedAt.identifier(),
|
||||||
SessionColumnWebAuthNCheckedAt.identifier(),
|
SessionColumnWebAuthNCheckedAt.identifier(),
|
||||||
SessionColumnWebAuthNUserVerified.identifier(),
|
SessionColumnWebAuthNUserVerified.identifier(),
|
||||||
|
SessionColumnTOTPCheckedAt.identifier(),
|
||||||
SessionColumnMetadata.identifier(),
|
SessionColumnMetadata.identifier(),
|
||||||
countColumn.identifier(),
|
countColumn.identifier(),
|
||||||
).From(sessionsTable.identifier()).
|
).From(sessionsTable.identifier()).
|
||||||
@ -336,6 +350,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
|||||||
intentCheckedAt sql.NullTime
|
intentCheckedAt sql.NullTime
|
||||||
webAuthNCheckedAt sql.NullTime
|
webAuthNCheckedAt sql.NullTime
|
||||||
webAuthNUserPresent sql.NullBool
|
webAuthNUserPresent sql.NullBool
|
||||||
|
totpCheckedAt sql.NullTime
|
||||||
metadata database.Map[[]byte]
|
metadata database.Map[[]byte]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -356,6 +371,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
|||||||
&intentCheckedAt,
|
&intentCheckedAt,
|
||||||
&webAuthNCheckedAt,
|
&webAuthNCheckedAt,
|
||||||
&webAuthNUserPresent,
|
&webAuthNUserPresent,
|
||||||
|
&totpCheckedAt,
|
||||||
&metadata,
|
&metadata,
|
||||||
&sessions.Count,
|
&sessions.Count,
|
||||||
)
|
)
|
||||||
@ -372,6 +388,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
|||||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||||
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
|
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
|
||||||
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
|
||||||
|
session.TOTPFactor.TOTPCheckedAt = totpCheckedAt.Time
|
||||||
session.Metadata = metadata
|
session.Metadata = metadata
|
||||||
|
|
||||||
sessions.Sessions = append(sessions.Sessions, session)
|
sessions.Sessions = append(sessions.Sessions, session)
|
||||||
|
@ -33,6 +33,7 @@ var (
|
|||||||
` projections.sessions4.intent_checked_at,` +
|
` projections.sessions4.intent_checked_at,` +
|
||||||
` projections.sessions4.webauthn_checked_at,` +
|
` projections.sessions4.webauthn_checked_at,` +
|
||||||
` projections.sessions4.webauthn_user_verified,` +
|
` projections.sessions4.webauthn_user_verified,` +
|
||||||
|
` projections.sessions4.totp_checked_at,` +
|
||||||
` projections.sessions4.metadata,` +
|
` projections.sessions4.metadata,` +
|
||||||
` projections.sessions4.token_id` +
|
` projections.sessions4.token_id` +
|
||||||
` FROM projections.sessions4` +
|
` FROM projections.sessions4` +
|
||||||
@ -56,6 +57,7 @@ var (
|
|||||||
` projections.sessions4.intent_checked_at,` +
|
` projections.sessions4.intent_checked_at,` +
|
||||||
` projections.sessions4.webauthn_checked_at,` +
|
` projections.sessions4.webauthn_checked_at,` +
|
||||||
` projections.sessions4.webauthn_user_verified,` +
|
` projections.sessions4.webauthn_user_verified,` +
|
||||||
|
` projections.sessions4.totp_checked_at,` +
|
||||||
` projections.sessions4.metadata,` +
|
` projections.sessions4.metadata,` +
|
||||||
` COUNT(*) OVER ()` +
|
` COUNT(*) OVER ()` +
|
||||||
` FROM projections.sessions4` +
|
` FROM projections.sessions4` +
|
||||||
@ -81,6 +83,7 @@ var (
|
|||||||
"intent_checked_at",
|
"intent_checked_at",
|
||||||
"webauthn_checked_at",
|
"webauthn_checked_at",
|
||||||
"webauthn_user_verified",
|
"webauthn_user_verified",
|
||||||
|
"totp_checked_at",
|
||||||
"metadata",
|
"metadata",
|
||||||
"token",
|
"token",
|
||||||
}
|
}
|
||||||
@ -102,6 +105,7 @@ var (
|
|||||||
"intent_checked_at",
|
"intent_checked_at",
|
||||||
"webauthn_checked_at",
|
"webauthn_checked_at",
|
||||||
"webauthn_user_verified",
|
"webauthn_user_verified",
|
||||||
|
"totp_checked_at",
|
||||||
"metadata",
|
"metadata",
|
||||||
"count",
|
"count",
|
||||||
}
|
}
|
||||||
@ -155,6 +159,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
|||||||
testNow,
|
testNow,
|
||||||
testNow,
|
testNow,
|
||||||
true,
|
true,
|
||||||
|
testNow,
|
||||||
[]byte(`{"key": "dmFsdWU="}`),
|
[]byte(`{"key": "dmFsdWU="}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -190,6 +195,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
|||||||
WebAuthNCheckedAt: testNow,
|
WebAuthNCheckedAt: testNow,
|
||||||
UserVerified: true,
|
UserVerified: true,
|
||||||
},
|
},
|
||||||
|
TOTPFactor: SessionTOTPFactor{
|
||||||
|
TOTPCheckedAt: testNow,
|
||||||
|
},
|
||||||
Metadata: map[string][]byte{
|
Metadata: map[string][]byte{
|
||||||
"key": []byte("value"),
|
"key": []byte("value"),
|
||||||
},
|
},
|
||||||
@ -222,6 +230,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
|||||||
testNow,
|
testNow,
|
||||||
testNow,
|
testNow,
|
||||||
true,
|
true,
|
||||||
|
testNow,
|
||||||
[]byte(`{"key": "dmFsdWU="}`),
|
[]byte(`{"key": "dmFsdWU="}`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -241,6 +250,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
|||||||
testNow,
|
testNow,
|
||||||
testNow,
|
testNow,
|
||||||
false,
|
false,
|
||||||
|
testNow,
|
||||||
[]byte(`{"key": "dmFsdWU="}`),
|
[]byte(`{"key": "dmFsdWU="}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -276,6 +286,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
|||||||
WebAuthNCheckedAt: testNow,
|
WebAuthNCheckedAt: testNow,
|
||||||
UserVerified: true,
|
UserVerified: true,
|
||||||
},
|
},
|
||||||
|
TOTPFactor: SessionTOTPFactor{
|
||||||
|
TOTPCheckedAt: testNow,
|
||||||
|
},
|
||||||
Metadata: map[string][]byte{
|
Metadata: map[string][]byte{
|
||||||
"key": []byte("value"),
|
"key": []byte("value"),
|
||||||
},
|
},
|
||||||
@ -305,6 +318,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
|||||||
WebAuthNCheckedAt: testNow,
|
WebAuthNCheckedAt: testNow,
|
||||||
UserVerified: false,
|
UserVerified: false,
|
||||||
},
|
},
|
||||||
|
TOTPFactor: SessionTOTPFactor{
|
||||||
|
TOTPCheckedAt: testNow,
|
||||||
|
},
|
||||||
Metadata: map[string][]byte{
|
Metadata: map[string][]byte{
|
||||||
"key": []byte("value"),
|
"key": []byte("value"),
|
||||||
},
|
},
|
||||||
@ -390,6 +406,7 @@ func Test_SessionPrepare(t *testing.T) {
|
|||||||
testNow,
|
testNow,
|
||||||
testNow,
|
testNow,
|
||||||
true,
|
true,
|
||||||
|
testNow,
|
||||||
[]byte(`{"key": "dmFsdWU="}`),
|
[]byte(`{"key": "dmFsdWU="}`),
|
||||||
"tokenID",
|
"tokenID",
|
||||||
},
|
},
|
||||||
@ -420,6 +437,9 @@ func Test_SessionPrepare(t *testing.T) {
|
|||||||
WebAuthNCheckedAt: testNow,
|
WebAuthNCheckedAt: testNow,
|
||||||
UserVerified: true,
|
UserVerified: true,
|
||||||
},
|
},
|
||||||
|
TOTPFactor: SessionTOTPFactor{
|
||||||
|
TOTPCheckedAt: testNow,
|
||||||
|
},
|
||||||
Metadata: map[string][]byte{
|
Metadata: map[string][]byte{
|
||||||
"key": []byte("value"),
|
"key": []byte("value"),
|
||||||
},
|
},
|
||||||
|
@ -9,6 +9,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
|||||||
RegisterFilterEventMapper(AggregateType, IntentCheckedType, IntentCheckedEventMapper).
|
RegisterFilterEventMapper(AggregateType, IntentCheckedType, IntentCheckedEventMapper).
|
||||||
RegisterFilterEventMapper(AggregateType, WebAuthNChallengedType, eventstore.GenericEventMapper[WebAuthNChallengedEvent]).
|
RegisterFilterEventMapper(AggregateType, WebAuthNChallengedType, eventstore.GenericEventMapper[WebAuthNChallengedEvent]).
|
||||||
RegisterFilterEventMapper(AggregateType, WebAuthNCheckedType, eventstore.GenericEventMapper[WebAuthNCheckedEvent]).
|
RegisterFilterEventMapper(AggregateType, WebAuthNCheckedType, eventstore.GenericEventMapper[WebAuthNCheckedEvent]).
|
||||||
|
RegisterFilterEventMapper(AggregateType, TOTPCheckedType, eventstore.GenericEventMapper[TOTPCheckedEvent]).
|
||||||
RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper).
|
RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper).
|
||||||
RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper).
|
RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper).
|
||||||
RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper)
|
RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper)
|
||||||
|
@ -19,6 +19,7 @@ const (
|
|||||||
IntentCheckedType = sessionEventPrefix + "intent.checked"
|
IntentCheckedType = sessionEventPrefix + "intent.checked"
|
||||||
WebAuthNChallengedType = sessionEventPrefix + "webAuthN.challenged"
|
WebAuthNChallengedType = sessionEventPrefix + "webAuthN.challenged"
|
||||||
WebAuthNCheckedType = sessionEventPrefix + "webAuthN.checked"
|
WebAuthNCheckedType = sessionEventPrefix + "webAuthN.checked"
|
||||||
|
TOTPCheckedType = sessionEventPrefix + "totp.checked"
|
||||||
TokenSetType = sessionEventPrefix + "token.set"
|
TokenSetType = sessionEventPrefix + "token.set"
|
||||||
MetadataSetType = sessionEventPrefix + "metadata.set"
|
MetadataSetType = sessionEventPrefix + "metadata.set"
|
||||||
TerminateType = sessionEventPrefix + "terminated"
|
TerminateType = sessionEventPrefix + "terminated"
|
||||||
@ -264,6 +265,39 @@ func NewWebAuthNCheckedEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TOTPCheckedEvent struct {
|
||||||
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
CheckedAt time.Time `json:"checkedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TOTPCheckedEvent) Data() interface{} {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TOTPCheckedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *TOTPCheckedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
|
||||||
|
e.BaseEvent = *base
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTOTPCheckedEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
aggregate *eventstore.Aggregate,
|
||||||
|
checkedAt time.Time,
|
||||||
|
) *TOTPCheckedEvent {
|
||||||
|
return &TOTPCheckedEvent{
|
||||||
|
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||||
|
ctx,
|
||||||
|
aggregate,
|
||||||
|
TOTPCheckedType,
|
||||||
|
),
|
||||||
|
CheckedAt: checkedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type TokenSetEvent struct {
|
type TokenSetEvent struct {
|
||||||
eventstore.BaseEvent `json:"-"`
|
eventstore.BaseEvent `json:"-"`
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ message Factors {
|
|||||||
PasswordFactor password = 2;
|
PasswordFactor password = 2;
|
||||||
WebAuthNFactor web_auth_n = 3;
|
WebAuthNFactor web_auth_n = 3;
|
||||||
IntentFactor intent = 4;
|
IntentFactor intent = 4;
|
||||||
|
TOTPFactor totp = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UserFactor {
|
message UserFactor {
|
||||||
@ -101,6 +102,14 @@ message WebAuthNFactor {
|
|||||||
bool user_verified = 2;
|
bool user_verified = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message TOTPFactor {
|
||||||
|
google.protobuf.Timestamp verified_at = 1 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "\"time when the Time-based One-Time Password was last checked\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
message SearchQuery {
|
message SearchQuery {
|
||||||
oneof query {
|
oneof query {
|
||||||
option (validate.required) = true;
|
option (validate.required) = true;
|
||||||
|
@ -346,6 +346,11 @@ message Checks {
|
|||||||
description: "\"Checks the intent. Requires that the userlink is already checked and a successful intent.\"";
|
description: "\"Checks the intent. Requires that the userlink is already checked and a successful intent.\"";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
optional CheckTOTP totp = 5 [
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
description: "\"Checks the Time-based One-Time Password and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
message CheckUser {
|
message CheckUser {
|
||||||
@ -412,3 +417,14 @@ message CheckIntent {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message CheckTOTP {
|
||||||
|
string totp = 1 [
|
||||||
|
(validate.rules).string = {min_len: 6, max_len: 6},
|
||||||
|
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||||
|
min_length: 6;
|
||||||
|
max_length: 6;
|
||||||
|
example: "\"323764\"";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user