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:
Stefan Benz 2023-06-21 16:06:18 +02:00 committed by GitHub
parent c12d94f7d4
commit 1b5d6ce89e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 727 additions and 153 deletions

View File

@ -119,6 +119,7 @@ func factorsToPb(s *query.Session) *session.Factors {
User: user, User: user,
Password: passwordFactorToPb(s.PasswordFactor), Password: passwordFactorToPb(s.PasswordFactor),
Passkey: passkeyFactorToPb(s.PasskeyFactor), 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 { func passkeyFactorToPb(factor query.SessionPasskeyFactor) *session.PasskeyFactor {
if factor.PasskeyCheckedAt.IsZero() { if factor.PasskeyCheckedAt.IsZero() {
return nil return nil
@ -229,6 +239,9 @@ func (s *Server) checksToCommand(ctx context.Context, checks *session.Checks) ([
if password := checks.GetPassword(); password != nil { if password := checks.GetPassword(); password != nil {
sessionChecks = append(sessionChecks, command.CheckPassword(password.GetPassword())) 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 { if passkey := checks.GetPasskey(); passkey != nil {
sessionChecks = append(sessionChecks, s.command.CheckPasskey(passkey.GetCredentialAssertionData())) sessionChecks = append(sessionChecks, s.command.CheckPasskey(passkey.GetCredentialAssertionData()))
} }

View File

@ -10,19 +10,21 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/internal/integration"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha" session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
var ( var (
CTX context.Context CTX context.Context
Tester *integration.Tester Tester *integration.Tester
Client session.SessionServiceClient Client session.SessionServiceClient
User *user.AddHumanUserResponse User *user.AddHumanUserResponse
GenericOAuthIDPID string
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -82,6 +84,7 @@ const (
wantUserFactor wantFactor = iota wantUserFactor wantFactor = iota
wantPasswordFactor wantPasswordFactor
wantPasskeyFactor wantPasskeyFactor
wantIntentFactor
) )
func verifyFactors(t testing.TB, factors *session.Factors, window time.Duration, want []wantFactor) { 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() pf := factors.GetPasskey()
assert.NotNil(t, pf) assert.NotNil(t, pf)
assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window)) assert.WithinRange(t, pf.GetVerifiedAt().AsTime(), time.Now().Add(-window), time.Now().Add(window))
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) 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) { func TestServer_SetSession_flow(t *testing.T) {
var wantFactors []wantFactor var wantFactors []wantFactor

View File

@ -2,7 +2,6 @@ package user
import ( import (
"context" "context"
"encoding/base64"
"io" "io"
"golang.org/x/text/language" "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 { func (s *Server) checkIntentToken(token string, intentID string) error {
if token == "" { return crypto.CheckToken(s.idpAlg, token, intentID)
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
} }
func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) { func (s *Server) ListAuthenticationMethodTypes(ctx context.Context, req *user.ListAuthenticationMethodTypesRequest) (*user.ListAuthenticationMethodTypesResponse, error) {

View File

@ -13,17 +13,11 @@ import (
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "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/structpb"
"google.golang.org/protobuf/types/known/timestamppb" "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/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/integration"
"github.com/zitadel/zitadel/internal/repository/idp"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management" mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/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) { func TestServer_AddHumanUser(t *testing.T) {
idpID := createProvider(t) idpID := Tester.AddGenericOAuthProvider(t)
type args struct { type args struct {
ctx context.Context ctx context.Context
req *user.AddHumanUserRequest req *user.AddHumanUserRequest
@ -483,7 +422,7 @@ func TestServer_AddHumanUser(t *testing.T) {
} }
func TestServer_AddIDPLink(t *testing.T) { func TestServer_AddIDPLink(t *testing.T) {
idpID := createProvider(t) idpID := Tester.AddGenericOAuthProvider(t)
type args struct { type args struct {
ctx context.Context ctx context.Context
req *user.AddIDPLinkRequest req *user.AddIDPLinkRequest
@ -563,7 +502,7 @@ func TestServer_AddIDPLink(t *testing.T) {
} }
func TestServer_StartIdentityProviderFlow(t *testing.T) { func TestServer_StartIdentityProviderFlow(t *testing.T) {
idpID := createProvider(t) idpID := Tester.AddGenericOAuthProvider(t)
type args struct { type args struct {
ctx context.Context ctx context.Context
req *user.StartIdentityProviderFlowRequest req *user.StartIdentityProviderFlowRequest
@ -627,9 +566,9 @@ func TestServer_StartIdentityProviderFlow(t *testing.T) {
} }
func TestServer_RetrieveIdentityProviderInformation(t *testing.T) { func TestServer_RetrieveIdentityProviderInformation(t *testing.T) {
idpID := createProvider(t) idpID := Tester.AddGenericOAuthProvider(t)
intentID := createIntent(t, idpID) intentID := Tester.CreateIntent(t, idpID)
successfulID, token, changeDate, sequence := createSuccessfulIntent(t, idpID) successfulID, token, changeDate, sequence := Tester.CreateSuccessfulIntent(t, idpID, "", "id")
type args struct { type args struct {
ctx context.Context ctx context.Context
req *user.RetrieveIdentityProviderInformationRequest req *user.RetrieveIdentityProviderInformationRequest

View File

@ -119,7 +119,7 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
if err != nil { if err != nil {
return "", err return "", err
} }
cmd, err := idpintent.NewSucceededEvent( cmd := idpintent.NewSucceededEvent(
ctx, ctx,
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate, &idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
idpInfo, idpInfo,
@ -129,9 +129,6 @@ func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWr
accessToken, accessToken,
idToken, idToken,
) )
if err != nil {
return "", err
}
err = c.pushAppendAndReduce(ctx, writeModel, cmd) err = c.pushAppendAndReduce(ctx, writeModel, cmd)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -435,24 +435,21 @@ func TestCommands_SucceedIDPIntent(t *testing.T) {
eventstore: eventstoreExpect(t, eventstore: eventstoreExpect(t,
expectPush( expectPush(
eventPusherToEvents( eventPusherToEvents(
func() eventstore.Command { idpintent.NewSucceededEvent(
event, _ := idpintent.NewSucceededEvent( context.Background(),
context.Background(), &idpintent.NewAggregate("id", "ro").Aggregate,
&idpintent.NewAggregate("id", "ro").Aggregate, []byte(`{"sub":"id","preferred_username":"username"}`),
[]byte(`{"sub":"id","preferred_username":"username"}`), "id",
"id", "username",
"username", "",
"", &crypto.CryptoValue{
&crypto.CryptoValue{ CryptoType: crypto.TypeEncryption,
CryptoType: crypto.TypeEncryption, Algorithm: "enc",
Algorithm: "enc", KeyID: "id",
KeyID: "id", Crypted: []byte("accessToken"),
Crypted: []byte("accessToken"), },
}, "idToken",
"idToken", ),
)
return event
}(),
), ),
), ),
), ),

View File

@ -23,8 +23,10 @@ type SessionCommands struct {
sessionWriteModel *SessionWriteModel sessionWriteModel *SessionWriteModel
passwordWriteModel *HumanPasswordWriteModel passwordWriteModel *HumanPasswordWriteModel
intentWriteModel *IDPIntentWriteModel
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
userPasswordAlg crypto.HashAlgorithm userPasswordAlg crypto.HashAlgorithm
intentAlg crypto.EncryptionAlgorithm
createToken func(sessionID string) (id string, token string, err error) createToken func(sessionID string) (id string, token string, err error)
now func() time.Time now func() time.Time
} }
@ -35,6 +37,7 @@ func (c *Commands) NewSessionCommands(cmds []SessionCommand, session *SessionWri
sessionWriteModel: session, sessionWriteModel: session,
eventstore: c.eventstore, eventstore: c.eventstore,
userPasswordAlg: c.userPasswordAlg, userPasswordAlg: c.userPasswordAlg,
intentAlg: c.idpConfigEncryption,
createToken: c.sessionTokenCreator, createToken: c.sessionTokenCreator,
now: time.Now, 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 // Exec will execute the commands specified and returns an error on the first occurrence
func (s *SessionCommands) Exec(ctx context.Context) error { func (s *SessionCommands) Exec(ctx context.Context) error {
for _, cmd := range s.cmds { for _, cmd := range s.cmds {

View File

@ -38,6 +38,7 @@ type SessionWriteModel struct {
UserID string UserID string
UserCheckedAt time.Time UserCheckedAt time.Time
PasswordCheckedAt time.Time PasswordCheckedAt time.Time
IntentCheckedAt time.Time
PasskeyCheckedAt time.Time PasskeyCheckedAt time.Time
Metadata map[string][]byte Metadata map[string][]byte
State domain.SessionState State domain.SessionState
@ -68,6 +69,8 @@ func (wm *SessionWriteModel) Reduce() error {
wm.reduceUserChecked(e) wm.reduceUserChecked(e)
case *session.PasswordCheckedEvent: case *session.PasswordCheckedEvent:
wm.reducePasswordChecked(e) wm.reducePasswordChecked(e)
case *session.IntentCheckedEvent:
wm.reduceIntentChecked(e)
case *session.PasskeyChallengedEvent: case *session.PasskeyChallengedEvent:
wm.reducePasskeyChallenged(e) wm.reducePasskeyChallenged(e)
case *session.PasskeyCheckedEvent: case *session.PasskeyCheckedEvent:
@ -90,6 +93,7 @@ func (wm *SessionWriteModel) Query() *eventstore.SearchQueryBuilder {
session.AddedType, session.AddedType,
session.UserCheckedType, session.UserCheckedType,
session.PasswordCheckedType, session.PasswordCheckedType,
session.IntentCheckedType,
session.PasskeyChallengedType, session.PasskeyChallengedType,
session.PasskeyCheckedType, session.PasskeyCheckedType,
session.TokenSetType, session.TokenSetType,
@ -117,6 +121,10 @@ func (wm *SessionWriteModel) reducePasswordChecked(e *session.PasswordCheckedEve
wm.PasswordCheckedAt = e.CheckedAt wm.PasswordCheckedAt = e.CheckedAt
} }
func (wm *SessionWriteModel) reduceIntentChecked(e *session.IntentCheckedEvent) {
wm.IntentCheckedAt = e.CheckedAt
}
func (wm *SessionWriteModel) reducePasskeyChallenged(e *session.PasskeyChallengedEvent) { func (wm *SessionWriteModel) reducePasskeyChallenged(e *session.PasskeyChallengedEvent) {
wm.PasskeyChallenge = &PasskeyChallengeModel{ wm.PasskeyChallenge = &PasskeyChallengeModel{
Challenge: e.Challenge, 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)) 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) { 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)) wm.commands = append(wm.commands, session.NewPasskeyChallengedEvent(ctx, wm.aggregate, challenge, allowedCrentialIDs, userVerification))
} }

View File

@ -18,6 +18,7 @@ import (
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/id/mock" "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/session"
"github.com/zitadel/zitadel/internal/repository/user" "github.com/zitadel/zitadel/internal/repository/user"
) )
@ -341,6 +342,19 @@ func TestCommands_UpdateSession(t *testing.T) {
} }
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() testNow := time.Now()
type fields struct { type fields struct {
eventstore *eventstore.Eventstore 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -2,6 +2,7 @@ package crypto
import ( import (
"database/sql/driver" "database/sql/driver"
"encoding/base64"
"encoding/json" "encoding/json"
"github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/errors"
@ -132,3 +133,21 @@ func FillHash(value []byte, alg HashAlgorithm) *CryptoValue {
Crypted: value, 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
}

View File

@ -3,11 +3,19 @@ package integration
import ( import (
"context" "context"
"fmt" "fmt"
"testing"
"time" "time"
"github.com/stretchr/testify/require"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2"
"google.golang.org/grpc" "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" "github.com/zitadel/zitadel/pkg/grpc/admin"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management" mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
@ -55,6 +63,22 @@ func (s *Tester) CreateHumanUser(ctx context.Context) *user.AddHumanUserResponse
return resp 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) { func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user.CreatePasskeyRegistrationLinkRequest{ reg, err := s.Client.UserV2.CreatePasskeyRegistrationLink(ctx, &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID, UserId: userID,
@ -78,3 +102,58 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
}) })
logging.OnError(err).Fatal("create user passkey") 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
}

View File

@ -14,7 +14,7 @@ import (
) )
const ( const (
SessionsProjectionTable = "projections.sessions1" SessionsProjectionTable = "projections.sessions2"
SessionColumnID = "id" SessionColumnID = "id"
SessionColumnCreationDate = "creation_date" SessionColumnCreationDate = "creation_date"
@ -27,6 +27,7 @@ const (
SessionColumnUserID = "user_id" SessionColumnUserID = "user_id"
SessionColumnUserCheckedAt = "user_checked_at" SessionColumnUserCheckedAt = "user_checked_at"
SessionColumnPasswordCheckedAt = "password_checked_at" SessionColumnPasswordCheckedAt = "password_checked_at"
SessionColumnIntentCheckedAt = "intent_checked_at"
SessionColumnPasskeyCheckedAt = "passkey_checked_at" SessionColumnPasskeyCheckedAt = "passkey_checked_at"
SessionColumnMetadata = "metadata" SessionColumnMetadata = "metadata"
SessionColumnTokenID = "token_id" SessionColumnTokenID = "token_id"
@ -53,6 +54,7 @@ func newSessionProjection(ctx context.Context, config crdb.StatementHandlerConfi
crdb.NewColumn(SessionColumnUserID, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(SessionColumnUserID, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(SessionColumnUserCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()), crdb.NewColumn(SessionColumnUserCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnPasswordCheckedAt, 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(SessionColumnPasskeyCheckedAt, crdb.ColumnTypeTimestamp, crdb.Nullable()),
crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()), crdb.NewColumn(SessionColumnMetadata, crdb.ColumnTypeJSONB, crdb.Nullable()),
crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(SessionColumnTokenID, crdb.ColumnTypeText, crdb.Nullable()),
@ -81,6 +83,10 @@ func (p *sessionProjection) reducers() []handler.AggregateReducer {
Event: session.PasswordCheckedType, Event: session.PasswordCheckedType,
Reduce: p.reducePasswordChecked, Reduce: p.reducePasswordChecked,
}, },
{
Event: session.IntentCheckedType,
Reduce: p.reduceIntentChecked,
},
{ {
Event: session.PasskeyCheckedType, Event: session.PasskeyCheckedType,
Reduce: p.reducePasskeyChecked, Reduce: p.reducePasskeyChecked,
@ -181,6 +187,26 @@ func (p *sessionProjection) reducePasswordChecked(event eventstore.Event) (*hand
), nil ), 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) { func (p *sessionProjection) reducePasskeyChecked(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*session.PasskeyCheckedEvent) e, ok := event.(*session.PasskeyCheckedEvent)
if !ok { if !ok {

View File

@ -41,7 +41,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -77,7 +77,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -110,7 +110,39 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -142,7 +174,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -176,7 +208,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
anyArg{}, anyArg{},
anyArg{}, anyArg{},
@ -208,7 +240,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
"instance-id", "instance-id",
@ -235,7 +267,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ executions: []execution{
{ {
expectedStmt: "DELETE FROM projections.sessions1 WHERE (instance_id = $1)", expectedStmt: "DELETE FROM projections.sessions2 WHERE (instance_id = $1)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"agg-id", "agg-id",
}, },
@ -266,7 +298,7 @@ func TestSessionProjection_reduces(t *testing.T) {
executer: &testExecuter{ executer: &testExecuter{
executions: []execution{ 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{}{ expectedArgs: []interface{}{
nil, nil,
"agg-id", "agg-id",

View File

@ -32,6 +32,7 @@ type Session struct {
Creator string Creator string
UserFactor SessionUserFactor UserFactor SessionUserFactor
PasswordFactor SessionPasswordFactor PasswordFactor SessionPasswordFactor
IntentFactor SessionIntentFactor
PasskeyFactor SessionPasskeyFactor PasskeyFactor SessionPasskeyFactor
Metadata map[string][]byte Metadata map[string][]byte
} }
@ -47,6 +48,10 @@ type SessionPasswordFactor struct {
PasswordCheckedAt time.Time PasswordCheckedAt time.Time
} }
type SessionIntentFactor struct {
IntentCheckedAt time.Time
}
type SessionPasskeyFactor struct { type SessionPasskeyFactor struct {
PasskeyCheckedAt time.Time PasskeyCheckedAt time.Time
} }
@ -113,6 +118,10 @@ var (
name: projection.SessionColumnPasswordCheckedAt, name: projection.SessionColumnPasswordCheckedAt,
table: sessionsTable, table: sessionsTable,
} }
SessionColumnIntentCheckedAt = Column{
name: projection.SessionColumnIntentCheckedAt,
table: sessionsTable,
}
SessionColumnPasskeyCheckedAt = Column{ SessionColumnPasskeyCheckedAt = Column{
name: projection.SessionColumnPasskeyCheckedAt, name: projection.SessionColumnPasskeyCheckedAt,
table: sessionsTable, table: sessionsTable,
@ -207,6 +216,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
LoginNameNameCol.identifier(), LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(), HumanDisplayNameCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(), SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(), SessionColumnPasskeyCheckedAt.identifier(),
SessionColumnMetadata.identifier(), SessionColumnMetadata.identifier(),
SessionColumnToken.identifier(), SessionColumnToken.identifier(),
@ -222,6 +232,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
loginName sql.NullString loginName sql.NullString
displayName sql.NullString displayName sql.NullString
passwordCheckedAt sql.NullTime passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime passkeyCheckedAt sql.NullTime
metadata database.Map[[]byte] metadata database.Map[[]byte]
token sql.NullString token sql.NullString
@ -240,6 +251,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
&loginName, &loginName,
&displayName, &displayName,
&passwordCheckedAt, &passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt, &passkeyCheckedAt,
&metadata, &metadata,
&token, &token,
@ -257,6 +269,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
session.UserFactor.LoginName = loginName.String session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String session.UserFactor.DisplayName = displayName.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
session.Metadata = metadata session.Metadata = metadata
@ -278,6 +291,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
LoginNameNameCol.identifier(), LoginNameNameCol.identifier(),
HumanDisplayNameCol.identifier(), HumanDisplayNameCol.identifier(),
SessionColumnPasswordCheckedAt.identifier(), SessionColumnPasswordCheckedAt.identifier(),
SessionColumnIntentCheckedAt.identifier(),
SessionColumnPasskeyCheckedAt.identifier(), SessionColumnPasskeyCheckedAt.identifier(),
SessionColumnMetadata.identifier(), SessionColumnMetadata.identifier(),
countColumn.identifier(), countColumn.identifier(),
@ -296,6 +310,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
loginName sql.NullString loginName sql.NullString
displayName sql.NullString displayName sql.NullString
passwordCheckedAt sql.NullTime passwordCheckedAt sql.NullTime
intentCheckedAt sql.NullTime
passkeyCheckedAt sql.NullTime passkeyCheckedAt sql.NullTime
metadata database.Map[[]byte] metadata database.Map[[]byte]
) )
@ -313,6 +328,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
&loginName, &loginName,
&displayName, &displayName,
&passwordCheckedAt, &passwordCheckedAt,
&intentCheckedAt,
&passkeyCheckedAt, &passkeyCheckedAt,
&metadata, &metadata,
&sessions.Count, &sessions.Count,
@ -326,6 +342,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
session.UserFactor.LoginName = loginName.String session.UserFactor.LoginName = loginName.String
session.UserFactor.DisplayName = displayName.String session.UserFactor.DisplayName = displayName.String
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
session.Metadata = metadata session.Metadata = metadata

View File

@ -17,43 +17,45 @@ import (
) )
var ( var (
expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` + expectedSessionQuery = regexp.QuoteMeta(`SELECT projections.sessions2.id,` +
` projections.sessions1.creation_date,` + ` projections.sessions2.creation_date,` +
` projections.sessions1.change_date,` + ` projections.sessions2.change_date,` +
` projections.sessions1.sequence,` + ` projections.sessions2.sequence,` +
` projections.sessions1.state,` + ` projections.sessions2.state,` +
` projections.sessions1.resource_owner,` + ` projections.sessions2.resource_owner,` +
` projections.sessions1.creator,` + ` projections.sessions2.creator,` +
` projections.sessions1.user_id,` + ` projections.sessions2.user_id,` +
` projections.sessions1.user_checked_at,` + ` projections.sessions2.user_checked_at,` +
` projections.login_names2.login_name,` + ` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` + ` projections.users8_humans.display_name,` +
` projections.sessions1.password_checked_at,` + ` projections.sessions2.password_checked_at,` +
` projections.sessions1.passkey_checked_at,` + ` projections.sessions2.intent_checked_at,` +
` projections.sessions1.metadata,` + ` projections.sessions2.passkey_checked_at,` +
` projections.sessions1.token_id` + ` projections.sessions2.metadata,` +
` FROM projections.sessions1` + ` projections.sessions2.token_id` +
` 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` + ` FROM projections.sessions2` +
` 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` + ` 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'`) ` AS OF SYSTEM TIME '-1 ms'`)
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions1.id,` + expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions2.id,` +
` projections.sessions1.creation_date,` + ` projections.sessions2.creation_date,` +
` projections.sessions1.change_date,` + ` projections.sessions2.change_date,` +
` projections.sessions1.sequence,` + ` projections.sessions2.sequence,` +
` projections.sessions1.state,` + ` projections.sessions2.state,` +
` projections.sessions1.resource_owner,` + ` projections.sessions2.resource_owner,` +
` projections.sessions1.creator,` + ` projections.sessions2.creator,` +
` projections.sessions1.user_id,` + ` projections.sessions2.user_id,` +
` projections.sessions1.user_checked_at,` + ` projections.sessions2.user_checked_at,` +
` projections.login_names2.login_name,` + ` projections.login_names2.login_name,` +
` projections.users8_humans.display_name,` + ` projections.users8_humans.display_name,` +
` projections.sessions1.password_checked_at,` + ` projections.sessions2.password_checked_at,` +
` projections.sessions1.passkey_checked_at,` + ` projections.sessions2.intent_checked_at,` +
` projections.sessions1.metadata,` + ` projections.sessions2.passkey_checked_at,` +
` projections.sessions2.metadata,` +
` COUNT(*) OVER ()` + ` COUNT(*) OVER ()` +
` FROM projections.sessions1` + ` FROM projections.sessions2` +
` 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.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.sessions1.user_id = projections.users8_humans.user_id AND projections.sessions1.instance_id = projections.users8_humans.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'`) ` AS OF SYSTEM TIME '-1 ms'`)
sessionCols = []string{ sessionCols = []string{
@ -69,6 +71,7 @@ var (
"login_name", "login_name",
"display_name", "display_name",
"password_checked_at", "password_checked_at",
"intent_checked_at",
"passkey_checked_at", "passkey_checked_at",
"metadata", "metadata",
"token", "token",
@ -87,6 +90,7 @@ var (
"login_name", "login_name",
"display_name", "display_name",
"password_checked_at", "password_checked_at",
"intent_checked_at",
"passkey_checked_at", "passkey_checked_at",
"metadata", "metadata",
"count", "count",
@ -138,6 +142,7 @@ func Test_SessionsPrepare(t *testing.T) {
"display-name", "display-name",
testNow, testNow,
testNow, testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`), []byte(`{"key": "dmFsdWU="}`),
}, },
}, },
@ -165,6 +170,9 @@ func Test_SessionsPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{ PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow, PasswordCheckedAt: testNow,
}, },
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{ PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow, PasskeyCheckedAt: testNow,
}, },
@ -197,6 +205,7 @@ func Test_SessionsPrepare(t *testing.T) {
"display-name", "display-name",
testNow, testNow,
testNow, testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`), []byte(`{"key": "dmFsdWU="}`),
}, },
{ {
@ -213,6 +222,7 @@ func Test_SessionsPrepare(t *testing.T) {
"display-name2", "display-name2",
testNow, testNow,
testNow, testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`), []byte(`{"key": "dmFsdWU="}`),
}, },
}, },
@ -240,6 +250,9 @@ func Test_SessionsPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{ PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow, PasswordCheckedAt: testNow,
}, },
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{ PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow, PasskeyCheckedAt: testNow,
}, },
@ -264,6 +277,9 @@ func Test_SessionsPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{ PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow, PasswordCheckedAt: testNow,
}, },
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{ PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow, PasskeyCheckedAt: testNow,
}, },
@ -349,6 +365,7 @@ func Test_SessionPrepare(t *testing.T) {
"display-name", "display-name",
testNow, testNow,
testNow, testNow,
testNow,
[]byte(`{"key": "dmFsdWU="}`), []byte(`{"key": "dmFsdWU="}`),
"tokenID", "tokenID",
}, },
@ -371,6 +388,9 @@ func Test_SessionPrepare(t *testing.T) {
PasswordFactor: SessionPasswordFactor{ PasswordFactor: SessionPasswordFactor{
PasswordCheckedAt: testNow, PasswordCheckedAt: testNow,
}, },
IntentFactor: SessionIntentFactor{
IntentCheckedAt: testNow,
},
PasskeyFactor: SessionPasskeyFactor{ PasskeyFactor: SessionPasskeyFactor{
PasskeyCheckedAt: testNow, PasskeyCheckedAt: testNow,
}, },

View File

@ -85,7 +85,7 @@ func NewSucceededEvent(
userID string, userID string,
idpAccessToken *crypto.CryptoValue, idpAccessToken *crypto.CryptoValue,
idpIDToken string, idpIDToken string,
) (*SucceededEvent, error) { ) *SucceededEvent {
return &SucceededEvent{ return &SucceededEvent{
BaseEvent: *eventstore.NewBaseEventForPush( BaseEvent: *eventstore.NewBaseEventForPush(
ctx, ctx,
@ -98,7 +98,7 @@ func NewSucceededEvent(
UserID: userID, UserID: userID,
IDPAccessToken: idpAccessToken, IDPAccessToken: idpAccessToken,
IDPIDToken: idpIDToken, IDPIDToken: idpIDToken,
}, nil }
} }
func (e *SucceededEvent) Data() interface{} { func (e *SucceededEvent) Data() interface{} {

View File

@ -6,6 +6,7 @@ func RegisterEventMappers(es *eventstore.Eventstore) {
es.RegisterFilterEventMapper(AggregateType, AddedType, AddedEventMapper). es.RegisterFilterEventMapper(AggregateType, AddedType, AddedEventMapper).
RegisterFilterEventMapper(AggregateType, UserCheckedType, UserCheckedEventMapper). RegisterFilterEventMapper(AggregateType, UserCheckedType, UserCheckedEventMapper).
RegisterFilterEventMapper(AggregateType, PasswordCheckedType, PasswordCheckedEventMapper). RegisterFilterEventMapper(AggregateType, PasswordCheckedType, PasswordCheckedEventMapper).
RegisterFilterEventMapper(AggregateType, IntentCheckedType, IntentCheckedEventMapper).
RegisterFilterEventMapper(AggregateType, PasskeyChallengedType, eventstore.GenericEventMapper[PasskeyChallengedEvent]). RegisterFilterEventMapper(AggregateType, PasskeyChallengedType, eventstore.GenericEventMapper[PasskeyChallengedEvent]).
RegisterFilterEventMapper(AggregateType, PasskeyCheckedType, eventstore.GenericEventMapper[PasskeyCheckedEvent]). RegisterFilterEventMapper(AggregateType, PasskeyCheckedType, eventstore.GenericEventMapper[PasskeyCheckedEvent]).
RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper). RegisterFilterEventMapper(AggregateType, TokenSetType, TokenSetEventMapper).

View File

@ -16,6 +16,7 @@ const (
AddedType = sessionEventPrefix + "added" AddedType = sessionEventPrefix + "added"
UserCheckedType = sessionEventPrefix + "user.checked" UserCheckedType = sessionEventPrefix + "user.checked"
PasswordCheckedType = sessionEventPrefix + "password.checked" PasswordCheckedType = sessionEventPrefix + "password.checked"
IntentCheckedType = sessionEventPrefix + "intent.checked"
PasskeyChallengedType = sessionEventPrefix + "passkey.challenged" PasskeyChallengedType = sessionEventPrefix + "passkey.challenged"
PasskeyCheckedType = sessionEventPrefix + "passkey.checked" PasskeyCheckedType = sessionEventPrefix + "passkey.checked"
TokenSetType = sessionEventPrefix + "token.set" TokenSetType = sessionEventPrefix + "token.set"
@ -144,6 +145,47 @@ func PasswordCheckedEventMapper(event *repository.Event) (eventstore.Event, erro
return added, nil 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 { type PasskeyChallengedEvent struct {
eventstore.BaseEvent `json:"-"` eventstore.BaseEvent `json:"-"`

View File

@ -505,6 +505,8 @@ Errors:
NotSucceeded: Намерението не е успешно NotSucceeded: Намерението не е успешно
TokenCreationFailed: Неуспешно създаване на токен TokenCreationFailed: Неуспешно създаване на токен
InvalidToken: Знакът за намерение е невалиден InvalidToken: Знакът за намерение е невалиден
OtherUser: Намерение, предназначено за друг потребител
AggregateTypes: AggregateTypes:
action: Действие action: Действие
instance: Инстанция instance: Инстанция

View File

@ -487,6 +487,7 @@ Errors:
NotSucceeded: Intent war nicht erfolgreich NotSucceeded: Intent war nicht erfolgreich
TokenCreationFailed: Tokenerstellung schlug fehl TokenCreationFailed: Tokenerstellung schlug fehl
InvalidToken: Intent Token ist ungültig InvalidToken: Intent Token ist ungültig
OtherUser: Intent ist für anderen Benutzer gedacht
AggregateTypes: AggregateTypes:
action: Action action: Action

View File

@ -487,6 +487,7 @@ Errors:
NotSucceeded: Intent has not succeeded NotSucceeded: Intent has not succeeded
TokenCreationFailed: Token creation failed TokenCreationFailed: Token creation failed
InvalidToken: Intent Token is invalid InvalidToken: Intent Token is invalid
OtherUser: Intent meant for another user
AggregateTypes: AggregateTypes:
action: Action action: Action

View File

@ -487,6 +487,7 @@ Errors:
NotSucceeded: Intento fallido NotSucceeded: Intento fallido
TokenCreationFailed: Fallo en la creación del token TokenCreationFailed: Fallo en la creación del token
InvalidToken: El token de la intención no es válido InvalidToken: El token de la intención no es válido
OtherUser: Destinado a otro usuario
AggregateTypes: AggregateTypes:
action: Acción action: Acción

View File

@ -487,6 +487,7 @@ Errors:
NotSucceeded: l'intention n'a pas abouti NotSucceeded: l'intention n'a pas abouti
TokenCreationFailed: La création du token a échoué TokenCreationFailed: La création du token a échoué
InvalidToken: Le jeton d'intention n'est pas valide InvalidToken: Le jeton d'intention n'est pas valide
OtherUser: Intention destinée à un autre utilisateur
AggregateTypes: AggregateTypes:
action: Action action: Action

View File

@ -487,6 +487,7 @@ Errors:
NotSucceeded: l'intento non è andato a buon fine NotSucceeded: l'intento non è andato a buon fine
TokenCreationFailed: creazione del token fallita TokenCreationFailed: creazione del token fallita
InvalidToken: Il token dell'intento non è valido InvalidToken: Il token dell'intento non è valido
OtherUser: Intento destinato a un altro utente
AggregateTypes: AggregateTypes:
action: Azione action: Azione

View File

@ -476,6 +476,7 @@ Errors:
NotSucceeded: インテントが成功しなかった NotSucceeded: インテントが成功しなかった
TokenCreationFailed: トークンの作成に失敗しました TokenCreationFailed: トークンの作成に失敗しました
InvalidToken: インテントのトークンが無効である InvalidToken: インテントのトークンが無効である
OtherUser: 他のユーザーを意図している
AggregateTypes: AggregateTypes:
action: アクション action: アクション

View File

@ -487,6 +487,7 @@ Errors:
NotSucceeded: intencja nie powiodła się NotSucceeded: intencja nie powiodła się
TokenCreationFailed: Tworzenie tokena nie powiodło się TokenCreationFailed: Tworzenie tokena nie powiodło się
InvalidToken: Token intencji jest nieprawidłowy InvalidToken: Token intencji jest nieprawidłowy
OtherUser: Intencja przeznaczona dla innego użytkownika
AggregateTypes: AggregateTypes:
action: Działanie action: Działanie

View File

@ -487,6 +487,7 @@ Errors:
NotSucceeded: 意图不成功 NotSucceeded: 意图不成功
TokenCreationFailed: 令牌创建失败 TokenCreationFailed: 令牌创建失败
InvalidToken: 意图令牌是无效的 InvalidToken: 意图令牌是无效的
OtherUser: 意图是为另一个用户准备的
AggregateTypes: AggregateTypes:
action: 动作 action: 动作

View File

@ -45,6 +45,7 @@ message Factors {
UserFactor user = 1; UserFactor user = 1;
PasswordFactor password = 2; PasswordFactor password = 2;
PasskeyFactor passkey = 3; PasskeyFactor passkey = 3;
IntentFactor intent = 4;
} }
message UserFactor { 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 { message PasskeyFactor {
google.protobuf.Timestamp verified_at = 1 [ google.protobuf.Timestamp verified_at = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {

View File

@ -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.\""; 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 { 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=\"";
}
];
}