mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-12 02:54:20 +00:00
feat: session checks with intent (#6031)
* feat: session checks with intent * feat: session checks with intent * fix: integration tests for intent session * fix: integration tests for intent session * fix merge * fix: integration tests for intent session --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
parent
c12d94f7d4
commit
1b5d6ce89e
@ -119,6 +119,7 @@ func factorsToPb(s *query.Session) *session.Factors {
|
||||
User: user,
|
||||
Password: passwordFactorToPb(s.PasswordFactor),
|
||||
Passkey: passkeyFactorToPb(s.PasskeyFactor),
|
||||
Intent: intentFactorToPb(s.IntentFactor),
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +132,15 @@ func passwordFactorToPb(factor query.SessionPasswordFactor) *session.PasswordFac
|
||||
}
|
||||
}
|
||||
|
||||
func intentFactorToPb(factor query.SessionIntentFactor) *session.IntentFactor {
|
||||
if factor.IntentCheckedAt.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return &session.IntentFactor{
|
||||
VerifiedAt: timestamppb.New(factor.IntentCheckedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func passkeyFactorToPb(factor query.SessionPasskeyFactor) *session.PasskeyFactor {
|
||||
if factor.PasskeyCheckedAt.IsZero() {
|
||||
return nil
|
||||
@ -229,6 +239,9 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
|
||||
if password := checks.GetPassword(); password != nil {
|
||||
sessionChecks = append(sessionChecks, command.CheckPassword(password.GetPassword()))
|
||||
}
|
||||
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()))
|
||||
}
|
||||
|
@ -10,19 +10,21 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client session.SessionServiceClient
|
||||
User *user.AddHumanUserResponse
|
||||
CTX context.Context
|
||||
Tester *integration.Tester
|
||||
Client session.SessionServiceClient
|
||||
User *user.AddHumanUserResponse
|
||||
GenericOAuthIDPID string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@ -82,6 +84,7 @@ const (
|
||||
wantUserFactor wantFactor = iota
|
||||
wantPasswordFactor
|
||||
wantPasskeyFactor
|
||||
wantIntentFactor
|
||||
)
|
||||
|
||||
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, want []wantFactor) {
|
||||
@ -100,6 +103,10 @@ func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration,
|
||||
pf := factors.GetPasskey()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
case wantIntentFactor:
|
||||
pf := factors.GetIntent()
|
||||
assert.NotNil(t, pf)
|
||||
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -212,6 +219,108 @@ func TestServer_CreateSession_passkey(t *testing.T) {
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantPasskeyFactor)
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntent(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t)
|
||||
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil)
|
||||
|
||||
intentID, token, _, _ := Tester.CreateSuccessfulIntent(t, idpID, User.GetUserId(), "id")
|
||||
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: createResp.GetSessionToken(),
|
||||
Checks: &session.Checks{
|
||||
Intent: &session.CheckIntent{
|
||||
IntentId: intentID,
|
||||
Token: token,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantIntentFactor)
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_successfulIntentUnknownUserID(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t)
|
||||
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil)
|
||||
|
||||
idpUserID := "id"
|
||||
intentID, token, _, _ := Tester.CreateSuccessfulIntent(t, idpID, "", idpUserID)
|
||||
updateResp, err := Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: createResp.GetSessionToken(),
|
||||
Checks: &session.Checks{
|
||||
Intent: &session.CheckIntent{
|
||||
IntentId: intentID,
|
||||
Token: token,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
Tester.CreateUserIDPlink(CTX, User.GetUserId(), idpUserID, idpID, User.GetUserId())
|
||||
updateResp, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: createResp.GetSessionToken(),
|
||||
Checks: &session.Checks{
|
||||
Intent: &session.CheckIntent{
|
||||
IntentId: intentID,
|
||||
Token: token,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), updateResp.GetSessionToken(), updateResp.GetDetails().GetSequence(), time.Minute, nil, wantUserFactor, wantIntentFactor)
|
||||
}
|
||||
|
||||
func TestServer_CreateSession_startedIntentFalseToken(t *testing.T) {
|
||||
idpID := Tester.AddGenericOAuthProvider(t)
|
||||
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{
|
||||
UserId: User.GetUserId(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), createResp.GetSessionToken(), createResp.GetDetails().GetSequence(), time.Minute, nil)
|
||||
|
||||
intentID := Tester.CreateIntent(t, idpID)
|
||||
_, err = Client.SetSession(CTX, &session.SetSessionRequest{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: createResp.GetSessionToken(),
|
||||
Checks: &session.Checks{
|
||||
Intent: &session.CheckIntent{
|
||||
IntentId: intentID,
|
||||
Token: "false",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestServer_SetSession_flow(t *testing.T) {
|
||||
var wantFactors []wantFactor
|
||||
|
||||
|
@ -2,7 +2,6 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
@ -205,21 +204,7 @@ func intentToIDPInformationPb(intent *command.IDPIntentWriteModel, alg crypto.En
|
||||
}
|
||||
|
||||
func (s *Server) checkIntentToken(token string, intentID string) error {
|
||||
if token == "" {
|
||||
return errors.ThrowPermissionDenied(nil, "IDP-Sfefs", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
data, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "IDP-Swg31", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
decryptedToken, err := s.idpAlg.Decrypt(data, s.idpAlg.EncryptionKeyID())
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "IDP-Sf4gt", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
if string(decryptedToken) != intentID {
|
||||
return errors.ThrowPermissionDenied(nil, "IDP-dkje3", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
return nil
|
||||
return crypto.CheckToken(s.idpAlg, token, intentID)
|
||||
}
|
||||
|
||||
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {
|
||||
|
@ -13,17 +13,11 @@ import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
@ -50,63 +44,8 @@ func TestMain(m *testing.M) {
|
||||
}())
|
||||
}
|
||||
|
||||
func createProvider(t *testing.T) string {
|
||||
ctx := authz.WithInstance(context.Background(), Tester.Instance)
|
||||
id, _, err := Tester.Commands.AddOrgGenericOAuthProvider(ctx, Tester.Organisation.ID, command.GenericOAuthProvider{
|
||||
"idp",
|
||||
"clientID",
|
||||
"clientSecret",
|
||||
"https://example.com/oauth/v2/authorize",
|
||||
"https://example.com/oauth/v2/token",
|
||||
"https://api.example.com/user",
|
||||
[]string{"openid", "profile", "email"},
|
||||
"id",
|
||||
idp.Options{
|
||||
IsLinkingAllowed: true,
|
||||
IsCreationAllowed: true,
|
||||
IsAutoCreation: true,
|
||||
IsAutoUpdate: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func createIntent(t *testing.T, idpID string) string {
|
||||
ctx := authz.WithInstance(context.Background(), Tester.Instance)
|
||||
id, _, err := Tester.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", Tester.Organisation.ID)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func createSuccessfulIntent(t *testing.T, idpID string) (string, string, time.Time, uint64) {
|
||||
ctx := authz.WithInstance(context.Background(), Tester.Instance)
|
||||
intentID := createIntent(t, idpID)
|
||||
writeModel, err := Tester.Commands.GetIntentWriteModel(ctx, intentID, Tester.Organisation.ID)
|
||||
require.NoError(t, err)
|
||||
idpUser := openid.NewUser(
|
||||
&oidc.UserInfo{
|
||||
Subject: "id",
|
||||
UserInfoProfile: oidc.UserInfoProfile{
|
||||
PreferredUsername: "username",
|
||||
},
|
||||
},
|
||||
)
|
||||
idpSession := &openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
}
|
||||
token, err := Tester.Commands.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, "")
|
||||
require.NoError(t, err)
|
||||
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
|
||||
}
|
||||
|
||||
func TestServer_AddHumanUser(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
idpID := Tester.AddGenericOAuthProvider(t)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddHumanUserRequest
|
||||
@ -483,7 +422,7 @@ func TestServer_AddHumanUser(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_AddIDPLink(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
idpID := Tester.AddGenericOAuthProvider(t)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddIDPLinkRequest
|
||||
@ -563,7 +502,7 @@ func TestServer_AddIDPLink(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_StartIdentityProviderFlow(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
idpID := Tester.AddGenericOAuthProvider(t)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.StartIdentityProviderFlowRequest
|
||||
@ -627,9 +566,9 @@ func TestServer_StartIdentityProviderFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_RetrieveIdentityProviderInformation(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
intentID := createIntent(t, idpID)
|
||||
successfulID, token, changeDate, sequence := createSuccessfulIntent(t, idpID)
|
||||
idpID := Tester.AddGenericOAuthProvider(t)
|
||||
intentID := Tester.CreateIntent(t, idpID)
|
||||
successfulID, token, changeDate, sequence := Tester.CreateSuccessfulIntent(t, idpID, "", "id")
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RetrieveIdentityProviderInformationRequest
|
||||
|
@ -119,7 +119,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd, err := idpintent.NewSucceededEvent(
|
||||
cmd := idpintent.NewSucceededEvent(
|
||||
ctx,
|
||||
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
|
||||
idpInfo,
|
||||
@ -129,9 +129,6 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
|
||||
accessToken,
|
||||
idToken,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -435,24 +435,21 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
func() eventstore.Command {
|
||||
event, _ := idpintent.NewSucceededEvent(
|
||||
context.Background(),
|
||||
&idpintent.NewAggregate("id", "ro").Aggregate,
|
||||
[]byte(`{"sub":"id","preferred_username":"username"}`),
|
||||
"id",
|
||||
"username",
|
||||
"",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
"idToken",
|
||||
)
|
||||
return event
|
||||
}(),
|
||||
idpintent.NewSucceededEvent(
|
||||
context.Background(),
|
||||
&idpintent.NewAggregate("id", "ro").Aggregate,
|
||||
[]byte(`{"sub":"id","preferred_username":"username"}`),
|
||||
"id",
|
||||
"username",
|
||||
"",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
"idToken",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -23,8 +23,10 @@ type SessionCommands struct {
|
||||
|
||||
sessionWriteModel *SessionWriteModel
|
||||
passwordWriteModel *HumanPasswordWriteModel
|
||||
intentWriteModel *IDPIntentWriteModel
|
||||
eventstore *eventstore.Eventstore
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
intentAlg crypto.EncryptionAlgorithm
|
||||
createToken func(sessionID string) (id string, token string, err error)
|
||||
now func() time.Time
|
||||
}
|
||||
@ -35,6 +37,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
|
||||
sessionWriteModel: session,
|
||||
eventstore: c.eventstore,
|
||||
userPasswordAlg: c.userPasswordAlg,
|
||||
intentAlg: c.idpConfigEncryption,
|
||||
createToken: c.sessionTokenCreator,
|
||||
now: time.Now,
|
||||
}
|
||||
@ -80,6 +83,42 @@ func CheckPassword(password string) SessionCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckIntent defines a check for a succeeded intent to be executed for a session update
|
||||
func CheckIntent(intentID, token string) SessionCommand {
|
||||
return func(ctx context.Context, cmd *SessionCommands) error {
|
||||
if cmd.sessionWriteModel.UserID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Sfw3r", "Errors.User.UserIDMissing")
|
||||
}
|
||||
if err := crypto.CheckToken(cmd.intentAlg, token, intentID); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.intentWriteModel = NewIDPIntentWriteModel(intentID, "")
|
||||
err := cmd.eventstore.FilterToQueryReducer(ctx, cmd.intentWriteModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cmd.intentWriteModel.State != domain.IDPIntentStateSucceeded {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded")
|
||||
}
|
||||
if cmd.intentWriteModel.UserID != "" {
|
||||
if cmd.intentWriteModel.UserID != cmd.sessionWriteModel.UserID {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
|
||||
}
|
||||
} else {
|
||||
linkWriteModel := NewUserIDPLinkWriteModel(cmd.sessionWriteModel.UserID, cmd.intentWriteModel.IDPID, cmd.intentWriteModel.IDPUserID, cmd.intentWriteModel.ResourceOwner)
|
||||
err := cmd.eventstore.FilterToQueryReducer(ctx, linkWriteModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if linkWriteModel.State != domain.UserIDPLinkStateActive {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser")
|
||||
}
|
||||
}
|
||||
cmd.sessionWriteModel.IntentChecked(ctx, cmd.now())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Exec will execute the commands specified and returns an error on the first occurrence
|
||||
func (s *SessionCommands) Exec(ctx context.Context) error {
|
||||
for _, cmd := range s.cmds {
|
||||
|
@ -38,6 +38,7 @@ type SessionWriteModel struct {
|
||||
UserID string
|
||||
UserCheckedAt time.Time
|
||||
PasswordCheckedAt time.Time
|
||||
IntentCheckedAt time.Time
|
||||
PasskeyCheckedAt time.Time
|
||||
Metadata map[string][]byte
|
||||
State domain.SessionState
|
||||
@ -68,6 +69,8 @@ func (wm *SessionWriteModel) Reduce() error {
|
||||
wm.reduceUserChecked(e)
|
||||
case *session.PasswordCheckedEvent:
|
||||
wm.reducePasswordChecked(e)
|
||||
case *session.IntentCheckedEvent:
|
||||
wm.reduceIntentChecked(e)
|
||||
case *session.PasskeyChallengedEvent:
|
||||
wm.reducePasskeyChallenged(e)
|
||||
case *session.PasskeyCheckedEvent:
|
||||
@ -90,6 +93,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
session.AddedType,
|
||||
session.UserCheckedType,
|
||||
session.PasswordCheckedType,
|
||||
session.IntentCheckedType,
|
||||
session.PasskeyChallengedType,
|
||||
session.PasskeyCheckedType,
|
||||
session.TokenSetType,
|
||||
@ -117,6 +121,10 @@ func (wm *SessionWriteModel) reducePasswordChecked(e *session.PasswordCheckedEve
|
||||
wm.PasswordCheckedAt = e.CheckedAt
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reduceIntentChecked(e *session.IntentCheckedEvent) {
|
||||
wm.IntentCheckedAt = e.CheckedAt
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) reducePasskeyChallenged(e *session.PasskeyChallengedEvent) {
|
||||
wm.PasskeyChallenge = &PasskeyChallengeModel{
|
||||
Challenge: e.Challenge,
|
||||
@ -153,6 +161,10 @@ func (wm *SessionWriteModel) PasswordChecked(ctx context.Context, checkedAt time
|
||||
wm.commands = append(wm.commands, session.NewPasswordCheckedEvent(ctx, wm.aggregate, checkedAt))
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) IntentChecked(ctx context.Context, checkedAt time.Time) {
|
||||
wm.commands = append(wm.commands, session.NewIntentCheckedEvent(ctx, wm.aggregate, checkedAt))
|
||||
}
|
||||
|
||||
func (wm *SessionWriteModel) PasskeyChallenged(ctx context.Context, challenge string, allowedCrentialIDs [][]byte, userVerification domain.UserVerificationRequirement) {
|
||||
wm.commands = append(wm.commands, session.NewPasskeyChallengedEvent(ctx, wm.aggregate, challenge, allowedCrentialIDs, userVerification))
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
@ -341,6 +342,19 @@ func TestCommands_UpdateSession(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCommands_updateSession(t *testing.T) {
|
||||
decryption := func(err error) crypto.EncryptionAlgorithm {
|
||||
mCrypto := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
||||
mCrypto.EXPECT().EncryptionKeyID().Return("id")
|
||||
mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(code []byte, keyID string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(code), nil
|
||||
})
|
||||
return mCrypto
|
||||
}
|
||||
|
||||
testNow := time.Now()
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
@ -484,6 +498,194 @@ func TestCommands_updateSession(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"set user, intent not successful",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent", "aW50ZW50"),
|
||||
},
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
|
||||
),
|
||||
),
|
||||
),
|
||||
createToken: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
intentAlg: decryption(nil),
|
||||
now: func() time.Time {
|
||||
return testNow
|
||||
},
|
||||
},
|
||||
metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Df4bw", "Errors.Intent.NotSucceeded"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"set user, intent not for user",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent", "aW50ZW50"),
|
||||
},
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
idpintent.NewSucceededEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate,
|
||||
nil,
|
||||
"idpUserID",
|
||||
"idpUserName",
|
||||
"userID2",
|
||||
nil,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
createToken: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
intentAlg: decryption(nil),
|
||||
now: func() time.Time {
|
||||
return testNow
|
||||
},
|
||||
},
|
||||
metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-O8xk3w", "Errors.Intent.OtherUser"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"set user, intent incorrect token",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent2", "aW50ZW50"),
|
||||
},
|
||||
eventstore: eventstoreExpect(t),
|
||||
createToken: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
intentAlg: decryption(nil),
|
||||
now: func() time.Time {
|
||||
return testNow
|
||||
},
|
||||
},
|
||||
metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: caos_errs.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"set user, intent, metadata and token",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
session.NewUserCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"userID", testNow),
|
||||
session.NewIntentCheckedEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
testNow),
|
||||
session.NewMetadataSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
map[string][]byte{"key": []byte("value")}),
|
||||
session.NewTokenSetEvent(context.Background(), &session.NewAggregate("sessionID", "org1").Aggregate,
|
||||
"tokenID"),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
checks: &SessionCommands{
|
||||
sessionWriteModel: NewSessionWriteModel("sessionID", "org1"),
|
||||
cmds: []SessionCommand{
|
||||
CheckUser("userID"),
|
||||
CheckIntent("intent", "aW50ZW50"),
|
||||
},
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "", "", "", "", language.English, domain.GenderUnspecified, "", false),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
idpintent.NewSucceededEvent(context.Background(), &idpintent.NewAggregate("intent", "org1").Aggregate,
|
||||
nil,
|
||||
"idpUserID",
|
||||
"idpUsername",
|
||||
"userID",
|
||||
nil,
|
||||
"",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
createToken: func(sessionID string) (string, string, error) {
|
||||
return "tokenID",
|
||||
"token",
|
||||
nil
|
||||
},
|
||||
intentAlg: decryption(nil),
|
||||
now: func() time.Time {
|
||||
return testNow
|
||||
},
|
||||
},
|
||||
metadata: map[string][]byte{
|
||||
"key": []byte("value"),
|
||||
},
|
||||
},
|
||||
res{
|
||||
want: &SessionChanged{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
ID: "sessionID",
|
||||
NewToken: "token",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -2,6 +2,7 @@ package crypto
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
@ -132,3 +133,21 @@ func FillHash(value []byte, alg HashAlgorithm) *CryptoValue {
|
||||
Crypted: value,
|
||||
}
|
||||
}
|
||||
|
||||
func CheckToken(alg EncryptionAlgorithm, token string, content string) error {
|
||||
if token == "" {
|
||||
return errors.ThrowPermissionDenied(nil, "CRYPTO-Sfefs", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
data, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "CRYPTO-Swg31", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
decryptedToken, err := alg.DecryptString(data, alg.EncryptionKeyID())
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "CRYPTO-Sf4gt", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
if decryptedToken != content {
|
||||
return errors.ThrowPermissionDenied(nil, "CRYPTO-CRYPTO", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -3,11 +3,19 @@ package integration
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
@ -55,6 +63,22 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Tester) CreateUserIDPlink(ctx context.Context, userID, externalID, idpID, username string) *user.AddIDPLinkResponse {
|
||||
resp, err := s.Client.UserV2.AddIDPLink(
|
||||
ctx,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: userID,
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
UserId: externalID,
|
||||
UserName: username,
|
||||
},
|
||||
},
|
||||
)
|
||||
logging.OnError(err).Fatal("create human user link")
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
|
||||
reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
@ -78,3 +102,58 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
|
||||
})
|
||||
logging.OnError(err).Fatal("create user passkey")
|
||||
}
|
||||
|
||||
func (s *Tester) AddGenericOAuthProvider(t *testing.T) string {
|
||||
ctx := authz.WithInstance(context.Background(), s.Instance)
|
||||
id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, s.Organisation.ID, command.GenericOAuthProvider{
|
||||
Name: "idp",
|
||||
ClientID: "clientID",
|
||||
ClientSecret: "clientSecret",
|
||||
AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
|
||||
TokenEndpoint: "https://example.com/oauth/v2/token",
|
||||
UserEndpoint: "https://api.example.com/user",
|
||||
Scopes: []string{"openid", "profile", "email"},
|
||||
IDAttribute: "id",
|
||||
IDPOptions: idp.Options{
|
||||
IsLinkingAllowed: true,
|
||||
IsCreationAllowed: true,
|
||||
IsAutoCreation: true,
|
||||
IsAutoUpdate: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *Tester) CreateIntent(t *testing.T, idpID string) string {
|
||||
ctx := authz.WithInstance(context.Background(), s.Instance)
|
||||
id, _, err := s.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", s.Organisation.ID)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *Tester) CreateSuccessfulIntent(t *testing.T, idpID, userID, idpUserID string) (string, string, time.Time, uint64) {
|
||||
ctx := authz.WithInstance(context.Background(), s.Instance)
|
||||
intentID := s.CreateIntent(t, idpID)
|
||||
writeModel, err := s.Commands.GetIntentWriteModel(ctx, intentID, s.Organisation.ID)
|
||||
require.NoError(t, err)
|
||||
idpUser := openid.NewUser(
|
||||
&oidc.UserInfo{
|
||||
Subject: idpUserID,
|
||||
UserInfoProfile: oidc.UserInfoProfile{
|
||||
PreferredUsername: "username",
|
||||
},
|
||||
},
|
||||
)
|
||||
idpSession := &openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
}
|
||||
token, err := s.Commands.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, userID)
|
||||
require.NoError(t, err)
|
||||
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SessionsProjectionTable = "projections.sessions1"
|
||||
SessionsProjectionTable = "projections.sessions2"
|
||||
|
||||
SessionColumnID = "id"
|
||||
SessionColumnCreationDate = "creation_date"
|
||||
@ -27,6 +27,7 @@ const (
|
||||
SessionColumnUserID = "user_id"
|
||||
SessionColumnUserCheckedAt = "user_checked_at"
|
||||
SessionColumnPasswordCheckedAt = "password_checked_at"
|
||||
SessionColumnIntentCheckedAt = "intent_checked_at"
|
||||
SessionColumnPasskeyCheckedAt = "passkey_checked_at"
|
||||
SessionColumnMetadata = "metadata"
|
||||
SessionColumnTokenID = "token_id"
|
||||
@ -53,6 +54,7 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi
|
||||
crdb.NewColumn(SessionColumnUserID, crdb.ColumnTypeText, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnUserCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnPasswordCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnIntentCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnPasskeyCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()),
|
||||
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
|
||||
@ -81,6 +83,10 @@ func (p *sessionProjection) reducers() []handler.AggregateReducer {
|
||||
Event: session.PasswordCheckedType,
|
||||
Reduce: p.reducePasswordChecked,
|
||||
},
|
||||
{
|
||||
Event: session.IntentCheckedType,
|
||||
Reduce: p.reduceIntentChecked,
|
||||
},
|
||||
{
|
||||
Event: session.PasskeyCheckedType,
|
||||
Reduce: p.reducePasskeyChecked,
|
||||
@ -181,6 +187,26 @@ func (p *sessionProjection) reducePasswordChecked(event eventstore.Event) (*hand
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reduceIntentChecked(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.IntentCheckedEvent)
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SDgr2", "reduce.wrong.event.type %s", session.IntentCheckedType)
|
||||
}
|
||||
|
||||
return crdb.NewUpdateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(SessionColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(SessionColumnSequence, e.Sequence()),
|
||||
handler.NewCol(SessionColumnIntentCheckedAt, e.CheckedAt),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(SessionColumnID, e.Aggregate().ID),
|
||||
handler.NewCond(SessionColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *sessionProjection) reducePasskeyChecked(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*session.PasskeyCheckedEvent)
|
||||
if !ok {
|
||||
|
@ -41,7 +41,7 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "INSERT INTO projections.sessions1 (id, instance_id, creation_date, change_date, resource_owner, state, sequence, creator) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
|
||||
expectedStmt: "INSERT INTO projections.sessions2 (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",
|
||||
@ -77,7 +77,7 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
|
||||
expectedStmt: "UPDATE projections.sessions2 SET (change_date, sequence, user_id, user_checked_at) = ($1, $2, $3, $4) WHERE (id = $5) AND (instance_id = $6)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
@ -110,7 +110,39 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedStmt: "UPDATE projections.sessions2 SET (change_date, sequence, password_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
time.Date(2023, time.May, 4, 0, 0, 0, 0, time.UTC),
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "instance reduceIntentChecked",
|
||||
args: args{
|
||||
event: getEvent(testEvent(
|
||||
session.AddedType,
|
||||
session.AggregateType,
|
||||
[]byte(`{
|
||||
"checkedAt": "2023-05-04T00:00:00Z"
|
||||
}`),
|
||||
), session.IntentCheckedEventMapper),
|
||||
},
|
||||
reduce: (&sessionProjection{}).reduceIntentChecked,
|
||||
want: wantReduce{
|
||||
aggregateType: eventstore.AggregateType("session"),
|
||||
sequence: 15,
|
||||
previousSequence: 10,
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions2 SET (change_date, sequence, intent_checked_at) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
@ -142,7 +174,7 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedStmt: "UPDATE projections.sessions2 SET (change_date, sequence, token_id) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
@ -176,7 +208,7 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions1 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedStmt: "UPDATE projections.sessions2 SET (change_date, sequence, metadata) = ($1, $2, $3) WHERE (id = $4) AND (instance_id = $5)",
|
||||
expectedArgs: []interface{}{
|
||||
anyArg{},
|
||||
anyArg{},
|
||||
@ -208,7 +240,7 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.sessions1 WHERE (id = $1) AND (instance_id = $2)",
|
||||
expectedStmt: "DELETE FROM projections.sessions2 WHERE (id = $1) AND (instance_id = $2)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
"instance-id",
|
||||
@ -235,7 +267,7 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "DELETE FROM projections.sessions1 WHERE (instance_id = $1)",
|
||||
expectedStmt: "DELETE FROM projections.sessions2 WHERE (instance_id = $1)",
|
||||
expectedArgs: []interface{}{
|
||||
"agg-id",
|
||||
},
|
||||
@ -266,7 +298,7 @@ func TestSessionProjection_reduces(t *testing.T) {
|
||||
executer: &testExecuter{
|
||||
executions: []execution{
|
||||
{
|
||||
expectedStmt: "UPDATE projections.sessions1 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)",
|
||||
expectedStmt: "UPDATE projections.sessions2 SET password_checked_at = $1 WHERE (user_id = $2) AND (password_checked_at < $3)",
|
||||
expectedArgs: []interface{}{
|
||||
nil,
|
||||
"agg-id",
|
||||
|
@ -32,6 +32,7 @@ type Session struct {
|
||||
Creator string
|
||||
UserFactor SessionUserFactor
|
||||
PasswordFactor SessionPasswordFactor
|
||||
IntentFactor SessionIntentFactor
|
||||
PasskeyFactor SessionPasskeyFactor
|
||||
Metadata map[string][]byte
|
||||
}
|
||||
@ -47,6 +48,10 @@ type SessionPasswordFactor struct {
|
||||
PasswordCheckedAt time.Time
|
||||
}
|
||||
|
||||
type SessionIntentFactor struct {
|
||||
IntentCheckedAt time.Time
|
||||
}
|
||||
|
||||
type SessionPasskeyFactor struct {
|
||||
PasskeyCheckedAt time.Time
|
||||
}
|
||||
@ -113,6 +118,10 @@ var (
|
||||
name: projection.SessionColumnPasswordCheckedAt,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnIntentCheckedAt = Column{
|
||||
name: projection.SessionColumnIntentCheckedAt,
|
||||
table: sessionsTable,
|
||||
}
|
||||
SessionColumnPasskeyCheckedAt = Column{
|
||||
name: projection.SessionColumnPasskeyCheckedAt,
|
||||
table: sessionsTable,
|
||||
@ -207,6 +216,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
LoginNameNameCol.identifier(),
|
||||
HumanDisplayNameCol.identifier(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnIntentCheckedAt.identifier(),
|
||||
SessionColumnPasskeyCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
SessionColumnToken.identifier(),
|
||||
@ -222,6 +232,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
loginName sql.NullString
|
||||
displayName sql.NullString
|
||||
passwordCheckedAt sql.NullTime
|
||||
intentCheckedAt sql.NullTime
|
||||
passkeyCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
token sql.NullString
|
||||
@ -240,6 +251,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
&loginName,
|
||||
&displayName,
|
||||
&passwordCheckedAt,
|
||||
&intentCheckedAt,
|
||||
&passkeyCheckedAt,
|
||||
&metadata,
|
||||
&token,
|
||||
@ -257,6 +269,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
session.UserFactor.LoginName = loginName.String
|
||||
session.UserFactor.DisplayName = displayName.String
|
||||
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
@ -278,6 +291,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
LoginNameNameCol.identifier(),
|
||||
HumanDisplayNameCol.identifier(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnIntentCheckedAt.identifier(),
|
||||
SessionColumnPasskeyCheckedAt.identifier(),
|
||||
SessionColumnMetadata.identifier(),
|
||||
countColumn.identifier(),
|
||||
@ -296,6 +310,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
loginName sql.NullString
|
||||
displayName sql.NullString
|
||||
passwordCheckedAt sql.NullTime
|
||||
intentCheckedAt sql.NullTime
|
||||
passkeyCheckedAt sql.NullTime
|
||||
metadata database.Map[[]byte]
|
||||
)
|
||||
@ -313,6 +328,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
&loginName,
|
||||
&displayName,
|
||||
&passwordCheckedAt,
|
||||
&intentCheckedAt,
|
||||
&passkeyCheckedAt,
|
||||
&metadata,
|
||||
&sessions.Count,
|
||||
@ -326,6 +342,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
session.UserFactor.LoginName = loginName.String
|
||||
session.UserFactor.DisplayName = displayName.String
|
||||
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
|
||||
session.Metadata = metadata
|
||||
|
||||
|
@ -17,43 +17,45 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` +
|
||||
` projections.sessions1.creation_date,` +
|
||||
` projections.sessions1.change_date,` +
|
||||
` projections.sessions1.sequence,` +
|
||||
` projections.sessions1.state,` +
|
||||
` projections.sessions1.resource_owner,` +
|
||||
` projections.sessions1.creator,` +
|
||||
` projections.sessions1.user_id,` +
|
||||
` projections.sessions1.user_checked_at,` +
|
||||
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions2.id,` +
|
||||
` projections.sessions2.creation_date,` +
|
||||
` projections.sessions2.change_date,` +
|
||||
` projections.sessions2.sequence,` +
|
||||
` projections.sessions2.state,` +
|
||||
` projections.sessions2.resource_owner,` +
|
||||
` projections.sessions2.creator,` +
|
||||
` projections.sessions2.user_id,` +
|
||||
` projections.sessions2.user_checked_at,` +
|
||||
` projections.login_names2.login_name,` +
|
||||
` projections.users8_humans.display_name,` +
|
||||
` projections.sessions1.password_checked_at,` +
|
||||
` projections.sessions1.passkey_checked_at,` +
|
||||
` projections.sessions1.metadata,` +
|
||||
` projections.sessions1.token_id` +
|
||||
` FROM projections.sessions1` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions1.user_id = projections.login_names2.user_id AND projections.sessions1.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions1.user_id = projections.users8_humans.user_id AND projections.sessions1.instance_id = projections.users8_humans.instance_id` +
|
||||
` projections.sessions2.password_checked_at,` +
|
||||
` projections.sessions2.intent_checked_at,` +
|
||||
` projections.sessions2.passkey_checked_at,` +
|
||||
` projections.sessions2.metadata,` +
|
||||
` projections.sessions2.token_id` +
|
||||
` FROM projections.sessions2` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions2.user_id = projections.login_names2.user_id AND projections.sessions2.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions2.user_id = projections.users8_humans.user_id AND projections.sessions2.instance_id = projections.users8_humans.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` +
|
||||
` projections.sessions1.creation_date,` +
|
||||
` projections.sessions1.change_date,` +
|
||||
` projections.sessions1.sequence,` +
|
||||
` projections.sessions1.state,` +
|
||||
` projections.sessions1.resource_owner,` +
|
||||
` projections.sessions1.creator,` +
|
||||
` projections.sessions1.user_id,` +
|
||||
` projections.sessions1.user_checked_at,` +
|
||||
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions2.id,` +
|
||||
` projections.sessions2.creation_date,` +
|
||||
` projections.sessions2.change_date,` +
|
||||
` projections.sessions2.sequence,` +
|
||||
` projections.sessions2.state,` +
|
||||
` projections.sessions2.resource_owner,` +
|
||||
` projections.sessions2.creator,` +
|
||||
` projections.sessions2.user_id,` +
|
||||
` projections.sessions2.user_checked_at,` +
|
||||
` projections.login_names2.login_name,` +
|
||||
` projections.users8_humans.display_name,` +
|
||||
` projections.sessions1.password_checked_at,` +
|
||||
` projections.sessions1.passkey_checked_at,` +
|
||||
` projections.sessions1.metadata,` +
|
||||
` projections.sessions2.password_checked_at,` +
|
||||
` projections.sessions2.intent_checked_at,` +
|
||||
` projections.sessions2.passkey_checked_at,` +
|
||||
` projections.sessions2.metadata,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.sessions1` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions1.user_id = projections.login_names2.user_id AND projections.sessions1.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions1.user_id = projections.users8_humans.user_id AND projections.sessions1.instance_id = projections.users8_humans.instance_id` +
|
||||
` FROM projections.sessions2` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions2.user_id = projections.login_names2.user_id AND projections.sessions2.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions2.user_id = projections.users8_humans.user_id AND projections.sessions2.instance_id = projections.users8_humans.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
|
||||
sessionCols = []string{
|
||||
@ -69,6 +71,7 @@ var (
|
||||
"login_name",
|
||||
"display_name",
|
||||
"password_checked_at",
|
||||
"intent_checked_at",
|
||||
"passkey_checked_at",
|
||||
"metadata",
|
||||
"token",
|
||||
@ -87,6 +90,7 @@ var (
|
||||
"login_name",
|
||||
"display_name",
|
||||
"password_checked_at",
|
||||
"intent_checked_at",
|
||||
"passkey_checked_at",
|
||||
"metadata",
|
||||
"count",
|
||||
@ -138,6 +142,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
"display-name",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
},
|
||||
@ -165,6 +170,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
IntentFactor: SessionIntentFactor{
|
||||
IntentCheckedAt: testNow,
|
||||
},
|
||||
PasskeyFactor: SessionPasskeyFactor{
|
||||
PasskeyCheckedAt: testNow,
|
||||
},
|
||||
@ -197,6 +205,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
"display-name",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
{
|
||||
@ -213,6 +222,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
"display-name2",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
},
|
||||
},
|
||||
@ -240,6 +250,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
IntentFactor: SessionIntentFactor{
|
||||
IntentCheckedAt: testNow,
|
||||
},
|
||||
PasskeyFactor: SessionPasskeyFactor{
|
||||
PasskeyCheckedAt: testNow,
|
||||
},
|
||||
@ -264,6 +277,9 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
IntentFactor: SessionIntentFactor{
|
||||
IntentCheckedAt: testNow,
|
||||
},
|
||||
PasskeyFactor: SessionPasskeyFactor{
|
||||
PasskeyCheckedAt: testNow,
|
||||
},
|
||||
@ -349,6 +365,7 @@ func Test_SessionPrepare(t *testing.T) {
|
||||
"display-name",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
[]byte(`{"key": "dmFsdWU="}`),
|
||||
"tokenID",
|
||||
},
|
||||
@ -371,6 +388,9 @@ func Test_SessionPrepare(t *testing.T) {
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
},
|
||||
IntentFactor: SessionIntentFactor{
|
||||
IntentCheckedAt: testNow,
|
||||
},
|
||||
PasskeyFactor: SessionPasskeyFactor{
|
||||
PasskeyCheckedAt: testNow,
|
||||
},
|
||||
|
@ -85,7 +85,7 @@ func NewSucceededEvent(
|
||||
userID string,
|
||||
idpAccessToken *crypto.CryptoValue,
|
||||
idpIDToken string,
|
||||
) (*SucceededEvent, error) {
|
||||
) *SucceededEvent {
|
||||
return &SucceededEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
@ -98,7 +98,7 @@ func NewSucceededEvent(
|
||||
UserID: userID,
|
||||
IDPAccessToken: idpAccessToken,
|
||||
IDPIDToken: idpIDToken,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (e *SucceededEvent) Data() interface{} {
|
||||
|
@ -6,6 +6,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
es.RegisterFilterEventMapper(AggregateType, AddedType, AddedEventMapper).
|
||||
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, TokenSetType, TokenSetEventMapper).
|
||||
|
@ -16,6 +16,7 @@ const (
|
||||
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"
|
||||
@ -144,6 +145,47 @@ func PasswordCheckedEventMapper(event *repository.Event) (eventstore.Event, erro
|
||||
return added, nil
|
||||
}
|
||||
|
||||
type IntentCheckedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
CheckedAt time.Time `json:"checkedAt"`
|
||||
}
|
||||
|
||||
func (e *IntentCheckedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *IntentCheckedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewIntentCheckedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
checkedAt time.Time,
|
||||
) *IntentCheckedEvent {
|
||||
return &IntentCheckedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
IntentCheckedType,
|
||||
),
|
||||
CheckedAt: checkedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func IntentCheckedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
added := &IntentCheckedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
err := json.Unmarshal(event.Data, added)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "SESSION-DGt90", "unable to unmarshal intent checked")
|
||||
}
|
||||
|
||||
return added, nil
|
||||
}
|
||||
|
||||
type PasskeyChallengedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
|
@ -505,6 +505,8 @@ Errors:
|
||||
NotSucceeded: Намерението не е успешно
|
||||
TokenCreationFailed: Неуспешно създаване на токен
|
||||
InvalidToken: Знакът за намерение е невалиден
|
||||
OtherUser: Намерение, предназначено за друг потребител
|
||||
|
||||
AggregateTypes:
|
||||
action: Действие
|
||||
instance: Инстанция
|
||||
|
@ -487,6 +487,7 @@ Errors:
|
||||
NotSucceeded: Intent war nicht erfolgreich
|
||||
TokenCreationFailed: Tokenerstellung schlug fehl
|
||||
InvalidToken: Intent Token ist ungültig
|
||||
OtherUser: Intent ist für anderen Benutzer gedacht
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -487,6 +487,7 @@ Errors:
|
||||
NotSucceeded: Intent has not succeeded
|
||||
TokenCreationFailed: Token creation failed
|
||||
InvalidToken: Intent Token is invalid
|
||||
OtherUser: Intent meant for another user
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -487,6 +487,7 @@ Errors:
|
||||
NotSucceeded: Intento fallido
|
||||
TokenCreationFailed: Fallo en la creación del token
|
||||
InvalidToken: El token de la intención no es válido
|
||||
OtherUser: Destinado a otro usuario
|
||||
|
||||
AggregateTypes:
|
||||
action: Acción
|
||||
|
@ -487,6 +487,7 @@ Errors:
|
||||
NotSucceeded: l'intention n'a pas abouti
|
||||
TokenCreationFailed: La création du token a échoué
|
||||
InvalidToken: Le jeton d'intention n'est pas valide
|
||||
OtherUser: Intention destinée à un autre utilisateur
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -487,6 +487,7 @@ Errors:
|
||||
NotSucceeded: l'intento non è andato a buon fine
|
||||
TokenCreationFailed: creazione del token fallita
|
||||
InvalidToken: Il token dell'intento non è valido
|
||||
OtherUser: Intento destinato a un altro utente
|
||||
|
||||
AggregateTypes:
|
||||
action: Azione
|
||||
|
@ -476,6 +476,7 @@ Errors:
|
||||
NotSucceeded: インテントが成功しなかった
|
||||
TokenCreationFailed: トークンの作成に失敗しました
|
||||
InvalidToken: インテントのトークンが無効である
|
||||
OtherUser: 他のユーザーを意図している
|
||||
|
||||
AggregateTypes:
|
||||
action: アクション
|
||||
|
@ -487,6 +487,7 @@ Errors:
|
||||
NotSucceeded: intencja nie powiodła się
|
||||
TokenCreationFailed: Tworzenie tokena nie powiodło się
|
||||
InvalidToken: Token intencji jest nieprawidłowy
|
||||
OtherUser: Intencja przeznaczona dla innego użytkownika
|
||||
|
||||
AggregateTypes:
|
||||
action: Działanie
|
||||
|
@ -487,6 +487,7 @@ Errors:
|
||||
NotSucceeded: 意图不成功
|
||||
TokenCreationFailed: 令牌创建失败
|
||||
InvalidToken: 意图令牌是无效的
|
||||
OtherUser: 意图是为另一个用户准备的
|
||||
|
||||
AggregateTypes:
|
||||
action: 动作
|
||||
|
@ -45,6 +45,7 @@ message Factors {
|
||||
UserFactor user = 1;
|
||||
PasswordFactor password = 2;
|
||||
PasskeyFactor passkey = 3;
|
||||
IntentFactor intent = 4;
|
||||
}
|
||||
|
||||
message UserFactor {
|
||||
@ -78,6 +79,14 @@ message PasswordFactor {
|
||||
];
|
||||
}
|
||||
|
||||
message IntentFactor {
|
||||
google.protobuf.Timestamp verified_at = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"time when an intent was last checked\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message PasskeyFactor {
|
||||
google.protobuf.Timestamp verified_at = 1 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
|
@ -341,6 +341,11 @@ message Checks {
|
||||
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.\"";
|
||||
}
|
||||
];
|
||||
optional CheckIntent intent = 4 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"Checks the intent. Requires that the userlink is already checked and a successful intent.\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message CheckUser {
|
||||
@ -386,3 +391,24 @@ message CheckPasskey {
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message CheckIntent {
|
||||
string intent_id = 1 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "ID of the intent, previously returned on the success response of the IDP callback"
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\"";
|
||||
}
|
||||
];
|
||||
string token = 2 [
|
||||
(validate.rules).string = {min_len: 1, max_len: 200},
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "token of the intent, previously returned on the success response of the IDP callback"
|
||||
min_length: 1;
|
||||
max_length: 200;
|
||||
example: "\"SJKL3ioIDpo342ioqw98fjp3sdf32wahb=\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user