mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
feat(api/v2): implement U2F session check (#6339)
This commit is contained in:
parent
4e0c3115fe
commit
86af67d1be
1
.gitignore
vendored
1
.gitignore
vendored
@ -64,7 +64,6 @@ docs/docs/apis/proto
|
||||
**/.sass-cache
|
||||
/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css
|
||||
/internal/api/ui/login/static/resources/themes/zitadel/css/zitadel.css.map
|
||||
zitadel
|
||||
zitadel-*-*
|
||||
|
||||
# local
|
||||
|
7
Makefile
7
Makefile
@ -90,12 +90,15 @@ clean:
|
||||
core_unit_test:
|
||||
go test -race -coverprofile=profile.cov ./...
|
||||
|
||||
.PHONY: core_integration_test
|
||||
core_integration_test:
|
||||
.PHONY: core_integration_setup
|
||||
core_integration_setup:
|
||||
go build -o zitadel main.go
|
||||
./zitadel init --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
||||
./zitadel setup --masterkeyFromEnv --config internal/integration/config/zitadel.yaml --config internal/integration/config/${INTEGRATION_DB_FLAVOR}.yaml
|
||||
$(RM) zitadel
|
||||
|
||||
.PHONY: core_integration_test
|
||||
core_integration_test: core_integration_setup
|
||||
go test -tags=integration -race -p 1 -v -coverprofile=profile.cov -coverpkg=./internal/...,./cmd/... ./internal/integration ./internal/api/grpc/... ./internal/notification/handlers/... ./internal/api/oidc/...
|
||||
|
||||
.PHONY: console_lint
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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{
|
||||
|
@ -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{
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
/*
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
/*
|
||||
|
75
internal/command/session_model_test.go
Normal file
75
internal/command/session_model_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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"),
|
||||
|
89
internal/command/session_webauhtn.go
Normal file
89
internal/command/session_webauhtn.go
Normal 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
|
||||
}
|
||||
}
|
250
internal/command/session_webauthn_test.go
Normal file
250
internal/command/session_webauthn_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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(),
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -496,8 +496,8 @@ Errors:
|
||||
Terminated: Сесията вече е прекратена
|
||||
Token:
|
||||
Invalid: Токенът на сесията е невалиден
|
||||
Passkey:
|
||||
NoChallenge: Сесия без предизвикателство за парола
|
||||
WebAuthN:
|
||||
NoChallenge: Сесия без WebAuthN предизвикателство
|
||||
Intent:
|
||||
IDPMissing: IDP липсва в заявката
|
||||
SuccessURLMissing: В заявката липсва URL адрес за успех
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -467,8 +467,8 @@ Errors:
|
||||
Terminated: セッションはすでに終了しています
|
||||
Token:
|
||||
Invalid: セッショントークンが無効です
|
||||
Passkey:
|
||||
NoChallenge: パスキーチャレンジなしのセッション
|
||||
WebAuthN:
|
||||
NoChallenge: WebAuthN チャレンジを使用しないセッション
|
||||
Intent:
|
||||
IDPMissing: リクエストにIDP IDが含まれていません
|
||||
SuccessURLMissing: リクエストに成功時の URL がありません
|
||||
|
@ -478,8 +478,8 @@ Errors:
|
||||
Terminated: Сесијата е веќе завршена
|
||||
Token:
|
||||
Invalid: Токенот за сесија е невалиден
|
||||
Passkey:
|
||||
NoChallenge: Сесија без предизвик за passkey
|
||||
WebAuthN:
|
||||
NoChallenge: Сесија без предизвик WebAuthN
|
||||
Intent:
|
||||
IDPMissing: ID на IDP недостасува во барањето
|
||||
SuccessURLMissing: URL за успех недостасува во барањето
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -478,8 +478,8 @@ Errors:
|
||||
Terminated: 会话已经终止
|
||||
Token:
|
||||
Invalid: 会话令牌是无效的
|
||||
Passkey:
|
||||
NoChallenge: 没有密码挑战的会话
|
||||
WebAuthN:
|
||||
NoChallenge: 没有 WebAuthN 质询的会话
|
||||
Intent:
|
||||
IDPMissing: 请求中缺少IDP ID
|
||||
SuccessURLMissing: 请求中缺少成功URL
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -7,6 +7,7 @@ deps:
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
ignore_unstable_packages: true
|
||||
lint:
|
||||
use:
|
||||
- MINIMAL
|
||||
|
@ -2,18 +2,47 @@ syntax = "proto3";
|
||||
|
||||
package zitadel.session.v2alpha;
|
||||
|
||||
import "google/api/field_behavior.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
import "protoc-gen-openapiv2/options/annotations.proto";
|
||||
import "validate/validate.proto";
|
||||
|
||||
option go_package = "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha;session";
|
||||
|
||||
enum ChallengeKind {
|
||||
CHALLENGE_KIND_UNSPECIFIED = 0;
|
||||
CHALLENGE_KIND_PASSKEY = 1;
|
||||
enum UserVerificationRequirement {
|
||||
USER_VERIFICATION_REQUIREMENT_UNSPECIFIED = 0;
|
||||
USER_VERIFICATION_REQUIREMENT_REQUIRED = 1;
|
||||
USER_VERIFICATION_REQUIREMENT_PREFERRED = 2;
|
||||
USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3;
|
||||
}
|
||||
|
||||
message RequestChallenges {
|
||||
message WebAuthN {
|
||||
string domain = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"Domain on which the session was created. Will be used in the WebAuthN challenge.\"";
|
||||
}
|
||||
];
|
||||
UserVerificationRequirement user_verification_requirement = 2 [
|
||||
(validate.rules).enum = {
|
||||
defined_only: true,
|
||||
not_in: [0]
|
||||
},
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"User verification that is required during validation. When set to `USER_VERIFICATION_REQUIREMENT_REQUIRED` the behaviour is for passkey authentication. Other values will mean U2F\"";
|
||||
ref: "https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
optional WebAuthN web_auth_n = 1;
|
||||
}
|
||||
|
||||
message Challenges {
|
||||
message Passkey {
|
||||
message WebAuthN {
|
||||
google.protobuf.Struct public_key_credential_request_options = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "Options for Assertion Generaration (dictionary PublicKeyCredentialRequestOptions). Generated helper methods transform the field to JSON, for use in a WebauthN client. See also: https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrequestoptions"
|
||||
@ -22,5 +51,5 @@ message Challenges {
|
||||
];
|
||||
}
|
||||
|
||||
optional Passkey passkey = 1;
|
||||
optional WebAuthN web_auth_n = 1;
|
||||
}
|
||||
|
@ -39,17 +39,12 @@ message Session {
|
||||
description: "\"custom key value list\"";
|
||||
}
|
||||
];
|
||||
string domain = 7 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"domain on which the session was created\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message Factors {
|
||||
UserFactor user = 1;
|
||||
PasswordFactor password = 2;
|
||||
PasskeyFactor passkey = 3;
|
||||
WebAuthNFactor web_auth_n = 3;
|
||||
IntentFactor intent = 4;
|
||||
}
|
||||
|
||||
@ -97,12 +92,13 @@ message IntentFactor {
|
||||
];
|
||||
}
|
||||
|
||||
message PasskeyFactor {
|
||||
message WebAuthNFactor {
|
||||
google.protobuf.Timestamp verified_at = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"time when the passkey challenge was last checked\"";
|
||||
}
|
||||
];
|
||||
bool user_verified = 2;
|
||||
}
|
||||
|
||||
message SearchQuery {
|
||||
|
@ -244,12 +244,7 @@ message CreateSessionRequest{
|
||||
description: "\"custom key value list to be stored on the session\"";
|
||||
}
|
||||
];
|
||||
repeated ChallengeKind challenges = 3;
|
||||
string domain = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"Domain on which the session was created. Will be used for Passkey and U2F challenges.\"";
|
||||
}
|
||||
];
|
||||
RequestChallenges challenges = 3;
|
||||
}
|
||||
|
||||
message CreateSessionResponse{
|
||||
@ -296,7 +291,7 @@ message SetSessionRequest{
|
||||
description: "\"custom key value list to be stored on the session\"";
|
||||
}
|
||||
];
|
||||
repeated ChallengeKind challenges = 5;
|
||||
RequestChallenges challenges = 5;
|
||||
}
|
||||
|
||||
message SetSessionResponse{
|
||||
@ -306,7 +301,7 @@ message SetSessionResponse{
|
||||
description: "\"token of the session, which is required for further updates of the session or the request other resources\"";
|
||||
}
|
||||
];
|
||||
Challenges challenges = 3;
|
||||
Challenges challenges = 3;
|
||||
}
|
||||
|
||||
message DeleteSessionRequest{
|
||||
@ -341,9 +336,9 @@ message Checks {
|
||||
description: "\"Checks the password and updates the session on success. Requires that the user is already checked, either in the previous or the same request.\"";
|
||||
}
|
||||
];
|
||||
optional CheckPasskey passkey = 3 [
|
||||
optional CheckWebAuthN web_auth_n = 3 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"Checks the public key credential issued by the passkey client. Requires that the user is already checked and a passkey challenge to be requested, in any previous request.\"";
|
||||
description: "\"Checks the public key credential issued by the WebAuthN client. Requires that the user is already checked and a WebAuthN challenge to be requested, in any previous request.\"";
|
||||
}
|
||||
];
|
||||
optional CheckIntent intent = 4 [
|
||||
@ -385,12 +380,12 @@ message CheckPassword {
|
||||
];
|
||||
}
|
||||
|
||||
message CheckPasskey {
|
||||
message CheckWebAuthN {
|
||||
google.protobuf.Struct credential_assertion_data = 1 [
|
||||
(validate.rules).message.required = true,
|
||||
(google.api.field_behavior) = REQUIRED,
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "JSON representation of public key credential issued by the passkey client";
|
||||
description: "JSON representation of public key credential issued by the webAuthN client";
|
||||
min_length: 55;
|
||||
max_length: 1048576; //1 MB
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user