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

@@ -325,80 +325,18 @@ func (h *AuthHandler) LinuxDoOAuthCallback(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: identityKey,
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 h.isForceEmailOnThirdPartySignup(c.Request.Context()) {
if err := h.createOAuthEmailRequiredPendingSession(c, identityKey, redirectTo, browserSessionKey, upstreamClaims); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
redirectToFrontendCallback(c, frontendCallback)
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: identityKey,
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: identityKey,
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.createLinuxDoOAuthChoicePendingSession(
c,
identityKey,
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
}
@@ -431,6 +369,62 @@ func (h *AuthHandler) findLinuxDoCompatEmailUser(ctx context.Context, email stri
return userEntity, nil
}
func (h *AuthHandler) createLinuxDoOAuthChoicePendingSession(
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)
}
resolvedChoiceEmail := suggestionEmail
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"
resolvedChoiceEmail = strings.TrimSpace(compatEmailUser.Email)
}
if forceEmailOnSignup && compatEmailUser == nil {
completionResponse["choice_reason"] = "force_email_on_signup"
}
return h.createOAuthPendingSession(c, oauthPendingSessionPayload{
Intent: oauthIntentLogin,
Identity: identity,
ResolvedEmail: resolvedChoiceEmail,
RedirectTo: redirectTo,
BrowserSessionKey: browserSessionKey,
UpstreamIdentityClaims: upstreamClaims,
CompletionResponse: completionResponse,
})
}
type completeLinuxDoOAuthRequest struct {
InvitationCode string `json:"invitation_code" binding:"required"`
AdoptDisplayName *bool `json:"adopt_display_name,omitempty"`

View File

@@ -32,6 +32,7 @@ const (
oauthPendingSessionCookiePath = "/api/v1/auth/oauth"
oauthPendingSessionCookieName = "oauth_pending_session"
oauthPendingCookieMaxAgeSec = 10 * 60
oauthPendingChoiceStep = "choose_account_action_required"
oauthCompletionResponseKey = "completion_response"
)
@@ -431,8 +432,9 @@ func (h *AuthHandler) createOAuthEmailRequiredPendingSession(
BrowserSessionKey: browserSessionKey,
UpstreamIdentityClaims: upstreamClaims,
CompletionResponse: map[string]any{
"redirect": redirectTo,
"step": "email_required",
"redirect": strings.TrimSpace(redirectTo),
"step": oauthPendingChoiceStep,
"adoption_required": true,
"force_email_on_signup": true,
"email_binding_required": true,
"existing_account_bindable": true,
@@ -492,7 +494,7 @@ func (h *AuthHandler) SendPendingOAuthVerifyCode(c *gin.Context) {
email := strings.TrimSpace(strings.ToLower(req.Email))
if existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email); err == nil && existingUser != nil {
session, err = h.transitionPendingOAuthAccountToBindLogin(c, client, session, email, oauthAdoptionDecisionRequest{})
session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, email)
if err != nil {
response.ErrorFrom(c, err)
return
@@ -1206,12 +1208,13 @@ func readPendingOAuthBrowserSession(c *gin.Context, h *AuthHandler) (*service.Au
}
func buildPendingOAuthSessionStatusPayload(session *dbent.PendingAuthSession) gin.H {
completionResponse := normalizePendingOAuthCompletionResponse(mergePendingCompletionResponse(session, nil))
payload := gin.H{
"auth_result": "pending_session",
"provider": strings.TrimSpace(session.ProviderType),
"intent": strings.TrimSpace(session.Intent),
}
for key, value := range mergePendingCompletionResponse(session, nil) {
for key, value := range completionResponse {
payload[key] = value
}
if email := strings.TrimSpace(session.ResolvedEmail); email != "" {
@@ -1220,38 +1223,58 @@ func buildPendingOAuthSessionStatusPayload(session *dbent.PendingAuthSession) gi
return payload
}
func (h *AuthHandler) transitionPendingOAuthAccountToBindLogin(
func normalizePendingOAuthCompletionResponse(payload map[string]any) map[string]any {
normalized := clonePendingMap(payload)
step := strings.ToLower(strings.TrimSpace(pendingSessionStringValue(normalized, "step")))
switch step {
case "choice", "choose_account_action", "choose_account", "choose", "email_required", "bind_login_required":
normalized["step"] = oauthPendingChoiceStep
}
if strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(normalized, "step")), oauthPendingChoiceStep) {
normalized["adoption_required"] = true
}
if _, exists := normalized["adoption_required"]; !exists {
if _, hasChoiceFields := normalized["email_binding_required"]; hasChoiceFields {
normalized["adoption_required"] = true
}
}
return normalized
}
func pendingOAuthChoiceCompletionResponse(session *dbent.PendingAuthSession, email string) map[string]any {
response := mergePendingCompletionResponse(session, map[string]any{
"step": oauthPendingChoiceStep,
"adoption_required": true,
"force_email_on_signup": true,
"email_binding_required": true,
"existing_account_bindable": true,
})
if email = strings.TrimSpace(email); email != "" {
response["email"] = email
response["resolved_email"] = email
}
return response
}
func (h *AuthHandler) transitionPendingOAuthAccountToChoiceState(
c *gin.Context,
client *dbent.Client,
session *dbent.PendingAuthSession,
email string,
decision oauthAdoptionDecisionRequest,
) (*dbent.PendingAuthSession, error) {
existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email)
if err != nil {
return nil, err
}
completionResponse := mergePendingCompletionResponse(session, map[string]any{
"step": "bind_login_required",
"email": email,
})
session, err = updatePendingOAuthSessionProgress(
completionResponse := pendingOAuthChoiceCompletionResponse(session, email)
session, err := updatePendingOAuthSessionProgress(
c.Request.Context(),
client,
session,
"adopt_existing_user_by_email",
strings.TrimSpace(session.Intent),
email,
&existingUser.ID,
nil,
completionResponse,
)
if err != nil {
return nil, infraerrors.InternalServer("PENDING_AUTH_SESSION_UPDATE_FAILED", "failed to update pending oauth session").WithCause(err)
}
if _, err := h.ensurePendingOAuthAdoptionDecision(c, session.ID, decision); err != nil {
return nil, err
}
return session, nil
}
@@ -1365,12 +1388,20 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
email := strings.TrimSpace(strings.ToLower(req.Email))
existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email)
if err != nil && !errors.Is(err, service.ErrUserNotFound) {
response.ErrorFrom(c, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable"))
return
if err != nil {
switch {
case errors.Is(err, service.ErrUserNotFound):
existingUser = nil
case infraerrors.Code(err) >= http.StatusBadRequest && infraerrors.Code(err) < http.StatusInternalServerError:
response.ErrorFrom(c, err)
return
default:
response.ErrorFrom(c, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable"))
return
}
}
if existingUser != nil {
session, err = h.transitionPendingOAuthAccountToBindLogin(c, client, session, email, req.adoptionDecision())
session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, email)
if err != nil {
response.ErrorFrom(c, err)
return
@@ -1393,7 +1424,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
)
if err != nil {
if errors.Is(err, service.ErrEmailExists) {
session, err = h.transitionPendingOAuthAccountToBindLogin(c, client, session, email, req.adoptionDecision())
session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, email)
if err != nil {
response.ErrorFrom(c, err)
return
@@ -1548,6 +1579,7 @@ func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_COMPLETION_INVALID", "pending auth completion payload is invalid"))
return
}
payload = normalizePendingOAuthCompletionResponse(payload)
if strings.TrimSpace(session.RedirectTo) != "" {
if _, exists := payload["redirect"]; !exists {
payload["redirect"] = session.RedirectTo

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"`

View File

@@ -62,6 +62,8 @@ type wechatOAuthConfig struct {
scope string
redirectURI string
frontendCallback string
openEnabled bool
mpEnabled bool
}
type wechatOAuthTokenResponse struct {
@@ -209,11 +211,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
unionid := strings.TrimSpace(firstNonEmpty(userInfo.UnionID, tokenResp.UnionID))
openid := strings.TrimSpace(firstNonEmpty(userInfo.OpenID, tokenResp.OpenID))
if unionid == "" {
providerSubject := unionid
if providerSubject == "" {
if cfg.requiresUnionID() {
redirectOAuthError(c, frontendCallback, "provider_error", "wechat_missing_unionid", "")
return
}
providerSubject = openid
}
if providerSubject == "" {
redirectOAuthError(c, frontendCallback, "provider_error", "wechat_missing_unionid", "")
return
}
providerSubject := unionid
username := firstNonEmpty(userInfo.Nickname, wechatFallbackUsername(providerSubject))
email := wechatSyntheticEmail(providerSubject)
@@ -284,7 +293,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
}
if h.isForceEmailOnThirdPartySignup(c.Request.Context()) {
if err := h.createOAuthEmailRequiredPendingSession(c, identityRef, redirectTo, browserSessionKey, upstreamClaims); err != nil {
if err := h.createWeChatChoicePendingSession(
c,
identityRef,
email,
email,
redirectTo,
browserSessionKey,
upstreamClaims,
"",
nil,
true,
); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
@@ -292,17 +312,18 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
return
}
tokenPair, _, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, "")
if err != nil {
if err := h.createWeChatPendingSession(c, normalizedIntent, providerSubject, email, redirectTo, browserSessionKey, upstreamClaims, tokenPair, err, nil); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
redirectToFrontendCallback(c, frontendCallback)
return
}
if err := h.createWeChatPendingSession(c, normalizedIntent, providerSubject, email, redirectTo, browserSessionKey, upstreamClaims, tokenPair, nil, nil); err != nil {
if err := h.createWeChatChoicePendingSession(
c,
identityRef,
email,
email,
redirectTo,
browserSessionKey,
upstreamClaims,
"",
nil,
false,
); err != nil {
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
return
}
@@ -600,6 +621,65 @@ func (h *AuthHandler) createWeChatPendingSession(
})
}
func (h *AuthHandler) createWeChatChoicePendingSession(
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 {
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,
})
}
func (h *AuthHandler) createWeChatBindPendingSession(
c *gin.Context,
cfg wechatOAuthConfig,
@@ -874,7 +954,7 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
if err != nil {
return wechatOAuthConfig{}, err
}
if effective.Mode != mode {
if !effective.SupportsMode(mode) {
return wechatOAuthConfig{}, infraerrors.NotFound("OAUTH_DISABLED", "wechat oauth is disabled")
}
@@ -884,7 +964,9 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
appSecret: strings.TrimSpace(effective.AppSecret),
redirectURI: firstNonEmpty(strings.TrimSpace(effective.RedirectURL), resolveWeChatOAuthAbsoluteURL(apiBaseURL, c, "/api/v1/auth/oauth/wechat/callback")),
frontendCallback: firstNonEmpty(strings.TrimSpace(effective.FrontendRedirectURL), wechatOAuthDefaultFrontendCB),
scope: firstNonEmpty(strings.TrimSpace(effective.Scopes), service.DefaultWeChatConnectScopesForMode(mode)),
scope: effective.ScopeForMode(mode),
openEnabled: effective.OpenEnabled,
mpEnabled: effective.MPEnabled,
}
switch mode {
@@ -900,6 +982,10 @@ func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string,
return cfg, nil
}
func (cfg wechatOAuthConfig) requiresUnionID() bool {
return cfg.openEnabled && cfg.mpEnabled
}
func (h *AuthHandler) wechatOAuthFrontendCallback(ctx context.Context) string {
if h != nil && h.settingSvc != nil {
cfg, err := h.settingSvc.GetWeChatConnectOAuthConfig(ctx)

View File

@@ -65,6 +65,36 @@ func TestWeChatOAuthStartRedirectsAndSetsPendingCookies(t *testing.T) {
require.NotEmpty(t, findCookie(cookies, oauthPendingBrowserCookieName))
}
func TestWeChatOAuthStart_AllowsOpenModeWhenBothCapabilitiesEnabled(t *testing.T) {
gin.SetMode(gin.TestMode)
handler, client := newWeChatOAuthTestHandlerWithSettings(t, false, map[string]string{
service.SettingKeyWeChatConnectEnabled: "true",
service.SettingKeyWeChatConnectAppID: "wx-shared-app",
service.SettingKeyWeChatConnectAppSecret: "wx-shared-secret",
service.SettingKeyWeChatConnectMode: "mp",
service.SettingKeyWeChatConnectScopes: "snsapi_base",
service.SettingKeyWeChatConnectOpenEnabled: "true",
service.SettingKeyWeChatConnectMPEnabled: "true",
service.SettingKeyWeChatConnectRedirectURL: "https://api.example.com/api/v1/auth/oauth/wechat/callback",
service.SettingKeyWeChatConnectFrontendRedirectURL: "/auth/wechat/callback",
})
defer client.Close()
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/auth/oauth/wechat/start?mode=open&redirect=/billing", nil)
c.Request.Host = "api.example.com"
handler.WeChatOAuthStart(c)
require.Equal(t, http.StatusFound, recorder.Code)
location := recorder.Header().Get("Location")
require.NotEmpty(t, location)
require.Contains(t, location, "open.weixin.qq.com")
require.Contains(t, location, "connect/qrconnect")
require.Contains(t, location, "scope=snsapi_login")
}
func TestWeChatOAuthCallbackCreatesPendingSessionForUnifiedFlow(t *testing.T) {
originalAccessTokenURL := wechatOAuthAccessTokenURL
originalUserInfoURL := wechatOAuthUserInfoURL