feat(api/v2): implement U2F session check (#6339)

This commit is contained in:
Tim Möhlmann 2023-08-11 18:36:18 +03:00 committed by GitHub
parent 4e0c3115fe
commit 86af67d1be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1035 additions and 665 deletions

1
.gitignore vendored
View File

@ -64,7 +64,6 @@ docs/docs/apis/proto
**/.sass-cache
/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css
/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map
zitadel
zitadel-*-*
# local

View File

@ -90,12 +90,15 @@ clean:
core_unit_test:
go test -race -coverprofile=profile.cov ./...
.PHONY: core_integration_test
core_integration_test:
.PHONY: core_integration_setup
core_integration_setup:
go build -o zitadel main.go
./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
$(RM) zitadel
.PHONY: core_integration_test
core_integration_test: core_integration_setup
go test -tags=integration -race -p 1 -v -coverprofile=profile.cov -coverpkg=./internal/...,./cmd/... ./internal/integration ./internal/api/grpc/... ./internal/notification/handlers/... ./internal/api/oidc/...
.PHONY: console_lint

View File

@ -47,7 +47,7 @@ func (s *Server) CreateSession(ctx context.Context, req *session.CreateSessionRe
}
challengeResponse, cmds := s.challengesToCommand(req.GetChallenges(), checks)
set, err := s.command.CreateSession(ctx, cmds, req.GetDomain(), metadata)
set, err := s.command.CreateSession(ctx, cmds, metadata)
if err != nil {
return nil, err
}
@ -107,7 +107,6 @@ func sessionToPb(s *query.Session) *session.Session {
Sequence: s.Sequence,
Factors: factorsToPb(s),
Metadata: s.Metadata,
Domain: s.Domain,
}
}
@ -119,7 +118,7 @@ func factorsToPb(s *query.Session) *session.Factors {
return &session.Factors{
User: user,
Password: passwordFactorToPb(s.PasswordFactor),
Passkey: passkeyFactorToPb(s.PasskeyFactor),
WebAuthN: webAuthNFactorToPb(s.WebAuthNFactor),
Intent: intentFactorToPb(s.IntentFactor),
}
}
@ -142,12 +141,13 @@ func intentFactorToPb(factor query.SessionIntentFactor) *session.IntentFactor {
}
}
func passkeyFactorToPb(factor query.SessionPasskeyFactor) *session.PasskeyFactor {
if factor.PasskeyCheckedAt.IsZero() {
func webAuthNFactorToPb(factor query.SessionWebAuthNFactor) *session.WebAuthNFactor {
if factor.WebAuthNCheckedAt.IsZero() {
return nil
}
return &session.PasskeyFactor{
VerifiedAt: timestamppb.New(factor.PasskeyCheckedAt),
return &session.WebAuthNFactor{
VerifiedAt: timestamppb.New(factor.WebAuthNCheckedAt),
UserVerified: factor.UserVerified,
}
}
@ -244,36 +244,47 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
if intent := checks.GetIntent(); intent != nil {
sessionChecks = append(sessionChecks, command.CheckIntent(intent.GetIntentId(), intent.GetToken()))
}
if passkey := checks.GetPasskey(); passkey != nil {
sessionChecks = append(sessionChecks, s.command.CheckPasskey(passkey.GetCredentialAssertionData()))
if passkey := checks.GetWebAuthN(); passkey != nil {
sessionChecks = append(sessionChecks, s.command.CheckWebAuthN(passkey.GetCredentialAssertionData()))
}
return sessionChecks, nil
}
func (s *Server) challengesToCommand(challenges []session.ChallengeKind, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand) {
if len(challenges) == 0 {
func (s *Server) challengesToCommand(challenges *session.RequestChallenges, cmds []command.SessionCommand) (*session.Challenges, []command.SessionCommand) {
if challenges == nil {
return nil, cmds
}
resp := new(session.Challenges)
for _, c := range challenges {
switch c {
case session.ChallengeKind_CHALLENGE_KIND_UNSPECIFIED:
continue
case session.ChallengeKind_CHALLENGE_KIND_PASSKEY:
passkeyChallenge, cmd := s.createPasskeyChallengeCommand()
resp.Passkey = passkeyChallenge
cmds = append(cmds, cmd)
}
if req := challenges.GetWebAuthN(); req != nil {
challenge, cmd := s.createWebAuthNChallengeCommand(req)
resp.WebAuthN = challenge
cmds = append(cmds, cmd)
}
return resp, cmds
}
func (s *Server) createPasskeyChallengeCommand() (*session.Challenges_Passkey, command.SessionCommand) {
challenge := &session.Challenges_Passkey{
func (s *Server) createWebAuthNChallengeCommand(req *session.RequestChallenges_WebAuthN) (*session.Challenges_WebAuthN, command.SessionCommand) {
challenge := &session.Challenges_WebAuthN{
PublicKeyCredentialRequestOptions: new(structpb.Struct),
}
return challenge, s.command.CreatePasskeyChallenge(domain.UserVerificationRequirementRequired, challenge.PublicKeyCredentialRequestOptions)
userVerification := userVerificationRequirementToDomain(req.GetUserVerificationRequirement())
return challenge, s.command.CreateWebAuthNChallenge(userVerification, req.GetDomain(), challenge.PublicKeyCredentialRequestOptions)
}
func userVerificationRequirementToDomain(req session.UserVerificationRequirement) domain.UserVerificationRequirement {
switch req {
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED:
return domain.UserVerificationRequirementUnspecified
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED:
return domain.UserVerificationRequirementRequired
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED:
return domain.UserVerificationRequirementPreferred
case session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED:
return domain.UserVerificationRequirementDiscouraged
default:
return domain.UserVerificationRequirementUnspecified
}
}
func userCheck(user *session.CheckUser) (userSearch, error) {

View File

@ -69,7 +69,8 @@ type wantFactor int
const (
wantUserFactor wantFactor = iota
wantPasswordFactor
wantPasskeyFactor
wantWebAuthNFactor
wantWebAuthNFactorUserVerified
wantIntentFactor
)
@ -85,10 +86,16 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration,
pf := factors.GetPassword()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
case wantPasskeyFactor:
pf := factors.GetPasskey()
case wantWebAuthNFactor:
pf := factors.GetWebAuthN()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.False(t, pf.UserVerified)
case wantWebAuthNFactorUserVerified:
pf := factors.GetWebAuthN()
assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
assert.True(t, pf.UserVerified)
case wantIntentFactor:
pf := factors.GetIntent()
assert.NotNil(t, pf)
@ -127,7 +134,6 @@ func TestServer_CreateSession(t *testing.T) {
},
},
Metadata: map[string][]byte{"foo": []byte("bar")},
Domain: "domain",
},
want: &session.CreateSessionResponse{
Details: &object.Details{
@ -150,8 +156,11 @@ func TestServer_CreateSession(t *testing.T) {
{
name: "passkey without user error",
req: &session.CreateSessionRequest{
Challenges: []session.ChallengeKind{
session.ChallengeKind_CHALLENGE_KIND_PASSKEY,
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
},
wantErr: true,
@ -166,8 +175,10 @@ func TestServer_CreateSession(t *testing.T) {
},
},
},
Challenges: []session.ChallengeKind{
session.ChallengeKind_CHALLENGE_KIND_PASSKEY,
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
},
wantErr: true,
@ -188,8 +199,8 @@ func TestServer_CreateSession(t *testing.T) {
}
}
func TestServer_CreateSession_passkey(t *testing.T) {
// create new session with user and request the passkey challenge
func TestServer_CreateSession_webauthn(t *testing.T) {
// create new session with user and request the webauthn challenge
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
@ -198,29 +209,31 @@ func TestServer_CreateSession_passkey(t *testing.T) {
},
},
},
Challenges: []session.ChallengeKind{
session.ChallengeKind_CHALLENGE_KIND_PASSKEY,
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
Domain: Tester.Config.ExternalDomain,
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil)
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetPasskey().GetPublicKeyCredentialRequestOptions())
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
// update the session with passkey assertion data
// update the session with webauthn assertion data
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: createResp.GetSessionToken(),
Checks: &session.Checks{
Passkey: &session.CheckPasskey{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantPasskeyFactor)
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantWebAuthNFactorUserVerified)
}
func TestServer_CreateSession_successfulIntent(t *testing.T) {
@ -326,16 +339,14 @@ func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
}
func TestServer_SetSession_flow(t *testing.T) {
var wantFactors []wantFactor
// create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{Domain: Tester.Config.ExternalDomain})
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
sessionToken := createResp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, createResp.GetDetails().GetSequence(), time.Minute, nil)
t.Run("check user", func(t *testing.T) {
wantFactors = append(wantFactors, wantUserFactor)
wantFactors := []wantFactor{wantUserFactor}
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
@ -348,43 +359,92 @@ func TestServer_SetSession_flow(t *testing.T) {
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
})
t.Run("check passkey", func(t *testing.T) {
t.Run("check webauthn, user verified (passkey)", func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Challenges: []session.ChallengeKind{
session.ChallengeKind_CHALLENGE_KIND_PASSKEY,
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
sessionToken = resp.GetSessionToken()
wantFactors = append(wantFactors, wantPasskeyFactor)
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetPasskey().GetPublicKeyCredentialRequestOptions())
wantFactors := []wantFactor{wantUserFactor, wantWebAuthNFactorUserVerified}
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Checks: &session.Checks{
Passkey: &session.CheckPasskey{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
})
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{
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED,
session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED,
} {
t.Run(userVerificationRequirement.String(), func(t *testing.T) {
resp, err := Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: Tester.Config.ExternalDomain,
UserVerificationRequirement: userVerificationRequirement,
},
},
})
require.NoError(t, err)
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil)
sessionToken = resp.GetSessionToken()
wantFactors := []wantFactor{wantUserFactor, wantWebAuthNFactor}
assertionData, err := Tester.WebAuthN.CreateAssertionResponse(resp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), false)
require.NoError(t, err)
resp, err = Client.SetSession(CTX, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: sessionToken,
Checks: &session.Checks{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertionData,
},
},
})
require.NoError(t, err)
sessionToken = resp.GetSessionToken()
verifyCurrentSession(t, createResp.GetSessionId(), sessionToken, resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
})
}
})
}
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
// create new, empty session
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{Domain: Tester.Config.ExternalDomain})
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{})
require.NoError(t, err)
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
@ -403,16 +463,19 @@ func Test_ZITADEL_API_missing_mfa(t *testing.T) {
}
func Test_ZITADEL_API_success(t *testing.T) {
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
id, token, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, User.GetUserId())
ctx := Tester.WithAuthorizationToken(context.Background(), token)
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
require.NoError(t, err)
require.NotNil(t, id, sessionResp.GetSession().GetFactors().GetPasskey().GetVerifiedAt().AsTime())
webAuthN := sessionResp.GetSession().GetFactors().GetWebAuthN()
require.NotNil(t, id, webAuthN.GetVerifiedAt().AsTime())
require.True(t, webAuthN.GetUserVerified())
}
func Test_ZITADEL_API_session_not_found(t *testing.T) {
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
id, token, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, User.GetUserId())
// test session token works
ctx := Tester.WithAuthorizationToken(context.Background(), token)

View File

@ -70,7 +70,7 @@ func Test_sessionsToPb(t *testing.T) {
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // passkey factor
{ // webAuthN factor
ID: "999",
CreationDate: now,
ChangeDate: now,
@ -85,8 +85,9 @@ func Test_sessionsToPb(t *testing.T) {
DisplayName: "donald duck",
ResourceOwner: "org1",
},
PasskeyFactor: query.SessionPasskeyFactor{
PasskeyCheckedAt: past,
WebAuthNFactor: query.SessionWebAuthNFactor{
WebAuthNCheckedAt: past,
UserVerified: true,
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
@ -136,7 +137,7 @@ func Test_sessionsToPb(t *testing.T) {
},
Metadata: map[string][]byte{"hello": []byte("world")},
},
{ // passkey factor
{ // webAuthN factor
Id: "999",
CreationDate: timestamppb.New(now),
ChangeDate: timestamppb.New(now),
@ -149,8 +150,9 @@ func Test_sessionsToPb(t *testing.T) {
DisplayName: "donald duck",
OrganisationId: "org1",
},
Passkey: &session.PasskeyFactor{
VerifiedAt: timestamppb.New(past),
WebAuthN: &session.WebAuthNFactor{
VerifiedAt: timestamppb.New(past),
UserVerified: true,
},
},
Metadata: map[string][]byte{"hello": []byte("world")},
@ -432,3 +434,40 @@ func Test_userCheck(t *testing.T) {
})
}
}
func Test_userVerificationRequirementToDomain(t *testing.T) {
type args struct {
req session.UserVerificationRequirement
}
tests := []struct {
args args
want domain.UserVerificationRequirement
}{
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_UNSPECIFIED},
want: domain.UserVerificationRequirementUnspecified,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED},
want: domain.UserVerificationRequirementRequired,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_PREFERRED},
want: domain.UserVerificationRequirementPreferred,
},
{
args: args{session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_DISCOURAGED},
want: domain.UserVerificationRequirementDiscouraged,
},
{
args: args{999},
want: domain.UserVerificationRequirementUnspecified,
},
}
for _, tt := range tests {
t.Run(tt.args.req.String(), func(t *testing.T) {
got := userVerificationRequirementToDomain(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -16,13 +16,13 @@ import (
func TestServer_AddOTPSMS(t *testing.T) {
userID := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userID)
_, sessionToken, _, _ := Tester.CreatePasskeySession(t, CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID)
// TODO: add when phone can be added to user
/*
userIDPhone := Tester.CreateHumanUser(CTX).GetUserId()
Tester.RegisterUserPasskey(CTX, userIDPhone)
_, sessionTokenPhone, _, _ := Tester.CreatePasskeySession(t, CTX, userIDPhone)
_, sessionTokenPhone, _, _ := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTX, userID)
_, sessionToken, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userID)
userVerified := Tester.CreateHumanUser(CTX)
Tester.RegisterUserPasskey(CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreatePasskeySession(t, CTX, userVerified.GetUserId())
_, sessionTokenVerified, _, _ := Tester.CreateVerfiedWebAuthNSession(t, CTX, userVerified.GetUserId())
userVerifiedCtx := Tester.WithAuthorizationToken(context.Background(), sessionTokenVerified)
_, err := Tester.Client.UserV2.VerifyEmail(userVerifiedCtx, &user.VerifyEmailRequest{
UserId: userVerified.GetUserId(),

View File

@ -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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{

View File

@ -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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{

View File

@ -57,7 +57,7 @@ func TestMain(m *testing.M) {
func Test_ZITADEL_API_missing_audience_scope(t *testing.T) {
clientID := createClient(t)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -92,7 +92,6 @@ func Test_ZITADEL_API_missing_authentication(t *testing.T) {
Search: &session.CheckUser_UserId{UserId: User.GetUserId()},
},
},
Domain: Tester.Config.ExternalDomain,
})
require.NoError(t, err)
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
@ -149,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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -178,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.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
@ -220,7 +219,7 @@ func Test_ZITADEL_API_terminated_session(t *testing.T) {
provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
require.NoError(t, err)
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
sessionID, sessionToken, startTime, changeTime := Tester.CreateVerfiedWebAuthNSession(t, CTXLOGIN, User.GetUserId())
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
AuthRequestId: authRequestID,
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{

View File

@ -190,8 +190,12 @@ func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType
if !session.PasswordFactor.PasswordCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePassword)
}
if !session.PasskeyFactor.PasskeyCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePasswordless)
if !session.WebAuthNFactor.WebAuthNCheckedAt.IsZero() {
if session.WebAuthNFactor.UserVerified {
types = append(types, domain.UserAuthMethodTypePasswordless)
} else {
types = append(types, domain.UserAuthMethodTypeU2F)
}
}
if !session.IntentFactor.IntentCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeIDP)
@ -201,9 +205,6 @@ func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeTOTP)
}
if !session.U2FFactor.U2FCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeU2F)
}
*/
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
/*

View File

@ -358,7 +358,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate)),
),
),
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
@ -401,7 +401,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate)),
),
),
tokenVerifier: func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
@ -444,7 +444,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate),
),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,
@ -523,7 +523,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
session.NewAddedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate),
),
eventFromEventPusher(
session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "org1").Aggregate,

View File

@ -164,7 +164,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, "domain.tld"),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate),
),
eventFromEventPusher(
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,
@ -365,7 +365,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
),
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate, "domain.tld"),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate),
),
eventFromEventPusher(
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "instanceID").Aggregate,

View File

@ -15,7 +15,6 @@ import (
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/session"
"github.com/zitadel/zitadel/internal/repository/user"
usr_repo "github.com/zitadel/zitadel/internal/repository/user"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
@ -138,10 +137,8 @@ func (s *SessionCommands) Exec(ctx context.Context) error {
return nil
}
func (s *SessionCommands) Start(ctx context.Context, domain string) {
s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate, domain))
// set the domain so checks can use it
s.sessionWriteModel.Domain = domain
func (s *SessionCommands) Start(ctx context.Context) {
s.eventCommands = append(s.eventCommands, session.NewAddedEvent(ctx, s.sessionWriteModel.aggregate))
}
func (s *SessionCommands) UserChecked(ctx context.Context, userID string, checkedAt time.Time) error {
@ -159,15 +156,23 @@ func (s *SessionCommands) IntentChecked(ctx context.Context, checkedAt time.Time
s.eventCommands = append(s.eventCommands, session.NewIntentCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt))
}
func (s *SessionCommands) PasskeyChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement) {
s.eventCommands = append(s.eventCommands, session.NewPasskeyChallengedEvent(ctx, s.sessionWriteModel.aggregate, challenge, allowedCrentialIDs, userVerification))
func (s *SessionCommands) WebAuthNChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement, rpid string) {
s.eventCommands = append(s.eventCommands, session.NewWebAuthNChallengedEvent(ctx, s.sessionWriteModel.aggregate, challenge, allowedCrentialIDs, userVerification, rpid))
}
func (s *SessionCommands) PasskeyChecked(ctx context.Context, checkedAt time.Time, tokenID string, signCount uint32) {
func (s *SessionCommands) WebAuthNChecked(ctx context.Context, checkedAt time.Time, tokenID string, signCount uint32, userVerified bool) {
s.eventCommands = append(s.eventCommands,
session.NewPasskeyCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt),
usr_repo.NewHumanPasswordlessSignCountChangedEvent(ctx, s.sessionWriteModel.aggregate, tokenID, signCount),
session.NewWebAuthNCheckedEvent(ctx, s.sessionWriteModel.aggregate, checkedAt, userVerified),
)
if s.sessionWriteModel.WebAuthNChallenge.UserVerification == domain.UserVerificationRequirementRequired {
s.eventCommands = append(s.eventCommands,
user.NewHumanPasswordlessSignCountChangedEvent(ctx, s.sessionWriteModel.aggregate, tokenID, signCount),
)
} else {
s.eventCommands = append(s.eventCommands,
user.NewHumanU2FSignCountChangedEvent(ctx, s.sessionWriteModel.aggregate, tokenID, signCount),
)
}
}
func (s *SessionCommands) SetToken(ctx context.Context, tokenID string) {
@ -226,7 +231,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, sessionDomain string, metadata map[string][]byte) (set *SessionChanged, err error) {
func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, metadata map[string][]byte) (set *SessionChanged, err error) {
sessionID, err := c.idGenerator.Next()
if err != nil {
return nil, err
@ -237,7 +242,7 @@ func (c *Commands) CreateSession(ctx context.Context, cmds []SessionCommand, ses
return nil, err
}
cmd := c.NewSessionCommands(cmds, sessionWriteModel)
cmd.Start(ctx, sessionDomain)
cmd.Start(ctx)
return c.updateSession(ctx, cmd, metadata)
}

View File

@ -4,22 +4,18 @@ import (
"time"
"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/repository/session"
)
type PasskeyChallengeModel struct {
type WebAuthNChallengeModel struct {
Challenge string
AllowedCrentialIDs [][]byte
UserVerification domain.UserVerificationRequirement
RPID string
}
func (p *PasskeyChallengeModel) WebAuthNLogin(human *domain.Human, credentialAssertionData []byte) (*domain.WebAuthNLogin, error) {
if p == nil {
return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Ioqu5", "Errors.Session.Passkey.NoChallenge")
}
func (p *WebAuthNChallengeModel) WebAuthNLogin(human *domain.Human, credentialAssertionData []byte) *domain.WebAuthNLogin {
return &domain.WebAuthNLogin{
ObjectRoot: human.ObjectRoot,
CredentialAssertionData: credentialAssertionData,
@ -27,23 +23,23 @@ func (p *PasskeyChallengeModel) WebAuthNLogin(human *domain.Human, credentialAss
AllowedCredentialIDs: p.AllowedCrentialIDs,
UserVerification: p.UserVerification,
RPID: p.RPID,
}, nil
}
}
type SessionWriteModel struct {
eventstore.WriteModel
TokenID string
UserID string
UserCheckedAt time.Time
PasswordCheckedAt time.Time
IntentCheckedAt time.Time
PasskeyCheckedAt time.Time
Metadata map[string][]byte
Domain string
State domain.SessionState
TokenID string
UserID string
UserCheckedAt time.Time
PasswordCheckedAt time.Time
IntentCheckedAt time.Time
WebAuthNCheckedAt time.Time
WebAuthNUserVerified bool
Metadata map[string][]byte
State domain.SessionState
PasskeyChallenge *PasskeyChallengeModel
WebAuthNChallenge *WebAuthNChallengeModel
aggregate *eventstore.Aggregate
}
@ -70,10 +66,10 @@ func (wm *SessionWriteModel) Reduce() error {
wm.reducePasswordChecked(e)
case *session.IntentCheckedEvent:
wm.reduceIntentChecked(e)
case *session.PasskeyChallengedEvent:
wm.reducePasskeyChallenged(e)
case *session.PasskeyCheckedEvent:
wm.reducePasskeyChecked(e)
case *session.WebAuthNChallengedEvent:
wm.reduceWebAuthNChallenged(e)
case *session.WebAuthNCheckedEvent:
wm.reduceWebAuthNChecked(e)
case *session.TokenSetEvent:
wm.reduceTokenSet(e)
case *session.TerminateEvent:
@ -93,8 +89,8 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
session.UserCheckedType,
session.PasswordCheckedType,
session.IntentCheckedType,
session.PasskeyChallengedType,
session.PasskeyCheckedType,
session.WebAuthNChallengedType,
session.WebAuthNCheckedType,
session.TokenSetType,
session.MetadataSetType,
session.TerminateType,
@ -108,7 +104,6 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
}
func (wm *SessionWriteModel) reduceAdded(e *session.AddedEvent) {
wm.Domain = e.Domain
wm.State = domain.SessionStateActive
}
@ -125,18 +120,19 @@ func (wm *SessionWriteModel) reduceIntentChecked(e *session.IntentCheckedEvent)
wm.IntentCheckedAt = e.CheckedAt
}
func (wm *SessionWriteModel) reducePasskeyChallenged(e *session.PasskeyChallengedEvent) {
wm.PasskeyChallenge = &PasskeyChallengeModel{
func (wm *SessionWriteModel) reduceWebAuthNChallenged(e *session.WebAuthNChallengedEvent) {
wm.WebAuthNChallenge = &WebAuthNChallengeModel{
Challenge: e.Challenge,
AllowedCrentialIDs: e.AllowedCrentialIDs,
UserVerification: e.UserVerification,
RPID: wm.Domain,
RPID: e.RPID,
}
}
func (wm *SessionWriteModel) reducePasskeyChecked(e *session.PasskeyCheckedEvent) {
wm.PasskeyChallenge = nil
wm.PasskeyCheckedAt = e.CheckedAt
func (wm *SessionWriteModel) reduceWebAuthNChecked(e *session.WebAuthNCheckedEvent) {
wm.WebAuthNChallenge = nil
wm.WebAuthNCheckedAt = e.CheckedAt
wm.WebAuthNUserVerified = e.UserVerified
}
func (wm *SessionWriteModel) reduceTokenSet(e *session.TokenSetEvent) {
@ -152,9 +148,9 @@ func (wm *SessionWriteModel) AuthenticationTime() time.Time {
var authTime time.Time
for _, check := range []time.Time{
wm.PasswordCheckedAt,
wm.PasskeyCheckedAt,
wm.WebAuthNCheckedAt,
wm.IntentCheckedAt,
// TODO: add U2F and OTP check https://github.com/zitadel/zitadel/issues/5477
// 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
} {
if check.After(authTime) {
@ -170,8 +166,12 @@ func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType {
if !wm.PasswordCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePassword)
}
if !wm.PasskeyCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypePasswordless)
if !wm.WebAuthNCheckedAt.IsZero() {
if wm.WebAuthNUserVerified {
types = append(types, domain.UserAuthMethodTypePasswordless)
} else {
types = append(types, domain.UserAuthMethodTypeU2F)
}
}
if !wm.IntentCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeIDP)
@ -181,9 +181,6 @@ func (wm *SessionWriteModel) AuthMethodTypes() []domain.UserAuthMethodType {
if !wm.TOTPCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeTOTP)
}
if !wm.U2FCheckedAt.IsZero() {
types = append(types, domain.UserAuthMethodTypeU2F)
}
*/
// TODO: add checks with https://github.com/zitadel/zitadel/issues/6224
/*

View File

@ -0,0 +1,75 @@
package command
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
)
func TestSessionWriteModel_AuthMethodTypes(t *testing.T) {
type fields struct {
PasswordCheckedAt time.Time
IntentCheckedAt time.Time
WebAuthNCheckedAt time.Time
WebAuthNUserVerified bool
}
tests := []struct {
name string
fields fields
want []domain.UserAuthMethodType
}{
{
name: "password",
fields: fields{
PasswordCheckedAt: testNow,
},
want: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePassword,
},
},
{
name: "passwordless",
fields: fields{
WebAuthNCheckedAt: testNow,
WebAuthNUserVerified: true,
},
want: []domain.UserAuthMethodType{
domain.UserAuthMethodTypePasswordless,
},
},
{
name: "u2f",
fields: fields{
WebAuthNCheckedAt: testNow,
WebAuthNUserVerified: false,
},
want: []domain.UserAuthMethodType{
domain.UserAuthMethodTypeU2F,
},
},
{
name: "intent",
fields: fields{
IntentCheckedAt: testNow,
},
want: []domain.UserAuthMethodType{
domain.UserAuthMethodTypeIDP,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wm := &SessionWriteModel{
PasswordCheckedAt: tt.fields.PasswordCheckedAt,
IntentCheckedAt: tt.fields.IntentCheckedAt,
WebAuthNCheckedAt: tt.fields.WebAuthNCheckedAt,
WebAuthNUserVerified: tt.fields.WebAuthNUserVerified,
}
got := wm.AuthMethodTypes()
assert.Equal(t, got, tt.want)
})
}
}

View File

@ -1,84 +0,0 @@
package command
import (
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
)
type humanPasskeys struct {
human *domain.Human
tokens []*domain.WebAuthNToken
}
func (s *SessionCommands) getHumanPasskeys(ctx context.Context) (*humanPasskeys, error) {
humanWritemodel, err := s.gethumanWriteModel(ctx)
if err != nil {
return nil, err
}
tokenReadModel, err := s.getHumanPasswordlessTokenReadModel(ctx)
if err != nil {
return nil, err
}
return &humanPasskeys{
human: writeModelToHuman(humanWritemodel),
tokens: readModelToPasswordlessTokens(tokenReadModel),
}, nil
}
func (s *SessionCommands) getHumanPasswordlessTokenReadModel(ctx context.Context) (*HumanPasswordlessTokensReadModel, error) {
tokenReadModel := NewHumanPasswordlessTokensReadModel(s.sessionWriteModel.UserID, s.sessionWriteModel.ResourceOwner)
err := s.eventstore.FilterToQueryReducer(ctx, tokenReadModel)
if err != nil {
return nil, err
}
return tokenReadModel, nil
}
func (c *Commands) CreatePasskeyChallenge(userVerification domain.UserVerificationRequirement, dst json.Unmarshaler) SessionCommand {
return func(ctx context.Context, cmd *SessionCommands) error {
humanPasskeys, err := cmd.getHumanPasskeys(ctx)
if err != nil {
return err
}
webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, humanPasskeys.human, userVerification, cmd.sessionWriteModel.Domain, humanPasskeys.tokens...)
if err != nil {
return err
}
if err = json.Unmarshal(webAuthNLogin.CredentialAssertionData, dst); err != nil {
return caos_errs.ThrowInternal(err, "COMMAND-Yah6A", "Errors.Internal")
}
cmd.PasskeyChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification)
return nil
}
}
func (c *Commands) CheckPasskey(credentialAssertionData json.Marshaler) SessionCommand {
return func(ctx context.Context, cmd *SessionCommands) error {
credentialAssertionData, err := json.Marshal(credentialAssertionData)
if err != nil {
return caos_errs.ThrowInvalidArgument(err, "COMMAND-ohG2o", "todo")
}
humanPasskeys, err := cmd.getHumanPasskeys(ctx)
if err != nil {
return err
}
webAuthN, err := cmd.sessionWriteModel.PasskeyChallenge.WebAuthNLogin(humanPasskeys.human, credentialAssertionData)
if err != nil {
return err
}
keyID, signCount, err := c.webauthnConfig.FinishLogin(ctx, humanPasskeys.human, webAuthN, credentialAssertionData, humanPasskeys.tokens...)
if err != nil && keyID == nil {
return err
}
_, token := domain.GetTokenByKeyID(humanPasskeys.tokens, keyID)
if token == nil {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Aej7i", "Errors.User.WebAuthN.NotFound")
}
cmd.PasskeyChecked(ctx, cmd.now(), token.WebAuthNTokenID, signCount)
return nil
}
}

View File

@ -1,131 +0,0 @@
package command
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"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/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
)
func TestSessionCommands_getHumanPasskeys(t *testing.T) {
userAggr := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
eventstore *eventstore.Eventstore
sessionWriteModel *SessionWriteModel
}
type res struct {
want *humanPasskeys
err error
}
tests := []struct {
name string
fields fields
res res
}{
{
name: "missing UID",
fields: fields{
eventstore: &eventstore.Eventstore{},
sessionWriteModel: &SessionWriteModel{},
},
res: res{
want: nil,
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing"),
},
},
{
name: "passwordless filter error",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
userAggr,
"", "", "", "", "", language.Georgian,
domain.GenderDiverse, "", true,
),
),
),
expectFilterError(io.ErrClosedPipe),
),
sessionWriteModel: &SessionWriteModel{
UserID: "user1",
},
},
res: res{
want: nil,
err: io.ErrClosedPipe,
},
},
{
name: "ok",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
userAggr,
"", "", "", "", "", language.Georgian,
domain.GenderDiverse, "", true,
),
),
),
expectFilter(eventFromEventPusher(
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
context.Background(), &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType,
), "111", "challenge", "rpID"),
)),
),
sessionWriteModel: &SessionWriteModel{
UserID: "user1",
},
},
res: res{
want: &humanPasskeys{
human: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
State: domain.UserStateActive,
Profile: &domain.Profile{
PreferredLanguage: language.Georgian,
Gender: domain.GenderDiverse,
},
Email: &domain.Email{},
},
tokens: []*domain.WebAuthNToken{{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
WebAuthNTokenID: "111",
State: domain.MFAStateNotReady,
Challenge: "challenge",
RPID: "rpID",
}},
},
err: nil,
},
},
}
for _, tt := range tests {
s := &SessionCommands{
eventstore: tt.fields.eventstore,
sessionWriteModel: tt.fields.sessionWriteModel,
}
got, err := s.getHumanPasskeys(context.Background())
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
}
}

View File

@ -146,7 +146,6 @@ func TestCommands_CreateSession(t *testing.T) {
type args struct {
ctx context.Context
checks []SessionCommand
domain string
metadata map[string][]byte
}
type res struct {
@ -205,40 +204,7 @@ func TestCommands_CreateSession(t *testing.T) {
expectFilter(),
expectPush(
eventPusherToEvents(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, ""),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID",
),
),
),
},
res{
want: &SessionChanged{
ObjectDetails: &domain.ObjectDetails{ResourceOwner: "org1"},
ID: "sessionID",
NewToken: "token",
},
},
},
{
"empty session with domain",
fields{
idGenerator: mock.NewIDGeneratorExpectIDs(t, "sessionID"),
tokenCreator: func(sessionID string) (string, string, error) {
return "tokenID",
"token",
nil
},
},
args{
ctx: authz.NewMockContext("", "org1", ""),
domain: "domain.tld",
},
[]expect{
expectFilter(),
expectPush(
eventPusherToEvents(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld"),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate),
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID",
),
@ -262,7 +228,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.domain, tt.args.metadata)
got, err := c.CreateSession(tt.args.ctx, tt.args.checks, tt.args.metadata)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
})
@ -311,7 +277,7 @@ func TestCommands_UpdateSession(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
eventFromEventPusher(
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID")),
@ -336,7 +302,7 @@ func TestCommands_UpdateSession(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
eventFromEventPusher(
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID")),
@ -769,7 +735,7 @@ func TestCommands_TerminateSession(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
eventFromEventPusher(
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID")),
@ -794,7 +760,7 @@ func TestCommands_TerminateSession(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
eventFromEventPusher(
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID")),
@ -823,7 +789,7 @@ func TestCommands_TerminateSession(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
eventFromEventPusher(
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID"),
@ -854,7 +820,7 @@ func TestCommands_TerminateSession(t *testing.T) {
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate, "domain.tld")),
session.NewAddedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate)),
eventFromEventPusher(
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
"tokenID"),

View File

@ -0,0 +1,89 @@
package command
import (
"context"
"encoding/json"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
)
type humanWebAuthNTokens struct {
human *domain.Human
tokens []*domain.WebAuthNToken
}
func (s *SessionCommands) getHumanWebAuthNTokens(ctx context.Context, userVerification domain.UserVerificationRequirement) (*humanWebAuthNTokens, error) {
humanWritemodel, err := s.gethumanWriteModel(ctx)
if err != nil {
return nil, err
}
tokenReadModel, err := s.getHumanWebAuthNTokenReadModel(ctx, userVerification)
if err != nil {
return nil, err
}
return &humanWebAuthNTokens{
human: writeModelToHuman(humanWritemodel),
tokens: readModelToWebAuthNTokens(tokenReadModel),
}, nil
}
func (s *SessionCommands) getHumanWebAuthNTokenReadModel(ctx context.Context, userVerification domain.UserVerificationRequirement) (readModel HumanWebAuthNTokensReadModel, err error) {
readModel = NewHumanU2FTokensReadModel(s.sessionWriteModel.UserID, s.sessionWriteModel.ResourceOwner)
if userVerification == domain.UserVerificationRequirementRequired {
readModel = NewHumanPasswordlessTokensReadModel(s.sessionWriteModel.UserID, s.sessionWriteModel.ResourceOwner)
}
err = s.eventstore.FilterToQueryReducer(ctx, readModel)
if err != nil {
return nil, err
}
return readModel, nil
}
func (c *Commands) CreateWebAuthNChallenge(userVerification domain.UserVerificationRequirement, rpid string, dst json.Unmarshaler) SessionCommand {
return func(ctx context.Context, cmd *SessionCommands) error {
humanPasskeys, err := cmd.getHumanWebAuthNTokens(ctx, userVerification)
if err != nil {
return err
}
webAuthNLogin, err := c.webauthnConfig.BeginLogin(ctx, humanPasskeys.human, userVerification, rpid, humanPasskeys.tokens...)
if err != nil {
return err
}
if err = json.Unmarshal(webAuthNLogin.CredentialAssertionData, dst); err != nil {
return caos_errs.ThrowInternal(err, "COMMAND-Yah6A", "Errors.Internal")
}
cmd.WebAuthNChallenged(ctx, webAuthNLogin.Challenge, webAuthNLogin.AllowedCredentialIDs, webAuthNLogin.UserVerification, rpid)
return nil
}
}
func (c *Commands) CheckWebAuthN(credentialAssertionData json.Marshaler) SessionCommand {
return func(ctx context.Context, cmd *SessionCommands) error {
credentialAssertionData, err := json.Marshal(credentialAssertionData)
if err != nil {
return caos_errs.ThrowInternal(err, "COMMAND-ohG2o", "Errors.Internal")
}
challenge := cmd.sessionWriteModel.WebAuthNChallenge
if challenge == nil {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Ioqu5", "Errors.Session.WebAuthN.NoChallenge")
}
webAuthNTokens, err := cmd.getHumanWebAuthNTokens(ctx, challenge.UserVerification)
if err != nil {
return err
}
webAuthN := challenge.WebAuthNLogin(webAuthNTokens.human, credentialAssertionData)
credential, err := c.webauthnConfig.FinishLogin(ctx, webAuthNTokens.human, webAuthN, credentialAssertionData, webAuthNTokens.tokens...)
if err != nil && (credential == nil || credential.ID == nil) {
return err
}
_, token := domain.GetTokenByKeyID(webAuthNTokens.tokens, credential.ID)
if token == nil {
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Aej7i", "Errors.User.WebAuthN.NotFound")
}
cmd.WebAuthNChecked(ctx, cmd.now(), token.WebAuthNTokenID, credential.Authenticator.SignCount, credential.Flags.UserVerified)
return nil
}
}

View File

@ -0,0 +1,250 @@
package command
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
"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/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/repository/org"
"github.com/zitadel/zitadel/internal/repository/user"
)
func TestSessionCommands_getHumanWebAuthNTokens(t *testing.T) {
userAggr := &user.NewAggregate("user1", "org1").Aggregate
type fields struct {
eventstore *eventstore.Eventstore
sessionWriteModel *SessionWriteModel
}
type args struct {
userVerification domain.UserVerificationRequirement
}
type res struct {
want *humanWebAuthNTokens
err error
}
tests := []struct {
name string
fields fields
args args
res res
}{
{
name: "missing UID",
fields: fields{
eventstore: &eventstore.Eventstore{},
sessionWriteModel: &SessionWriteModel{},
},
args: args{
domain.UserVerificationRequirementDiscouraged,
},
res: res{
want: nil,
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-eeR2e", "Errors.User.UserIDMissing"),
},
},
{
name: "passwordless filter error",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
userAggr,
"", "", "", "", "", language.Georgian,
domain.GenderDiverse, "", true,
),
),
),
expectFilterError(io.ErrClosedPipe),
),
sessionWriteModel: &SessionWriteModel{
UserID: "user1",
},
},
args: args{
domain.UserVerificationRequirementDiscouraged,
},
res: res{
want: nil,
err: io.ErrClosedPipe,
},
},
{
name: "ok, discouraged, u2f",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
userAggr,
"", "", "", "", "", language.Georgian,
domain.GenderDiverse, "", true,
),
),
),
expectFilter(eventFromEventPusher(
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
context.Background(), &org.NewAggregate("org1").Aggregate, user.HumanU2FTokenAddedType,
), "111", "challenge", "rpID"),
)),
),
sessionWriteModel: &SessionWriteModel{
UserID: "user1",
},
},
args: args{
domain.UserVerificationRequirementDiscouraged,
},
res: res{
want: &humanWebAuthNTokens{
human: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
State: domain.UserStateActive,
Profile: &domain.Profile{
PreferredLanguage: language.Georgian,
Gender: domain.GenderDiverse,
},
Email: &domain.Email{},
},
tokens: []*domain.WebAuthNToken{{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
WebAuthNTokenID: "111",
State: domain.MFAStateNotReady,
Challenge: "challenge",
RPID: "rpID",
}},
},
err: nil,
},
},
{
name: "ok, preferred, u2f",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
userAggr,
"", "", "", "", "", language.Georgian,
domain.GenderDiverse, "", true,
),
),
),
expectFilter(eventFromEventPusher(
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
context.Background(), &org.NewAggregate("org1").Aggregate, user.HumanU2FTokenAddedType,
), "111", "challenge", "rpID"),
)),
),
sessionWriteModel: &SessionWriteModel{
UserID: "user1",
},
},
args: args{
domain.UserVerificationRequirementPreferred,
},
res: res{
want: &humanWebAuthNTokens{
human: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
State: domain.UserStateActive,
Profile: &domain.Profile{
PreferredLanguage: language.Georgian,
Gender: domain.GenderDiverse,
},
Email: &domain.Email{},
},
tokens: []*domain.WebAuthNToken{{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
WebAuthNTokenID: "111",
State: domain.MFAStateNotReady,
Challenge: "challenge",
RPID: "rpID",
}},
},
err: nil,
},
},
{
name: "ok, required, u2f",
fields: fields{
eventstore: eventstoreExpect(t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
userAggr,
"", "", "", "", "", language.Georgian,
domain.GenderDiverse, "", true,
),
),
),
expectFilter(eventFromEventPusher(
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
context.Background(), &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType,
), "111", "challenge", "rpID"),
)),
),
sessionWriteModel: &SessionWriteModel{
UserID: "user1",
},
},
args: args{
domain.UserVerificationRequirementRequired,
},
res: res{
want: &humanWebAuthNTokens{
human: &domain.Human{
ObjectRoot: models.ObjectRoot{
AggregateID: "user1",
ResourceOwner: "org1",
},
State: domain.UserStateActive,
Profile: &domain.Profile{
PreferredLanguage: language.Georgian,
Gender: domain.GenderDiverse,
},
Email: &domain.Email{},
},
tokens: []*domain.WebAuthNToken{{
ObjectRoot: models.ObjectRoot{
AggregateID: "org1",
},
WebAuthNTokenID: "111",
State: domain.MFAStateNotReady,
Challenge: "challenge",
RPID: "rpID",
}},
},
err: nil,
},
},
}
for _, tt := range tests {
s := &SessionCommands{
eventstore: tt.fields.eventstore,
sessionWriteModel: tt.fields.sessionWriteModel,
}
got, err := s.getHumanWebAuthNTokens(context.Background(), tt.args.userVerification)
require.ErrorIs(t, err, tt.res.err)
assert.Equal(t, tt.res.want, got)
}
}

View File

@ -112,17 +112,9 @@ func personalTokenWriteModelToToken(wm *PersonalAccessTokenWriteModel, algorithm
}, base64.RawURLEncoding.EncodeToString(encrypted), nil
}
func readModelToU2FTokens(wm *HumanU2FTokensReadModel) []*domain.WebAuthNToken {
tokens := make([]*domain.WebAuthNToken, len(wm.WebAuthNTokens))
for i, token := range wm.WebAuthNTokens {
tokens[i] = writeModelToWebAuthN(token)
}
return tokens
}
func readModelToPasswordlessTokens(wm *HumanPasswordlessTokensReadModel) []*domain.WebAuthNToken {
tokens := make([]*domain.WebAuthNToken, len(wm.WebAuthNTokens))
for i, token := range wm.WebAuthNTokens {
func readModelToWebAuthNTokens(readModel HumanWebAuthNTokensReadModel) []*domain.WebAuthNToken {
tokens := make([]*domain.WebAuthNToken, len(readModel.GetWebAuthNTokens()))
for i, token := range readModel.GetWebAuthNTokens() {
tokens[i] = writeModelToWebAuthN(token)
}
return tokens

View File

@ -24,7 +24,7 @@ func (c *Commands) getHumanU2FTokens(ctx context.Context, userID, resourceowner
if tokenReadModel.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-4M0ds", "Errors.User.NotFound")
}
return readModelToU2FTokens(tokenReadModel), nil
return readModelToWebAuthNTokens(tokenReadModel), nil
}
func (c *Commands) getHumanPasswordlessTokens(ctx context.Context, userID, resourceOwner string) ([]*domain.WebAuthNToken, error) {
@ -36,7 +36,7 @@ func (c *Commands) getHumanPasswordlessTokens(ctx context.Context, userID, resou
if tokenReadModel.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Mv9sd", "Errors.User.NotFound")
}
return readModelToPasswordlessTokens(tokenReadModel), nil
return readModelToWebAuthNTokens(tokenReadModel), nil
}
func (c *Commands) getHumanU2FLogin(ctx context.Context, userID, authReqID, resourceowner string) (*domain.WebAuthNLogin, error) {
@ -454,12 +454,12 @@ func (c *Commands) finishWebAuthNLogin(ctx context.Context, userID, resourceOwne
if err != nil {
return nil, nil, 0, err
}
keyID, signCount, err := c.webauthnConfig.FinishLogin(ctx, human, webAuthN, credentialData, tokens...)
if err != nil && keyID == nil {
credential, err := c.webauthnConfig.FinishLogin(ctx, human, webAuthN, credentialData, tokens...)
if err != nil && (credential == nil || credential.ID == nil) {
return nil, nil, 0, err
}
_, token := domain.GetTokenByKeyID(tokens, keyID)
_, token := domain.GetTokenByKeyID(tokens, credential.ID)
if token == nil {
return nil, nil, 0, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3b7zs", "Errors.User.WebAuthN.NotFound")
}
@ -470,7 +470,7 @@ func (c *Commands) finishWebAuthNLogin(ctx context.Context, userID, resourceOwne
}
userAgg := UserAggregateFromWriteModel(&writeModel.WriteModel)
return userAgg, token, signCount, nil
return userAgg, token, credential.Authenticator.SignCount, nil
}
func (c *Commands) HumanRemoveU2F(ctx context.Context, userID, webAuthNID, resourceOwner string) (*domain.ObjectDetails, error) {

View File

@ -146,6 +146,12 @@ func (wm *HumanWebAuthNWriteModel) Query() *eventstore.SearchQueryBuilder {
Builder()
}
type HumanWebAuthNTokensReadModel interface {
eventstore.QueryReducer
GetWebAuthNTokens() []*HumanWebAuthNWriteModel
WebAuthNTokenByID(id string) (int, *HumanWebAuthNWriteModel)
}
type HumanU2FTokensReadModel struct {
eventstore.WriteModel
@ -220,6 +226,10 @@ func (rm *HumanU2FTokensReadModel) Query() *eventstore.SearchQueryBuilder {
}
func (wm *HumanU2FTokensReadModel) GetWebAuthNTokens() []*HumanWebAuthNWriteModel {
return wm.WebAuthNTokens
}
func (wm *HumanU2FTokensReadModel) WebAuthNTokenByID(id string) (idx int, token *HumanWebAuthNWriteModel) {
for idx, token = range wm.WebAuthNTokens {
if token.WebauthNTokenID == id {
@ -303,6 +313,10 @@ func (rm *HumanPasswordlessTokensReadModel) Query() *eventstore.SearchQueryBuild
}
func (wm *HumanPasswordlessTokensReadModel) GetWebAuthNTokens() []*HumanWebAuthNWriteModel {
return wm.WebAuthNTokens
}
func (wm *HumanPasswordlessTokensReadModel) WebAuthNTokenByID(id string) (idx int, token *HumanWebAuthNWriteModel) {
for idx, token = range wm.WebAuthNTokens {
if token.WebauthNTokenID == id {

View File

@ -146,6 +146,24 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
logging.OnError(err).Fatal("create user passkey")
}
func (s *Tester) RegisterUserU2F(ctx context.Context, userID string) {
pkr, err := s.Client.UserV2.RegisterU2F(ctx, &user.RegisterU2FRequest{
UserId: userID,
Domain: s.Config.ExternalDomain,
})
logging.OnError(err).Fatal("create user u2f")
attestationResponse, err := s.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
logging.OnError(err).Fatal("create user u2f")
_, err = s.Client.UserV2.VerifyU2FRegistration(ctx, &user.VerifyU2FRegistrationRequest{
UserId: userID,
U2FId: pkr.GetU2FId(),
PublicKeyCredential: attestationResponse,
TokenName: "nice name",
})
logging.OnError(err).Fatal("create user u2f")
}
func (s *Tester) SetUserPassword(ctx context.Context, userID, password string) {
_, err := s.Client.UserV2.SetPassword(ctx, &user.SetPasswordRequest{
UserId: userID,
@ -209,28 +227,30 @@ func (s *Tester) CreateSuccessfulIntent(t *testing.T, idpID, userID, idpUserID s
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
}
func (s *Tester) CreatePasskeySession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) {
func (s *Tester) CreateVerfiedWebAuthNSession(t *testing.T, ctx context.Context, userID string) (id, token string, start, change time.Time) {
createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
Checks: &session.Checks{
User: &session.CheckUser{
Search: &session.CheckUser_UserId{UserId: userID},
},
},
Challenges: []session.ChallengeKind{
session.ChallengeKind_CHALLENGE_KIND_PASSKEY,
Challenges: &session.RequestChallenges{
WebAuthN: &session.RequestChallenges_WebAuthN{
Domain: s.Config.ExternalDomain,
UserVerificationRequirement: session.UserVerificationRequirement_USER_VERIFICATION_REQUIREMENT_REQUIRED,
},
},
Domain: s.Config.ExternalDomain,
})
require.NoError(t, err)
assertion, err := s.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetPasskey().GetPublicKeyCredentialRequestOptions())
assertion, err := s.WebAuthN.CreateAssertionResponse(createResp.GetChallenges().GetWebAuthN().GetPublicKeyCredentialRequestOptions(), true)
require.NoError(t, err)
updateResp, err := s.Client.SessionV2.SetSession(ctx, &session.SetSessionRequest{
SessionId: createResp.GetSessionId(),
SessionToken: createResp.GetSessionToken(),
Checks: &session.Checks{
Passkey: &session.CheckPasskey{
WebAuthN: &session.CheckWebAuthN{
CredentialAssertionData: assertion,
},
},
@ -250,7 +270,6 @@ func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID
Password: password,
},
},
Domain: s.Config.ExternalDomain,
})
require.NoError(t, err)
return createResp.GetSessionId(), createResp.GetSessionToken(),

View File

@ -14,24 +14,24 @@ import (
)
const (
SessionsProjectionTable = "projections.sessions3"
SessionsProjectionTable = "projections.sessions4"
SessionColumnID = "id"
SessionColumnCreationDate = "creation_date"
SessionColumnChangeDate = "change_date"
SessionColumnSequence = "sequence"
SessionColumnState = "state"
SessionColumnResourceOwner = "resource_owner"
SessionColumnDomain = "domain"
SessionColumnInstanceID = "instance_id"
SessionColumnCreator = "creator"
SessionColumnUserID = "user_id"
SessionColumnUserCheckedAt = "user_checked_at"
SessionColumnPasswordCheckedAt = "password_checked_at"
SessionColumnIntentCheckedAt = "intent_checked_at"
SessionColumnPasskeyCheckedAt = "passkey_checked_at"
SessionColumnMetadata = "metadata"
SessionColumnTokenID = "token_id"
SessionColumnID = "id"
SessionColumnCreationDate = "creation_date"
SessionColumnChangeDate = "change_date"
SessionColumnSequence = "sequence"
SessionColumnState = "state"
SessionColumnResourceOwner = "resource_owner"
SessionColumnInstanceID = "instance_id"
SessionColumnCreator = "creator"
SessionColumnUserID = "user_id"
SessionColumnUserCheckedAt = "user_checked_at"
SessionColumnPasswordCheckedAt = "password_checked_at"
SessionColumnIntentCheckedAt = "intent_checked_at"
SessionColumnWebAuthNCheckedAt = "webauthn_checked_at"
SessionColumnWebAuthNUserVerified = "webauthn_user_verified"
SessionColumnMetadata = "metadata"
SessionColumnTokenID = "token_id"
)
type sessionProjection struct {
@ -50,14 +50,14 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi
crdb.NewColumn(SessionColumnSequence, crdb.ColumnTypeInt64),
crdb.NewColumn(SessionColumnState, crdb.ColumnTypeEnum),
crdb.NewColumn(SessionColumnResourceOwner, crdb.ColumnTypeText),
crdb.NewColumn(SessionColumnDomain, crdb.ColumnTypeText),
crdb.NewColumn(SessionColumnInstanceID, crdb.ColumnTypeText),
crdb.NewColumn(SessionColumnCreator, crdb.ColumnTypeText),
crdb.NewColumn(SessionColumnUserID, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(SessionColumnUserCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnPasswordCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnIntentCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnPasskeyCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnWebAuthNCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnWebAuthNUserVerified, crdb.ColumnTypeBool, crdb.Nullable()),
crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()),
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
},
@ -90,8 +90,8 @@ func (p *sessionProjection) reducers() []handler.AggregateReducer {
Reduce: p.reduceIntentChecked,
},
{
Event: session.PasskeyCheckedType,
Reduce: p.reducePasskeyChecked,
Event: session.WebAuthNCheckedType,
Reduce: p.reduceWebAuthNChecked,
},
{
Event: session.TokenSetType,
@ -142,7 +142,6 @@ func (p *sessionProjection) reduceSessionAdded(event eventstore.Event) (*handler
handler.NewCol(SessionColumnCreationDate, e.CreationDate()),
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
handler.NewCol(SessionColumnResourceOwner, e.Aggregate().ResourceOwner),
handler.NewCol(SessionColumnDomain, e.Domain),
handler.NewCol(SessionColumnState, domain.SessionStateActive),
handler.NewCol(SessionColumnSequence, e.Sequence()),
handler.NewCol(SessionColumnCreator, e.User),
@ -210,18 +209,18 @@ func (p *sessionProjection) reduceIntentChecked(event eventstore.Event) (*handle
), nil
}
func (p *sessionProjection) reducePasskeyChecked(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*session.PasskeyCheckedEvent)
func (p *sessionProjection) reduceWebAuthNChecked(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*session.WebAuthNCheckedEvent)
if !ok {
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-WieM4", "reduce.wrong.event.type %s", session.PasskeyCheckedType)
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-WieM4", "reduce.wrong.event.type %s", session.WebAuthNCheckedType)
}
return crdb.NewUpdateStatement(
e,
[]handler.Column{
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
handler.NewCol(SessionColumnSequence, e.Sequence()),
handler.NewCol(SessionColumnPasskeyCheckedAt, e.CheckedAt),
handler.NewCol(SessionColumnWebAuthNCheckedAt, e.CheckedAt),
handler.NewCol(SessionColumnWebAuthNUserVerified, e.UserVerified),
},
[]handler.Condition{
handler.NewCond(SessionColumnID, e.Aggregate().ID),

View File

@ -43,14 +43,13 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "INSERT INTO projections.sessions3 (id, instance_id, creation_date, change_date, resource_owner, domain, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedStmt: "INSERT INTO projections.sessions4 (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",
anyArg{},
anyArg{},
"ro-id",
"domain",
domain.SessionStateActive,
uint64(15),
"editor-user",
@ -80,7 +79,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions3 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
expectedStmt: "UPDATE projections.sessions4 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@ -113,7 +112,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions3 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.sessions4 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@ -126,6 +125,40 @@ func TestSessionProjection_reduces(t *testing.T) {
},
},
},
{
name: "instance reduceWebAuthNChecked",
args: args{
event: getEvent(testEvent(
session.WebAuthNCheckedType,
session.AggregateType,
[]byte(`{
"checkedAt": "2023-05-04T00:00:00Z",
"userVerified": true
}`),
), eventstore.GenericEventMapper[session.WebAuthNCheckedEvent]),
},
reduce: (&sessionProjection{}).reduceWebAuthNChecked,
want: wantReduce{
aggregateType: eventstore.AggregateType("session"),
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions4 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{},
time.Date(2023, time.May, 4, 0, 0, 0, 0, time.UTC),
true,
"agg-id",
"instance-id",
},
},
},
},
},
},
{
name: "instance reduceIntentChecked",
args: args{
@ -145,7 +178,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions3 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.sessions4 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@ -177,7 +210,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions3 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.sessions4 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@ -211,7 +244,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions3 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedStmt: "UPDATE projections.sessions4 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
expectedArgs: []interface{}{
anyArg{},
anyArg{},
@ -243,7 +276,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.sessions3 WHERE (id = $1) AND (instance_id = $2)",
expectedStmt: "DELETE FROM projections.sessions4 WHERE (id = $1) AND (instance_id = $2)",
expectedArgs: []interface{}{
"agg-id",
"instance-id",
@ -270,7 +303,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.sessions3 WHERE (instance_id = $1)",
expectedStmt: "DELETE FROM projections.sessions4 WHERE (instance_id = $1)",
expectedArgs: []interface{}{
"agg-id",
},
@ -301,7 +334,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "UPDATE projections.sessions3 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)",
expectedStmt: "UPDATE projections.sessions4 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)",
expectedArgs: []interface{}{
nil,
"agg-id",

View File

@ -29,12 +29,11 @@ type Session struct {
Sequence uint64
State domain.SessionState
ResourceOwner string
Domain string
Creator string
UserFactor SessionUserFactor
PasswordFactor SessionPasswordFactor
IntentFactor SessionIntentFactor
PasskeyFactor SessionPasskeyFactor
WebAuthNFactor SessionWebAuthNFactor
Metadata map[string][]byte
}
@ -54,8 +53,9 @@ type SessionIntentFactor struct {
IntentCheckedAt time.Time
}
type SessionPasskeyFactor struct {
PasskeyCheckedAt time.Time
type SessionWebAuthNFactor struct {
WebAuthNCheckedAt time.Time
UserVerified bool
}
type SessionsSearchQueries struct {
@ -100,10 +100,6 @@ var (
name: projection.SessionColumnResourceOwner,
table: sessionsTable,
}
SessionColumnDomain = Column{
name: projection.SessionColumnDomain,
table: sessionsTable,
}
SessionColumnInstanceID = Column{
name: projection.SessionColumnInstanceID,
table: sessionsTable,
@ -128,8 +124,12 @@ var (
name: projection.SessionColumnIntentCheckedAt,
table: sessionsTable,
}
SessionColumnPasskeyCheckedAt = Column{
name: projection.SessionColumnPasskeyCheckedAt,
SessionColumnWebAuthNCheckedAt = Column{
name: projection.SessionColumnWebAuthNCheckedAt,
table: sessionsTable,
}
SessionColumnWebAuthNUserVerified = Column{
name: projection.SessionColumnWebAuthNUserVerified,
table: sessionsTable,
}
SessionColumnMetadata = Column{
@ -221,7 +221,6 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
SessionColumnState.identifier(),
SessionColumnResourceOwner.identifier(),
SessionColumnCreator.identifier(),
SessionColumnDomain.identifier(),
SessionColumnUserID.identifier(),
SessionColumnUserCheckedAt.identifier(),
LoginNameNameCol.identifier(),
@ -229,7 +228,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
UserResourceOwnerCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
SessionColumnWebAuthNCheckedAt.identifier(),
SessionColumnWebAuthNUserVerified.identifier(),
SessionColumnMetadata.identifier(),
SessionColumnToken.identifier(),
).From(sessionsTable.identifier()).
@ -240,17 +240,17 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
session := new(Session)
var (
userID sql.NullString
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
metadata database.Map[[]byte]
token sql.NullString
sessionDomain sql.NullString
userID sql.NullString
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
webAuthNCheckedAt sql.NullTime
webAuthNUserPresent sql.NullBool
metadata database.Map[[]byte]
token sql.NullString
)
err := row.Scan(
@ -261,7 +261,6 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&session.State,
&session.ResourceOwner,
&session.Creator,
&sessionDomain,
&userID,
&userCheckedAt,
&loginName,
@ -269,7 +268,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&userResourceOwner,
&passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt,
&webAuthNCheckedAt,
&webAuthNUserPresent,
&metadata,
&token,
)
@ -281,7 +281,6 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
return nil, "", errors.ThrowInternal(err, "QUERY-SAder", "Errors.Internal")
}
session.Domain = sessionDomain.String
session.UserFactor.UserID = userID.String
session.UserFactor.UserCheckedAt = userCheckedAt.Time
session.UserFactor.LoginName = loginName.String
@ -289,7 +288,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
session.UserFactor.ResourceOwner = userResourceOwner.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
session.Metadata = metadata
return session, token.String, nil
@ -305,7 +305,6 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
SessionColumnState.identifier(),
SessionColumnResourceOwner.identifier(),
SessionColumnCreator.identifier(),
SessionColumnDomain.identifier(),
SessionColumnUserID.identifier(),
SessionColumnUserCheckedAt.identifier(),
LoginNameNameCol.identifier(),
@ -313,7 +312,8 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
UserResourceOwnerCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(),
SessionColumnWebAuthNCheckedAt.identifier(),
SessionColumnWebAuthNUserVerified.identifier(),
SessionColumnMetadata.identifier(),
countColumn.identifier(),
).From(sessionsTable.identifier()).
@ -327,16 +327,16 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
session := new(Session)
var (
userID sql.NullString
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime
metadata database.Map[[]byte]
sessionDomain sql.NullString
userID sql.NullString
userCheckedAt sql.NullTime
loginName sql.NullString
displayName sql.NullString
userResourceOwner sql.NullString
passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
webAuthNCheckedAt sql.NullTime
webAuthNUserPresent sql.NullBool
metadata database.Map[[]byte]
)
err := rows.Scan(
@ -347,7 +347,6 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
&session.State,
&session.ResourceOwner,
&session.Creator,
&sessionDomain,
&userID,
&userCheckedAt,
&loginName,
@ -355,7 +354,8 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
&userResourceOwner,
&passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt,
&webAuthNCheckedAt,
&webAuthNUserPresent,
&metadata,
&sessions.Count,
)
@ -363,7 +363,6 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-SAfeg", "Errors.Internal")
}
session.Domain = sessionDomain.String
session.UserFactor.UserID = userID.String
session.UserFactor.UserCheckedAt = userCheckedAt.Time
session.UserFactor.LoginName = loginName.String
@ -371,7 +370,8 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
session.UserFactor.ResourceOwner = userResourceOwner.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
session.WebAuthNFactor.WebAuthNCheckedAt = webAuthNCheckedAt.Time
session.WebAuthNFactor.UserVerified = webAuthNUserPresent.Bool
session.Metadata = metadata
sessions.Sessions = append(sessions.Sessions, session)

View File

@ -17,51 +17,51 @@ import (
)
var (
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions3.id,` +
` projections.sessions3.creation_date,` +
` projections.sessions3.change_date,` +
` projections.sessions3.sequence,` +
` projections.sessions3.state,` +
` projections.sessions3.resource_owner,` +
` projections.sessions3.creator,` +
` projections.sessions3.domain,` +
` projections.sessions3.user_id,` +
` projections.sessions3.user_checked_at,` +
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions4.id,` +
` projections.sessions4.creation_date,` +
` projections.sessions4.change_date,` +
` projections.sessions4.sequence,` +
` projections.sessions4.state,` +
` projections.sessions4.resource_owner,` +
` projections.sessions4.creator,` +
` projections.sessions4.user_id,` +
` projections.sessions4.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
` projections.users8.resource_owner,` +
` projections.sessions3.password_checked_at,` +
` projections.sessions3.intent_checked_at,` +
` projections.sessions3.passkey_checked_at,` +
` projections.sessions3.metadata,` +
` projections.sessions3.token_id` +
` FROM projections.sessions3` +
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
` projections.sessions4.password_checked_at,` +
` projections.sessions4.intent_checked_at,` +
` projections.sessions4.webauthn_checked_at,` +
` projections.sessions4.webauthn_user_verified,` +
` projections.sessions4.metadata,` +
` projections.sessions4.token_id` +
` FROM projections.sessions4` +
` LEFT JOIN projections.login_names2 ON projections.sessions4.user_id = projections.login_names2.user_id AND projections.sessions4.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions4.user_id = projections.users8_humans.user_id AND projections.sessions4.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions4.user_id = projections.users8.id AND projections.sessions4.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions3.id,` +
` projections.sessions3.creation_date,` +
` projections.sessions3.change_date,` +
` projections.sessions3.sequence,` +
` projections.sessions3.state,` +
` projections.sessions3.resource_owner,` +
` projections.sessions3.creator,` +
` projections.sessions3.domain,` +
` projections.sessions3.user_id,` +
` projections.sessions3.user_checked_at,` +
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions4.id,` +
` projections.sessions4.creation_date,` +
` projections.sessions4.change_date,` +
` projections.sessions4.sequence,` +
` projections.sessions4.state,` +
` projections.sessions4.resource_owner,` +
` projections.sessions4.creator,` +
` projections.sessions4.user_id,` +
` projections.sessions4.user_checked_at,` +
` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` +
` projections.users8.resource_owner,` +
` projections.sessions3.password_checked_at,` +
` projections.sessions3.intent_checked_at,` +
` projections.sessions3.passkey_checked_at,` +
` projections.sessions3.metadata,` +
` projections.sessions4.password_checked_at,` +
` projections.sessions4.intent_checked_at,` +
` projections.sessions4.webauthn_checked_at,` +
` projections.sessions4.webauthn_user_verified,` +
` projections.sessions4.metadata,` +
` COUNT(*) OVER ()` +
` FROM projections.sessions3` +
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
` FROM projections.sessions4` +
` LEFT JOIN projections.login_names2 ON projections.sessions4.user_id = projections.login_names2.user_id AND projections.sessions4.instance_id = projections.login_names2.instance_id` +
` LEFT JOIN projections.users8_humans ON projections.sessions4.user_id = projections.users8_humans.user_id AND projections.sessions4.instance_id = projections.users8_humans.instance_id` +
` LEFT JOIN projections.users8 ON projections.sessions4.user_id = projections.users8.id AND projections.sessions4.instance_id = projections.users8.instance_id` +
` AS OF SYSTEM TIME '-1 ms'`)
sessionCols = []string{
@ -72,7 +72,6 @@ var (
"state",
"resource_owner",
"creator",
"domain",
"user_id",
"user_checked_at",
"login_name",
@ -80,7 +79,8 @@ var (
"user_resource_owner",
"password_checked_at",
"intent_checked_at",
"passkey_checked_at",
"webauthn_checked_at",
"webauthn_user_verified",
"metadata",
"token",
}
@ -93,7 +93,6 @@ var (
"state",
"resource_owner",
"creator",
"domain",
"user_id",
"user_checked_at",
"login_name",
@ -101,7 +100,8 @@ var (
"user_resource_owner",
"password_checked_at",
"intent_checked_at",
"passkey_checked_at",
"webauthn_checked_at",
"webauthn_user_verified",
"metadata",
"count",
}
@ -146,7 +146,6 @@ func Test_SessionsPrepare(t *testing.T) {
domain.SessionStateActive,
"ro",
"creator",
"domain",
"user-id",
testNow,
"login-name",
@ -155,6 +154,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
testNow,
testNow,
true,
[]byte(`{"key": "dmFsdWU="}`),
},
},
@ -173,7 +173,6 @@ func Test_SessionsPrepare(t *testing.T) {
State: domain.SessionStateActive,
ResourceOwner: "ro",
Creator: "creator",
Domain: "domain",
UserFactor: SessionUserFactor{
UserID: "user-id",
UserCheckedAt: testNow,
@ -187,8 +186,9 @@ func Test_SessionsPrepare(t *testing.T) {
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
WebAuthNFactor: SessionWebAuthNFactor{
WebAuthNCheckedAt: testNow,
UserVerified: true,
},
Metadata: map[string][]byte{
"key": []byte("value"),
@ -213,7 +213,6 @@ func Test_SessionsPrepare(t *testing.T) {
domain.SessionStateActive,
"ro",
"creator",
"domain",
"user-id",
testNow,
"login-name",
@ -222,6 +221,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
testNow,
testNow,
true,
[]byte(`{"key": "dmFsdWU="}`),
},
{
@ -232,7 +232,6 @@ func Test_SessionsPrepare(t *testing.T) {
domain.SessionStateActive,
"ro",
"creator2",
"domain",
"user-id2",
testNow,
"login-name2",
@ -241,6 +240,7 @@ func Test_SessionsPrepare(t *testing.T) {
testNow,
testNow,
testNow,
false,
[]byte(`{"key": "dmFsdWU="}`),
},
},
@ -259,7 +259,6 @@ func Test_SessionsPrepare(t *testing.T) {
State: domain.SessionStateActive,
ResourceOwner: "ro",
Creator: "creator",
Domain: "domain",
UserFactor: SessionUserFactor{
UserID: "user-id",
UserCheckedAt: testNow,
@ -273,8 +272,9 @@ func Test_SessionsPrepare(t *testing.T) {
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
WebAuthNFactor: SessionWebAuthNFactor{
WebAuthNCheckedAt: testNow,
UserVerified: true,
},
Metadata: map[string][]byte{
"key": []byte("value"),
@ -288,7 +288,6 @@ func Test_SessionsPrepare(t *testing.T) {
State: domain.SessionStateActive,
ResourceOwner: "ro",
Creator: "creator2",
Domain: "domain",
UserFactor: SessionUserFactor{
UserID: "user-id2",
UserCheckedAt: testNow,
@ -302,8 +301,9 @@ func Test_SessionsPrepare(t *testing.T) {
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
WebAuthNFactor: SessionWebAuthNFactor{
WebAuthNCheckedAt: testNow,
UserVerified: false,
},
Metadata: map[string][]byte{
"key": []byte("value"),
@ -381,7 +381,6 @@ func Test_SessionPrepare(t *testing.T) {
domain.SessionStateActive,
"ro",
"creator",
"domain",
"user-id",
testNow,
"login-name",
@ -390,6 +389,7 @@ func Test_SessionPrepare(t *testing.T) {
testNow,
testNow,
testNow,
true,
[]byte(`{"key": "dmFsdWU="}`),
"tokenID",
},
@ -403,7 +403,6 @@ func Test_SessionPrepare(t *testing.T) {
State: domain.SessionStateActive,
ResourceOwner: "ro",
Creator: "creator",
Domain: "domain",
UserFactor: SessionUserFactor{
UserID: "user-id",
UserCheckedAt: testNow,
@ -417,8 +416,9 @@ func Test_SessionPrepare(t *testing.T) {
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow,
WebAuthNFactor: SessionWebAuthNFactor{
WebAuthNCheckedAt: testNow,
UserVerified: true,
},
Metadata: map[string][]byte{
"key": []byte("value"),

View File

@ -7,8 +7,8 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
RegisterFilterEventMapper(AggregateType, UserCheckedType, UserCheckedEventMapper).
RegisterFilterEventMapper(AggregateType, PasswordCheckedType, PasswordCheckedEventMapper).
RegisterFilterEventMapper(AggregateType, IntentCheckedType, IntentCheckedEventMapper).
RegisterFilterEventMapper(AggregateType, PasskeyChallengedType, eventstore.GenericEventMapper[PasskeyChallengedEvent]).
RegisterFilterEventMapper(AggregateType, PasskeyCheckedType, eventstore.GenericEventMapper[PasskeyCheckedEvent]).
RegisterFilterEventMapper(AggregateType, WebAuthNChallengedType, eventstore.GenericEventMapper[WebAuthNChallengedEvent]).
RegisterFilterEventMapper(AggregateType, WebAuthNCheckedType, eventstore.GenericEventMapper[WebAuthNCheckedEvent]).
RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper).
RegisterFilterEventMapper(AggregateType, MetadataSetType, MetadataSetEventMapper).
RegisterFilterEventMapper(AggregateType, TerminateType, TerminateEventMapper)

View File

@ -12,22 +12,20 @@ import (
)
const (
sessionEventPrefix = "session."
AddedType = sessionEventPrefix + "added"
UserCheckedType = sessionEventPrefix + "user.checked"
PasswordCheckedType = sessionEventPrefix + "password.checked"
IntentCheckedType = sessionEventPrefix + "intent.checked"
PasskeyChallengedType = sessionEventPrefix + "passkey.challenged"
PasskeyCheckedType = sessionEventPrefix + "passkey.checked"
TokenSetType = sessionEventPrefix + "token.set"
MetadataSetType = sessionEventPrefix + "metadata.set"
TerminateType = sessionEventPrefix + "terminated"
sessionEventPrefix = "session."
AddedType = sessionEventPrefix + "added"
UserCheckedType = sessionEventPrefix + "user.checked"
PasswordCheckedType = sessionEventPrefix + "password.checked"
IntentCheckedType = sessionEventPrefix + "intent.checked"
WebAuthNChallengedType = sessionEventPrefix + "webAuthN.challenged"
WebAuthNCheckedType = sessionEventPrefix + "webAuthN.checked"
TokenSetType = sessionEventPrefix + "token.set"
MetadataSetType = sessionEventPrefix + "metadata.set"
TerminateType = sessionEventPrefix + "terminated"
)
type AddedEvent struct {
eventstore.BaseEvent `json:"-"`
Domain string `json:"domain,omitempty"`
}
func (e *AddedEvent) Data() interface{} {
@ -40,7 +38,6 @@ func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
func NewAddedEvent(ctx context.Context,
aggregate *eventstore.Aggregate,
domain string,
) *AddedEvent {
return &AddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -48,7 +45,6 @@ func NewAddedEvent(ctx context.Context,
aggregate,
AddedType,
),
Domain: domain,
}
}
@ -190,75 +186,81 @@ func IntentCheckedEventMapper(event *repository.Event) (eventstore.Event, error)
return added, nil
}
type PasskeyChallengedEvent struct {
type WebAuthNChallengedEvent struct {
eventstore.BaseEvent `json:"-"`
Challenge string `json:"challenge,omitempty"`
AllowedCrentialIDs [][]byte `json:"allowedCrentialIDs,omitempty"`
UserVerification domain.UserVerificationRequirement `json:"userVerification,omitempty"`
RPID string `json:"rpid,omitempty"`
}
func (e *PasskeyChallengedEvent) Data() interface{} {
func (e *WebAuthNChallengedEvent) Data() interface{} {
return e
}
func (e *PasskeyChallengedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
func (e *WebAuthNChallengedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *PasskeyChallengedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
func (e *WebAuthNChallengedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = *base
}
func NewPasskeyChallengedEvent(
func NewWebAuthNChallengedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
challenge string,
allowedCrentialIDs [][]byte,
userVerification domain.UserVerificationRequirement,
) *PasskeyChallengedEvent {
return &PasskeyChallengedEvent{
rpid string,
) *WebAuthNChallengedEvent {
return &WebAuthNChallengedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
PasskeyChallengedType,
WebAuthNChallengedType,
),
Challenge: challenge,
AllowedCrentialIDs: allowedCrentialIDs,
UserVerification: userVerification,
RPID: rpid,
}
}
type PasskeyCheckedEvent struct {
type WebAuthNCheckedEvent struct {
eventstore.BaseEvent `json:"-"`
CheckedAt time.Time `json:"checkedAt"`
CheckedAt time.Time `json:"checkedAt"`
UserVerified bool `json:"userVerified,omitempty"`
}
func (e *PasskeyCheckedEvent) Data() interface{} {
func (e *WebAuthNCheckedEvent) Data() interface{} {
return e
}
func (e *PasskeyCheckedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
func (e *WebAuthNCheckedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
return nil
}
func (e *PasskeyCheckedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
func (e *WebAuthNCheckedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = *base
}
func NewPasskeyCheckedEvent(
func NewWebAuthNCheckedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
checkedAt time.Time,
) *PasswordCheckedEvent {
return &PasswordCheckedEvent{
userVerified bool,
) *WebAuthNCheckedEvent {
return &WebAuthNCheckedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
PasskeyCheckedType,
WebAuthNCheckedType,
),
CheckedAt: checkedAt,
CheckedAt: checkedAt,
UserVerified: userVerified,
}
}

View File

@ -496,8 +496,8 @@ Errors:
Terminated: Сесията вече е прекратена
Token:
Invalid: Токенът на сесията е невалиден
Passkey:
NoChallenge: Сесия без предизвикателство за парола
WebAuthN:
NoChallenge: Сесия без WebAuthN предизвикателство
Intent:
IDPMissing: IDP липсва в заявката
SuccessURLMissing: В заявката липсва URL адрес за успех

View File

@ -478,8 +478,8 @@ Errors:
Terminated: Session bereits beendet
Token:
Invalid: Session Token ist ungültig
Passkey:
NoChallenge: Sitzung ohne Passkey-Herausforderung
WebAuthN:
NoChallenge: Sitzung ohne WebAuthN-Challenge
Intent:
IDPMissing: IDP ID fehlt im Request
SuccessURLMissing: Success URL fehlt im Request

View File

@ -478,8 +478,8 @@ Errors:
Terminated: Session already terminated
Token:
Invalid: Session Token is invalid
Passkey:
NoChallenge: Session without passkey challenge
WebAuthN:
NoChallenge: Session without WebAuthN challenge
Intent:
IDPMissing: IDP ID is missing in the request
SuccessURLMissing: Success URL is missing in the request

View File

@ -478,8 +478,8 @@ Errors:
Terminated: Sesión ya terminada
Token:
Invalid: El identificador de sesión no es válido
Passkey:
NoChallenge: Sesión sin desafío de contraseña
WebAuthN:
NoChallenge: Sesión sin desafío WebAuthN
Intent:
IDPMissing: Falta IDP en la solicitud
SuccessURLMissing: Falta la URL de éxito en la solicitud

View File

@ -478,8 +478,8 @@ Errors:
Terminated: La session est déjà terminée
Token:
Invalid: Le jeton de session n'est pas valide
Passkey:
NoChallenge: Session sans défi de clé d'accès
WebAuthN:
NoChallenge: Session sans challenge WebAuthN
Intent:
IDPMissing: IDP manquant dans la requête
SuccessURLMissing: Success URL absent de la requête

View File

@ -478,8 +478,8 @@ Errors:
Terminated: Sessione già terminata
Token:
Invalid: Il token della sessione non è valido
Passkey:
NoChallenge: Sessione senza sfida passkey
WebAuthN:
NoChallenge: Sessione senza sfida WebAuthN
Intent:
IDPMissing: IDP mancante nella richiesta
SuccessURLMissing: URL di successo mancante nella richiesta

View File

@ -467,8 +467,8 @@ Errors:
Terminated: セッションはすでに終了しています
Token:
Invalid: セッショントークンが無効です
Passkey:
NoChallenge: パスキーチャレンジなしのセッション
WebAuthN:
NoChallenge: WebAuthN チャレンジを使用しないセッション
Intent:
IDPMissing: リクエストにIDP IDが含まれていません
SuccessURLMissing: リクエストに成功時の URL がありません

View File

@ -478,8 +478,8 @@ Errors:
Terminated: Сесијата е веќе завршена
Token:
Invalid: Токенот за сесија е невалиден
Passkey:
NoChallenge: Сесија без предизвик за passkey
WebAuthN:
NoChallenge: Сесија без предизвик WebAuthN
Intent:
IDPMissing: ID на IDP недостасува во барањето
SuccessURLMissing: URL за успех недостасува во барањето

View File

@ -478,8 +478,8 @@ Errors:
Terminated: Sesja już zakończona
Token:
Invalid: Token sesji jest nieprawidłowy
Passkey:
NoChallenge: Sesja bez wyzwania klucza
WebAuthN:
NoChallenge: Sesja bez wyzwania WebAuthN
Intent:
IDPMissing: Brak identyfikatora IDP w żądaniu
SuccessURLMissing: Brak adresu URL powodzenia w żądaniu

View File

@ -477,8 +477,8 @@ Errors:
Terminated: A sessão já foi encerrada
Token:
Invalid: O token da sessão é inválido
Passkey:
NoChallenge: Sessão sem desafio de senha
WebAuthN:
NoChallenge: Sessão sem desafio WebAuthN
Intent:
IDPMissing: O ID do IDP está faltando na solicitação
SuccessURLMissing: A URL de sucesso está faltando na solicitação

View File

@ -478,8 +478,8 @@ Errors:
Terminated: 会话已经终止
Token:
Invalid: 会话令牌是无效的
Passkey:
NoChallenge: 没有密码挑战的会话
WebAuthN:
NoChallenge: 没有 WebAuthN 质询的会话
Intent:
IDPMissing: 请求中缺少IDP ID
SuccessURLMissing: 请求中缺少成功URL

View File

@ -9,9 +9,10 @@ import (
)
type Client struct {
rp virtualwebauthn.RelyingParty
auth virtualwebauthn.Authenticator
credential virtualwebauthn.Credential
rp virtualwebauthn.RelyingParty
auth virtualwebauthn.Authenticator
authVerifyUser virtualwebauthn.Authenticator
credential virtualwebauthn.Credential
}
func NewClient(name, domain, origin string) *Client {
@ -21,9 +22,12 @@ func NewClient(name, domain, origin string) *Client {
Origin: origin,
}
return &Client{
rp: rp,
auth: virtualwebauthn.NewAuthenticator(),
credential: virtualwebauthn.NewCredential(virtualwebauthn.KeyTypeEC2),
rp: rp,
auth: virtualwebauthn.NewAuthenticatorWithOptions(virtualwebauthn.AuthenticatorOptions{
UserNotVerified: true,
}),
authVerifyUser: virtualwebauthn.NewAuthenticator(),
credential: virtualwebauthn.NewCredential(virtualwebauthn.KeyTypeEC2),
}
}
@ -46,7 +50,7 @@ func (c *Client) CreateAttestationResponse(optionsPb *structpb.Struct) (*structp
return resp, nil
}
func (c *Client) CreateAssertionResponse(optionsPb *structpb.Struct) (*structpb.Struct, error) {
func (c *Client) CreateAssertionResponse(optionsPb *structpb.Struct, verifyUser bool) (*structpb.Struct, error) {
options, err := protojson.Marshal(optionsPb)
if err != nil {
return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err)
@ -55,9 +59,13 @@ func (c *Client) CreateAssertionResponse(optionsPb *structpb.Struct) (*structpb.
if err != nil {
return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err)
}
authenticator := c.auth
if verifyUser {
authenticator = c.authVerifyUser
}
resp := new(structpb.Struct)
err = protojson.Unmarshal([]byte(virtualwebauthn.CreateAssertionResponse(
c.rp, c.auth, c.credential, *parsedAssertionOptions,
c.rp, authenticator, c.credential, *parsedAssertionOptions,
)), resp)
if err != nil {
return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err)

View File

@ -154,10 +154,10 @@ func (w *Config) BeginLogin(ctx context.Context, user *domain.Human, userVerific
}, nil
}
func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNLogin, credData []byte, webAuthNs ...*domain.WebAuthNToken) ([]byte, uint32, error) {
func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *domain.WebAuthNLogin, credData []byte, webAuthNs ...*domain.WebAuthNToken) (*webauthn.Credential, error) {
assertionData, err := protocol.ParseCredentialRequestResponseBody(bytes.NewReader(credData))
if err != nil {
return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-ADgv4", "Errors.User.WebAuthN.ValidateLoginFailed")
return nil, caos_errs.ThrowInternal(err, "WEBAU-ADgv4", "Errors.User.WebAuthN.ValidateLoginFailed")
}
webUser := &webUser{
Human: user,
@ -165,17 +165,17 @@ func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *
}
webAuthNServer, err := w.serverFromContext(ctx, webAuthN.RPID, assertionData.Response.CollectedClientData.Origin)
if err != nil {
return nil, 0, err
return nil, err
}
credential, err := webAuthNServer.ValidateLogin(webUser, WebAuthNLoginToSessionData(webAuthN), assertionData)
if err != nil {
return nil, 0, caos_errs.ThrowInternal(err, "WEBAU-3M9si", "Errors.User.WebAuthN.ValidateLoginFailed")
return nil, caos_errs.ThrowInternal(err, "WEBAU-3M9si", "Errors.User.WebAuthN.ValidateLoginFailed")
}
if credential.Authenticator.CloneWarning {
return credential.ID, credential.Authenticator.SignCount, caos_errs.ThrowInternal(err, "WEBAU-4M90s", "Errors.User.WebAuthN.CloneWarning")
return credential, caos_errs.ThrowInternal(err, "WEBAU-4M90s", "Errors.User.WebAuthN.CloneWarning")
}
return credential.ID, credential.Authenticator.SignCount, nil
return credential, nil
}
func (w *Config) serverFromContext(ctx context.Context, id, origin string) (*webauthn.WebAuthn, error) {

View File

@ -7,6 +7,7 @@ deps:
breaking:
use:
- FILE
ignore_unstable_packages: true
lint:
use:
- MINIMAL

View File

@ -2,18 +2,47 @@ syntax = "proto3";
package zitadel.session.v2alpha;
import "google/api/field_behavior.proto";
import "google/protobuf/struct.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha;session";
enum ChallengeKind {
CHALLENGE_KIND_UNSPECIFIED = 0;
CHALLENGE_KIND_PASSKEY = 1;
enum UserVerificationRequirement {
USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0;
USER_VERIFICATION_REQUIREMENT_REQUIRED = 1;
USER_VERIFICATION_REQUIREMENT_PREFERRED = 2;
USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3;
}
message RequestChallenges {
message WebAuthN {
string domain = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"Domain on which the session was created. Will be used in the WebAuthN challenge.\"";
}
];
UserVerificationRequirement user_verification_requirement = 2 [
(validate.rules).enum = {
defined_only: true,
not_in: [0]
},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"User verification that is required during validation. When set to `USER_VERIFICATION_REQUIREMENT_REQUIRED` the behaviour is for passkey authentication. Other values will mean U2F\"";
ref: "https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement";
}
];
}
optional WebAuthN web_auth_n = 1;
}
message Challenges {
message Passkey {
message WebAuthN {
google.protobuf.Struct public_key_credential_request_options = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Options for Assertion Generaration (dictionary PublicKeyCredentialRequestOptions). Generated helper methods transform the field to JSON, for use in a WebauthN client. See also: https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions"
@ -22,5 +51,5 @@ message Challenges {
];
}
optional Passkey passkey = 1;
optional WebAuthN web_auth_n = 1;
}

View File

@ -39,17 +39,12 @@ message Session {
description: "\"custom key value list\"";
}
];
string domain = 7 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"domain on which the session was created\"";
}
];
}
message Factors {
UserFactor user = 1;
PasswordFactor password = 2;
PasskeyFactor passkey = 3;
WebAuthNFactor web_auth_n = 3;
IntentFactor intent = 4;
}
@ -97,12 +92,13 @@ message IntentFactor {
];
}
message PasskeyFactor {
message WebAuthNFactor {
google.protobuf.Timestamp verified_at = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"time when the passkey challenge was last checked\"";
}
];
bool user_verified = 2;
}
message SearchQuery {

View File

@ -244,12 +244,7 @@ message CreateSessionRequest{
description: "\"custom key value list to be stored on the session\"";
}
];
repeated ChallengeKind challenges = 3;
string domain = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"Domain on which the session was created. Will be used for Passkey and U2F challenges.\"";
}
];
RequestChallenges challenges = 3;
}
message CreateSessionResponse{
@ -296,7 +291,7 @@ message SetSessionRequest{
description: "\"custom key value list to be stored on the session\"";
}
];
repeated ChallengeKind challenges = 5;
RequestChallenges challenges = 5;
}
message SetSessionResponse{
@ -306,7 +301,7 @@ message SetSessionResponse{
description: "\"token of the session, which is required for further updates of the session or the request other resources\"";
}
];
Challenges challenges = 3;
Challenges challenges = 3;
}
message DeleteSessionRequest{
@ -341,9 +336,9 @@ message Checks {
description: "\"Checks the password and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\"";
}
];
optional CheckPasskey passkey = 3 [
optional CheckWebAuthN web_auth_n = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "\"Checks the public key credential issued by the passkey client. Requires that the user is already checked and a passkey challenge to be requested, in any previous request.\"";
description: "\"Checks the public key credential issued by the WebAuthN client. Requires that the user is already checked and a WebAuthN challenge to be requested, in any previous request.\"";
}
];
optional CheckIntent intent = 4 [
@ -385,12 +380,12 @@ message CheckPassword {
];
}
message CheckPasskey {
message CheckWebAuthN {
google.protobuf.Struct credential_assertion_data = 1 [
(validate.rules).message.required = true,
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "JSON representation of public key credential issued by the passkey client";
description: "JSON representation of public key credential issued by the webAuthN client";
min_length: 55;
max_length: 1048576; //1 MB
}