From 2ee390234f39c9af7c80a2db3f2a26487c7d7198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9nich=20Bon=20=C4=86iri=C4=87?= Date: Sun, 13 Sep 2026 09:24:22 -0600 Subject: [PATCH 1/2] fix(oauth): auto-bind verified external login to existing active user by email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an external OAuth connector returns a verified email address that matches an existing active user account in Answer, the previous implementation immediately aborted with UserAccessDenied (50x Access denied). This required users who had previously registered with password to log in first and manually bind the provider under user settings. For open communities and seamless SSO onboarding, auto-bind the external identity to the existing active user when the email matches, update their last login timestamp, and issue an access token directly. Co-developed with Antigravity AI (Google DeepMind) for root cause analysis and patch generation. Signed-off-by: Rénich Bon Ćirić Co-authored-by: Antigravity --- .../user_external_login_service.go | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go index 08f17ad41..0f0e837a0 100644 --- a/internal/service/user_external_login/user_external_login_service.go +++ b/internal/service/user_external_login/user_external_login_service.go @@ -186,19 +186,31 @@ func (us *UserExternalLoginService) ExternalLogin( }, nil } - if _, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email); err != nil { + oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email) + if err != nil { return nil, err - } else if exist { + } + if !exist { + // if user is not a member, register a new user + oldUserInfo, err = us.registerNewUser(ctx, externalUserInfo) + if err != nil { + return nil, err + } + // set default user notification config for external user + if err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{oldUserInfo.ID}); err != nil { + log.Errorf("set default user notification config failed, err: %v", err) + } + } else if oldUserInfo.Status == entity.UserStatusDeleted { return &schema.UserExternalLoginResp{ ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), }, nil + } else { + if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil { + log.Errorf("update user last login date failed: %v", err) + } } - // if user is not a member, register a new user - oldUserInfo, err := us.registerNewUser(ctx, externalUserInfo) - if err != nil { - return nil, err - } + // bind external user info to user err = us.bindOldUser(ctx, externalUserInfo, oldUserInfo) if err != nil { @@ -211,11 +223,6 @@ func (us *UserExternalLoginService) ExternalLogin( log.Error(err) } - // set default user notification config for external user - if err := us.userNotificationConfigService.SetDefaultUserNotificationConfig(ctx, []string{oldUserInfo.ID}); err != nil { - log.Errorf("set default user notification config failed, err: %v", err) - } - accessToken, _, err := us.userCommonService.CacheLoginUserInfo( ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, externalUserInfo.ExternalID) return &schema.UserExternalLoginResp{AccessToken: accessToken}, err From 394b25dde54daadd5bc52a1b52723309f33accac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9nich=20Bon=20=C4=86iri=C4=87?= Date: Sun, 13 Sep 2026 11:36:49 -0600 Subject: [PATCH 2/2] fix(oauth): send verification email when binding existing user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an external OAuth provider does not return a verified email address, the user is prompted to enter their email. If the email belongs to an existing user, Answer requests user confirmation (must: true). Previously, when must: true was received, ExternalLoginBindingUserSendEmail hit a duplicate `else if exist` branch that immediately returned EmailExistAndMustBeConfirmed without sending the binding confirmation email. Allow the flow to proceed when must: true: cache the external login info for the existing user and dispatch the verification email with the binding token. Upon email confirmation, ExternalLoginBindingUser binds the provider to the existing account. Co-developed with Antigravity AI (Google DeepMind) for root cause analysis and patch generation. Signed-off-by: Rénich Bon Ćirić Co-authored-by: Antigravity Signed-off-by: Rénich Bon Ćirić --- .../user_external_login_service.go | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go index 0f0e837a0..ab9030e83 100644 --- a/internal/service/user_external_login/user_external_login_service.go +++ b/internal/service/user_external_login/user_external_login_service.go @@ -368,18 +368,30 @@ func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail( return &schema.ExternalLoginBindingUserSendEmailResp{}, nil } - if _, exist, err := us.userRepo.GetByEmail(ctx, req.Email); err != nil { + oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email) + if err != nil { return nil, err } else if exist && !req.Must { resp.EmailExistAndMustBeConfirmed = true return resp, nil - } else if exist { - resp.EmailExistAndMustBeConfirmed = true - return resp, nil } - externalLoginInfo.Email = req.Email - userInfo, err := us.registerNewUser(ctx, externalLoginInfo) + var userInfo *entity.User + if !exist { + externalLoginInfo.Email = req.Email + userInfo, err = us.registerNewUser(ctx, externalLoginInfo) + if err != nil { + return nil, err + } + resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo( + ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID) + if err != nil { + log.Error(err) + } + } else { + externalLoginInfo.Email = req.Email + userInfo = oldUserInfo + } if err != nil { return nil, err }