fix(auth): require explicit choice for third-party signup

This commit is contained in:
IanShaw027
2026-04-21 20:36:58 +08:00
parent 2cebb0dc60
commit 4c21320d1b
8 changed files with 638 additions and 296 deletions

View File

@@ -420,27 +420,6 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
redirectOAuthError(c, frontendCallback, "session_error", infraerrors.Reason(err), infraerrors.Message(err))
return
}
if compatEmailUser != nil {
if err := h.createOAuthPendingSession(c, oauthPendingSessionPayload{
Intent: "adopt_existing_user_by_email",
Identity: identityRef,
TargetUserID: &compatEmailUser.ID,
ResolvedEmail: compatEmailUser.Email,
RedirectTo: redirectTo,
BrowserSessionKey: browserSessionKey,
UpstreamIdentityClaims: upstreamClaims,
CompletionResponse: map[string]any{
"redirect": redirectTo,
"step": "bind_login_required",
"email": compatEmailUser.Email,
},
}); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
redirectToFrontendCallback(c, frontendCallback)
return
}
if cfg.RequireEmailVerified {
if emailVerified == nil || !*emailVerified {
@@ -450,7 +429,18 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
}
if h.isForceEmailOnThirdPartySignup(c.Request.Context()) {
if err := h.createOAuthEmailRequiredPendingSession(c, identityRef, redirectTo, browserSessionKey, upstreamClaims); err != nil {
if err := h.createOIDCOAuthChoicePendingSession(
c,
identityRef,
email,
email,
redirectTo,
browserSessionKey,
upstreamClaims,
compatEmail,
compatEmailUser,
true,
); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
@@ -458,48 +448,18 @@ func (h *AuthHandler) OIDCOAuthCallback(c *gin.Context) {
return
}
// 传入空邀请码;如果需要邀请码,服务层返回 ErrOAuthInvitationRequired
tokenPair, user, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, "")
if err != nil {
if errors.Is(err, service.ErrOAuthInvitationRequired) {
if err := h.createOAuthPendingSession(c, oauthPendingSessionPayload{
Intent: "login",
Identity: identityRef,
ResolvedEmail: email,
RedirectTo: redirectTo,
BrowserSessionKey: browserSessionKey,
UpstreamIdentityClaims: upstreamClaims,
CompletionResponse: map[string]any{
"error": "invitation_required",
"redirect": redirectTo,
},
}); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
redirectToFrontendCallback(c, frontendCallback)
return
}
redirectOAuthError(c, frontendCallback, "login_failed", infraerrors.Reason(err), infraerrors.Message(err))
return
}
if err := h.createOAuthPendingSession(c, oauthPendingSessionPayload{
Intent: "login",
Identity: identityRef,
TargetUserID: &user.ID,
ResolvedEmail: email,
RedirectTo: redirectTo,
BrowserSessionKey: browserSessionKey,
UpstreamIdentityClaims: upstreamClaims,
CompletionResponse: map[string]any{
"access_token": tokenPair.AccessToken,
"refresh_token": tokenPair.RefreshToken,
"expires_in": tokenPair.ExpiresIn,
"token_type": "Bearer",
"redirect": redirectTo,
},
}); err != nil {
if err := h.createOIDCOAuthChoicePendingSession(
c,
identityRef,
email,
email,
redirectTo,
browserSessionKey,
upstreamClaims,
compatEmail,
compatEmailUser,
h.isForceEmailOnThirdPartySignup(c.Request.Context()),
); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
@@ -530,6 +490,65 @@ func (h *AuthHandler) findOIDCCompatEmailUser(ctx context.Context, email string)
return userEntity, nil
}
func (h *AuthHandler) createOIDCOAuthChoicePendingSession(
c *gin.Context,
identity service.PendingAuthIdentityKey,
suggestedEmail string,
resolvedEmail string,
redirectTo string,
browserSessionKey string,
upstreamClaims map[string]any,
compatEmail string,
compatEmailUser *dbent.User,
forceEmailOnSignup bool,
) error {
suggestionEmail := strings.TrimSpace(suggestedEmail)
canonicalEmail := strings.TrimSpace(resolvedEmail)
if suggestionEmail == "" {
suggestionEmail = canonicalEmail
}
completionResponse := map[string]any{
"step": oauthPendingChoiceStep,
"adoption_required": true,
"redirect": strings.TrimSpace(redirectTo),
"email": suggestionEmail,
"resolved_email": canonicalEmail,
"existing_account_email": "",
"existing_account_bindable": false,
"create_account_allowed": true,
"force_email_on_signup": forceEmailOnSignup,
"choice_reason": "third_party_signup",
}
if strings.TrimSpace(compatEmail) != "" {
completionResponse["compat_email"] = strings.TrimSpace(compatEmail)
}
if compatEmailUser != nil {
completionResponse["email"] = strings.TrimSpace(compatEmailUser.Email)
completionResponse["existing_account_email"] = strings.TrimSpace(compatEmailUser.Email)
completionResponse["existing_account_bindable"] = true
completionResponse["choice_reason"] = "compat_email_match"
}
if forceEmailOnSignup && compatEmailUser == nil {
completionResponse["choice_reason"] = "force_email_on_signup"
}
resolvedChoiceEmail := suggestionEmail
if compatEmailUser != nil {
resolvedChoiceEmail = strings.TrimSpace(compatEmailUser.Email)
}
return h.createOAuthPendingSession(c, oauthPendingSessionPayload{
Intent: oauthIntentLogin,
Identity: identity,
ResolvedEmail: resolvedChoiceEmail,
RedirectTo: redirectTo,
BrowserSessionKey: browserSessionKey,
UpstreamIdentityClaims: upstreamClaims,
CompletionResponse: completionResponse,
})
}
type completeOIDCOAuthRequest struct {
InvitationCode string `json:"invitation_code" binding:"required"`
AdoptDisplayName *bool `json:"adopt_display_name,omitempty"`