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
47 changed files with 1035 additions and 665 deletions

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) {