Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions core/authenticate/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ func (s Service) StartFlow(ctx context.Context, request RegistrationStartRequest
return nil, ErrUnsupportedMethod
}
// the gate runs before anything is sent or redirected
if err := s.gateFlowConsent(request.Intent, request.AcceptedDocumentIDs); err != nil {
consented, err := s.gateFlowConsent(request.Intent, request.AcceptedDocumentIDs)
if err != nil {
return nil, err
}
// both mail strategies know the address before anything is sent, and share
Expand All @@ -246,7 +247,7 @@ func (s Service) StartFlow(ctx context.Context, request RegistrationStartRequest
if request.Intent != FlowIntentUnspecified {
flow.Metadata[flowIntentKey] = request.Intent.String()
}
if len(request.AcceptedDocumentIDs) > 0 {
if len(consented) > 0 {
flow.Metadata[flowConsentKey] = map[string]any{
consentDocumentIDsKey: request.AcceptedDocumentIDs,
consentIPAddressKey: request.IPAddress,
Expand Down Expand Up @@ -455,21 +456,22 @@ func (s Service) gateFlowStart(ctx context.Context, intent FlowIntent, email str
// decides which rule applies, because without one a signup and a login look
// identical: a signup has to be complete here, an unset intent only has to
// name known ids, and a login checks nothing because it writes no record.
func (s Service) gateFlowConsent(intent FlowIntent, ids []string) error {
func (s Service) gateFlowConsent(intent FlowIntent, ids []string) ([]consent.Document, error) {
if s.consentService == nil || intent == FlowIntentLogin {
return nil
return nil, nil
}

var documents []consent.Document
var err error
if intent == FlowIntentSignup {
_, err = s.consentService.ResolveAll(ids)
documents, err = s.consentService.ResolveAll(ids)
} else {
_, err = s.consentService.Resolve(ids)
documents, err = s.consentService.Resolve(ids)
}
if err != nil {
return fmt.Errorf("%w: %w", ErrConsentRequired, err)
return nil, fmt.Errorf("%w: %w", ErrConsentRequired, err)
}
return nil
return documents, nil
}

// applyMailOTP actions when user submitted otp from the email
Expand Down
62 changes: 56 additions & 6 deletions core/authenticate/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1298,10 +1298,16 @@ func TestService_StartFlow_WritesIntentAndConsent(t *testing.T) {
storedFlow = flow
}).Return(nil)

enabled := consent.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)),
consent.Config{Enabled: true, Documents: map[string]consent.DocumentConfig{
"terms_of_service": {Title: "Terms & Conditions", Version: "v2", URL: "https://example.org/t"},
"privacy_policy": {Title: "Privacy Policy", Version: "v1", URL: "https://example.org/p"},
}}, nil)

srv := authenticate.NewService(nil, authenticate.Config{
MailOTP: authenticate.MailOTPConfig{Validity: 10 * time.Minute},
TestUsers: testusers.Config{Enabled: true, OTP: "111111", Domain: "example.com"},
}, mockFlowRepo, mailer.NewMockDialer(), nil, nil, mockUserService, nil, nil, nil, nil)
}, mockFlowRepo, mailer.NewMockDialer(), nil, nil, mockUserService, nil, nil, nil, enabled)
srv.Now = func() time.Time { return timeNow }

_, err := srv.StartFlow(ctx, request)
Expand Down Expand Up @@ -1453,25 +1459,35 @@ func TestService_StartFlow_Consent(t *testing.T) {
assert.ErrorIs(t, err, consent.ErrUnknownDocuments)
})

t.Run("a login checks nothing, because it writes no record", func(t *testing.T) {
t.Run("a login checks nothing, and persists nothing, because it writes no record", func(t *testing.T) {
mockConsent := mocks.NewConsentService(t)

mockFlowRepo, mockUserService, _, _, _ := createMocks(t)
ctx := context.Background()
mockUserService.EXPECT().GetByID(ctx, email).Return(user.User{ID: "user-id", Email: email}, nil)
mockFlowRepo.EXPECT().Set(ctx, mock.Anything).Return(nil)

var storedFlow *authenticate.Flow
mockFlowRepo.EXPECT().Set(ctx, mock.Anything).Run(func(_ context.Context, flow *authenticate.Flow) {
storedFlow = flow
}).Return(nil)

srv := authenticate.NewService(nil, authenticate.Config{
MailOTP: authenticate.MailOTPConfig{Validity: 10 * time.Minute},
TestUsers: testusers.Config{Enabled: true, OTP: "111111", Domain: "example.com"},
}, mockFlowRepo, mailer.NewMockDialer(), nil, nil, mockUserService, nil, nil, nil, mockConsent)

_, err := srv.StartFlow(ctx, authenticate.RegistrationStartRequest{
Method: authenticate.MailOTPAuthMethod.String(),
Email: email,
Intent: authenticate.FlowIntentLogin,
Method: authenticate.MailOTPAuthMethod.String(),
Email: email,
Intent: authenticate.FlowIntentLogin,
AcceptedDocumentIDs: acceptedIDs,
IPAddress: "10.0.0.1",
})
require.NoError(t, err)
require.NotNil(t, storedFlow)

_, ok := storedFlow.Consent()
assert.False(t, ok, "a login must persist no consent block")
})

t.Run("a deployment with consent disabled ignores the ids rather than rejecting them", func(t *testing.T) {
Expand All @@ -1487,6 +1503,40 @@ func TestService_StartFlow_Consent(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, got)
})

t.Run("a deployment with consent disabled persists no consent onto the flow", func(t *testing.T) {
disabled := consent.NewService(slog.New(slog.NewTextHandler(io.Discard, nil)),
consent.Config{Enabled: false}, nil)

ctx := context.Background()
mockFlowRepo, mockUserService, _, _, _ := createMocks(t)
mockUserService.EXPECT().GetByID(ctx, email).
Return(user.User{}, errors.New("user not found")).Maybe()

var storedFlow *authenticate.Flow
mockFlowRepo.EXPECT().Set(ctx, mock.Anything).Run(func(_ context.Context, flow *authenticate.Flow) {
storedFlow = flow
}).Return(nil)

srv := authenticate.NewService(nil, authenticate.Config{
MailOTP: authenticate.MailOTPConfig{Validity: 10 * time.Minute},
TestUsers: testusers.Config{Enabled: true, OTP: "111111", Domain: "example.com"},
}, mockFlowRepo, mailer.NewMockDialer(), nil, nil, mockUserService, nil, nil, nil, disabled)

_, err := srv.StartFlow(ctx, authenticate.RegistrationStartRequest{
Method: authenticate.MailOTPAuthMethod.String(),
Email: email,
Intent: authenticate.FlowIntentSignup,
AcceptedDocumentIDs: acceptedIDs,
IPAddress: "10.0.0.1",
})
require.NoError(t, err)
require.NotNil(t, storedFlow)

_, ok := storedFlow.Consent()
assert.False(t, ok, "a disabled deployment must persist no consent block")
assert.Equal(t, authenticate.FlowIntentSignup, storedFlow.Intent())
})
}

// TestFlow_IntentAndConsent covers the accessors directly: the JSON round trip
Expand Down
Loading