Skip to content

Commit d36172b

Browse files
committed
feat: allow user.created without hash with external OIDC
1 parent 59c802a commit d36172b

2 files changed

Lines changed: 128 additions & 33 deletions

File tree

pkg/rabbitmq/handlers.go

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -173,51 +173,55 @@ func (h *UserCreatedHandler) Handle(ctx context.Context, d amqp.Delivery) error
173173
log.Debugf("user.created: no domain provided, using default: %s", DefaultDomain)
174174
msg.Domain = DefaultDomain
175175
}
176-
if msg.Hash == "" {
177-
return fmt.Errorf("user.created: missing passphrase hash")
178-
}
179176
if msg.WorkplaceFqdn == "" {
180177
return fmt.Errorf("user.created: missing workplaceFqdn")
181178
}
182-
if msg.Iterations <= 0 {
183-
return fmt.Errorf("user.created: missing iterations")
184-
}
185-
log.Debugf("user.created: message validation passed for TwakeID: %s", msg.TwakeID)
186179

187-
decoded, err := decodePassword(msg.Hash)
180+
log.Debugf("user.created: looking for instance for domain: %s", msg.WorkplaceFqdn)
181+
inst, err := lifecycle.GetInstance(msg.WorkplaceFqdn)
188182
if err != nil {
189-
return err
183+
return fmt.Errorf("user.created: get instance: %w", err)
190184
}
185+
log.Debugf("user.created: message validation passed for TwakeID: %s", msg.TwakeID)
191186

192-
params := lifecycle.PassParameters{
193-
Pass: decoded,
194-
Iterations: msg.Iterations,
195-
}
187+
if msg.Hash == "" {
188+
if !inst.HasForcedOIDC() {
189+
return fmt.Errorf("user.created: missing passphrase hash")
190+
}
191+
log.Infof("user.created: skipping passphrase update for instance %s (forced OIDC context: %s)", inst.Domain, inst.ContextName)
192+
} else {
193+
if msg.Iterations <= 0 {
194+
return fmt.Errorf("user.created: missing iterations")
195+
}
196+
decoded, err := decodePassword(msg.Hash)
197+
if err != nil {
198+
return err
199+
}
196200

197-
if msg.Key != "" {
198-
log.Debugf("user.created: setting key parameter for TwakeID: %s", msg.TwakeID)
199-
params.Key = msg.Key
200-
}
201+
params := lifecycle.PassParameters{
202+
Pass: decoded,
203+
Iterations: msg.Iterations,
204+
}
201205

202-
// if one of the keys is missing, do not update any of the keys
203-
if msg.PublicKey != "" && msg.PrivateKey != "" {
204-
log.Debugf("user.created: setting public/private key parameters for TwakeID: %s", msg.TwakeID)
205-
params.PublicKey = msg.PublicKey
206-
params.PrivateKey = msg.PrivateKey
207-
} else {
208-
log.Debugf("user.created: skipping key parameters (incomplete pair) for TwakeID: %s", msg.TwakeID)
209-
}
206+
if msg.Key != "" {
207+
log.Debugf("user.created: setting key parameter for TwakeID: %s", msg.TwakeID)
208+
params.Key = msg.Key
209+
}
210210

211-
log.Debugf("user.created: looking for instance for domain: %s", msg.WorkplaceFqdn)
212-
inst, err := lifecycle.GetInstance(msg.WorkplaceFqdn)
213-
if err != nil {
214-
return fmt.Errorf("user.created: get instance: %w", err)
215-
}
211+
// if one of the keys is missing, do not update any of the keys
212+
if msg.PublicKey != "" && msg.PrivateKey != "" {
213+
log.Debugf("user.created: setting public/private key parameters for TwakeID: %s", msg.TwakeID)
214+
params.PublicKey = msg.PublicKey
215+
params.PrivateKey = msg.PrivateKey
216+
} else {
217+
log.Debugf("user.created: skipping key parameters (incomplete pair) for TwakeID: %s", msg.TwakeID)
218+
}
216219

217-
if err := lifecycle.ForceUpdatePassphraseWithSHash(inst, params.Pass, params); err != nil {
218-
return fmt.Errorf("user.created: update passphrase: %w", err)
220+
if err := lifecycle.ForceUpdatePassphraseWithSHash(inst, params.Pass, params); err != nil {
221+
return fmt.Errorf("user.created: update passphrase: %w", err)
222+
}
223+
log.Infof("user.created: successfully updated passphrase for instance: %s (PasswordDefined: %v)", inst.Domain, inst.PasswordDefined)
219224
}
220-
log.Infof("user.created: successfully updated passphrase for instance: %s (PasswordDefined: %v)", inst.Domain, inst.PasswordDefined)
221225

222226
if msg.OrganizationDomain != "" {
223227
if err := SyncCreatedOrgContact(ctx, inst, msg); err != nil {

pkg/rabbitmq/rabbitmq_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,97 @@ func TestHandlers(t *testing.T) {
337337
require.Equal(t, prevPriv, bw.PrivateKey)
338338
})
339339

340+
t.Run("CreateUserWithoutHashInForcedOIDCContext", func(t *testing.T) {
341+
setup := setUpRabbitMQConfig(t, MQ, "CreateUserWithoutHashInForcedOIDCContext")
342+
cfg := config.GetConfig()
343+
prevAuthentication := cfg.Authentication
344+
const oidcContext = "oidc-no-password-context"
345+
cfg.Authentication = map[string]interface{}{
346+
oidcContext: map[string]interface{}{
347+
"disable_password_authentication": true,
348+
},
349+
}
350+
t.Cleanup(func() {
351+
cfg.Authentication = prevAuthentication
352+
})
353+
354+
suffix := fmt.Sprintf("%d", time.Now().UnixNano())
355+
orgDomain := "no-hash-org-" + suffix + ".example"
356+
orgID := "org-no-hash-" + suffix
357+
targetEmail := "target-" + suffix + "@example.com"
358+
target := setup.GetTestInstance(&lifecycle.Options{
359+
Domain: "no-hash-target-" + suffix + ".local",
360+
ContextName: oidcContext,
361+
OrgDomain: orgDomain,
362+
OrgID: orgID,
363+
Email: targetEmail,
364+
PublicName: "Target User",
365+
})
366+
other := createInstanceInOrg(
367+
t,
368+
"no-hash-other-"+suffix+".local",
369+
orgDomain,
370+
orgID,
371+
"other-"+suffix+"@example.com",
372+
"Other User",
373+
)
374+
375+
initialHash := string(target.PassphraseHash)
376+
require.NotEmpty(t, initialHash)
377+
require.NotNil(t, target.PasswordDefined)
378+
require.False(t, *target.PasswordDefined)
379+
380+
ch, err := getChannel(t, MQ)
381+
require.NoError(t, err)
382+
383+
slug, _ := SplitDomain(t, target.Domain)
384+
msg := rabbitmq.UserCreatedMessage{
385+
TwakeID: slug,
386+
Mobile: "+33700000000",
387+
InternalEmail: targetEmail,
388+
Timestamp: time.Now().Unix(),
389+
WorkplaceFqdn: target.Domain,
390+
OrganizationID: orgID,
391+
OrganizationDomain: orgDomain,
392+
}
393+
body, err := json.Marshal(msg)
394+
require.NoError(t, err)
395+
396+
err = ch.PublishWithContext(
397+
testCtx(t),
398+
"auth",
399+
"user.created",
400+
false,
401+
false,
402+
amqp.Publishing{
403+
DeliveryMode: amqp.Persistent,
404+
ContentType: "application/json",
405+
Body: body,
406+
MessageId: fmt.Sprintf("%d", time.Now().UnixNano()),
407+
},
408+
)
409+
require.NoError(t, err)
410+
411+
testutils.WaitForOrFail(t, 10*time.Second, func() bool {
412+
matches, err := contact.FindAllByEmail(other, targetEmail)
413+
if err != nil {
414+
return false
415+
}
416+
for _, doc := range matches {
417+
if doc.IsExternal() {
418+
return true
419+
}
420+
}
421+
return false
422+
})
423+
424+
updated, err := lifecycle.GetInstance(target.Domain)
425+
require.NoError(t, err)
426+
require.Equal(t, initialHash, string(updated.PassphraseHash))
427+
require.NotNil(t, updated.PasswordDefined)
428+
require.False(t, *updated.PasswordDefined)
429+
})
430+
340431
t.Run("DeleteUserHandler", func(t *testing.T) {
341432
setup := setUpRabbitMQConfig(t, MQ, "DeleteUserHandler")
342433
_ = setup.GetTestInstance()

0 commit comments

Comments
 (0)