feat: add profile auth identity binding flow
This commit is contained in:
@@ -12,6 +12,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dbent "github.com/Wei-Shaw/sub2api/ent"
|
||||
"github.com/Wei-Shaw/sub2api/ent/authidentity"
|
||||
"github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||||
@@ -27,6 +30,7 @@ const (
|
||||
wechatOAuthRedirectCookieName = "wechat_oauth_redirect"
|
||||
wechatOAuthIntentCookieName = "wechat_oauth_intent"
|
||||
wechatOAuthModeCookieName = "wechat_oauth_mode"
|
||||
wechatOAuthBindUserCookieName = "wechat_oauth_bind_user"
|
||||
wechatOAuthDefaultRedirectTo = "/dashboard"
|
||||
wechatOAuthDefaultFrontendCB = "/auth/wechat/callback"
|
||||
wechatOAuthProviderKey = "wechat-main"
|
||||
@@ -105,6 +109,16 @@ func (h *AuthHandler) WeChatOAuthStart(c *gin.Context) {
|
||||
wechatSetCookie(c, wechatOAuthModeCookieName, encodeCookieValue(cfg.mode), wechatOAuthCookieMaxAgeSec, secureCookie)
|
||||
setOAuthPendingBrowserCookie(c, browserSessionKey, secureCookie)
|
||||
clearOAuthPendingSessionCookie(c, secureCookie)
|
||||
if intent == oauthIntentBindCurrentUser {
|
||||
bindCookieValue, err := h.buildOAuthBindUserCookieFromContext(c)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
wechatSetCookie(c, wechatOAuthBindUserCookieName, encodeCookieValue(bindCookieValue), wechatOAuthCookieMaxAgeSec, secureCookie)
|
||||
} else {
|
||||
wechatClearCookie(c, wechatOAuthBindUserCookieName, secureCookie)
|
||||
}
|
||||
|
||||
authURL, err := buildWeChatAuthorizeURL(cfg, state)
|
||||
if err != nil {
|
||||
@@ -138,6 +152,7 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
|
||||
wechatClearCookie(c, wechatOAuthRedirectCookieName, secureCookie)
|
||||
wechatClearCookie(c, wechatOAuthIntentCookieName, secureCookie)
|
||||
wechatClearCookie(c, wechatOAuthModeCookieName, secureCookie)
|
||||
wechatClearCookie(c, wechatOAuthBindUserCookieName, secureCookie)
|
||||
}()
|
||||
|
||||
expectedState, err := readCookieDecoded(c, wechatOAuthStateCookieName)
|
||||
@@ -193,13 +208,33 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
|
||||
"openid": openid,
|
||||
"unionid": unionid,
|
||||
"mode": cfg.mode,
|
||||
"channel": cfg.mode,
|
||||
"channel_app_id": strings.TrimSpace(cfg.appID),
|
||||
"channel_subject": openid,
|
||||
"suggested_display_name": strings.TrimSpace(userInfo.Nickname),
|
||||
"suggested_avatar_url": strings.TrimSpace(userInfo.HeadImgURL),
|
||||
}
|
||||
|
||||
normalizedIntent := normalizeWeChatOAuthIntent(intent)
|
||||
if normalizedIntent == wechatOAuthIntentBind {
|
||||
if err := h.createWeChatBindPendingSession(c, cfg, providerSubject, openid, redirectTo, browserSessionKey, upstreamClaims); err != nil {
|
||||
switch infraerrors.Code(err) {
|
||||
case http.StatusConflict:
|
||||
redirectOAuthError(c, frontendCallback, "ownership_conflict", infraerrors.Reason(err), infraerrors.Message(err))
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
redirectOAuthError(c, frontendCallback, "auth_required", infraerrors.Reason(err), infraerrors.Message(err))
|
||||
default:
|
||||
redirectOAuthError(c, frontendCallback, "session_error", infraerrors.Reason(err), infraerrors.Message(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
redirectToFrontendCallback(c, frontendCallback)
|
||||
return
|
||||
}
|
||||
|
||||
tokenPair, _, err := h.authService.LoginOrRegisterOAuthWithTokenPair(c.Request.Context(), email, username, "")
|
||||
if err != nil {
|
||||
if err := h.createWeChatPendingSession(c, normalizeWeChatOAuthIntent(intent), providerSubject, email, redirectTo, browserSessionKey, upstreamClaims, tokenPair, err); 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
|
||||
}
|
||||
@@ -207,7 +242,7 @@ func (h *AuthHandler) WeChatOAuthCallback(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.createWeChatPendingSession(c, normalizeWeChatOAuthIntent(intent), providerSubject, email, redirectTo, browserSessionKey, upstreamClaims, tokenPair, nil); err != nil {
|
||||
if err := h.createWeChatPendingSession(c, normalizedIntent, providerSubject, email, redirectTo, browserSessionKey, upstreamClaims, tokenPair, nil, nil); err != nil {
|
||||
redirectOAuthError(c, frontendCallback, "session_error", "failed to continue oauth login", "")
|
||||
return
|
||||
}
|
||||
@@ -309,6 +344,7 @@ func (h *AuthHandler) createWeChatPendingSession(
|
||||
upstreamClaims map[string]any,
|
||||
tokenPair *service.TokenPair,
|
||||
authErr error,
|
||||
targetUserID *int64,
|
||||
) error {
|
||||
completionResponse := map[string]any{
|
||||
"redirect": redirectTo,
|
||||
@@ -333,6 +369,7 @@ func (h *AuthHandler) createWeChatPendingSession(
|
||||
ProviderKey: wechatOAuthProviderKey,
|
||||
ProviderSubject: providerSubject,
|
||||
},
|
||||
TargetUserID: targetUserID,
|
||||
ResolvedEmail: email,
|
||||
RedirectTo: redirectTo,
|
||||
BrowserSessionKey: browserSessionKey,
|
||||
@@ -341,6 +378,106 @@ func (h *AuthHandler) createWeChatPendingSession(
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) createWeChatBindPendingSession(
|
||||
c *gin.Context,
|
||||
cfg wechatOAuthConfig,
|
||||
providerSubject string,
|
||||
channelSubject string,
|
||||
redirectTo string,
|
||||
browserSessionKey string,
|
||||
upstreamClaims map[string]any,
|
||||
) error {
|
||||
currentUser, err := h.readOAuthBindTargetUser(c, wechatOAuthBindUserCookieName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.ensureWeChatBindOwnership(c.Request.Context(), currentUser.ID, providerSubject, cfg, channelSubject); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.createWeChatPendingSession(
|
||||
c,
|
||||
wechatOAuthIntentBind,
|
||||
providerSubject,
|
||||
currentUser.Email,
|
||||
redirectTo,
|
||||
browserSessionKey,
|
||||
upstreamClaims,
|
||||
nil,
|
||||
nil,
|
||||
¤tUser.ID,
|
||||
)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) readOAuthBindTargetUser(c *gin.Context, cookieName string) (*dbent.User, error) {
|
||||
client := h.entClient()
|
||||
if client == nil {
|
||||
return nil, infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
|
||||
}
|
||||
userID, err := h.readOAuthBindUserIDFromCookie(c, cookieName)
|
||||
if err != nil {
|
||||
return nil, infraerrors.Unauthorized("AUTH_REQUIRED", "current user is required to bind wechat account")
|
||||
}
|
||||
userEntity, err := client.User.Get(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
if dbent.IsNotFound(err) {
|
||||
return nil, infraerrors.Unauthorized("AUTH_REQUIRED", "current user is required to bind wechat account")
|
||||
}
|
||||
return nil, infraerrors.InternalServer("WECHAT_BIND_USER_LOOKUP_FAILED", "failed to load current user").WithCause(err)
|
||||
}
|
||||
return userEntity, nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ensureWeChatBindOwnership(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
providerSubject string,
|
||||
cfg wechatOAuthConfig,
|
||||
channelSubject string,
|
||||
) error {
|
||||
client := h.entClient()
|
||||
if client == nil {
|
||||
return infraerrors.ServiceUnavailable("PENDING_AUTH_NOT_READY", "pending auth service is not ready")
|
||||
}
|
||||
|
||||
identity, err := client.AuthIdentity.Query().
|
||||
Where(
|
||||
authidentity.ProviderTypeEQ("wechat"),
|
||||
authidentity.ProviderKeyEQ(wechatOAuthProviderKey),
|
||||
authidentity.ProviderSubjectEQ(strings.TrimSpace(providerSubject)),
|
||||
).
|
||||
Only(ctx)
|
||||
if err != nil && !dbent.IsNotFound(err) {
|
||||
return infraerrors.InternalServer("WECHAT_BIND_LOOKUP_FAILED", "failed to inspect wechat identity ownership").WithCause(err)
|
||||
}
|
||||
if identity != nil && identity.UserID != userID {
|
||||
return infraerrors.Conflict("AUTH_IDENTITY_OWNERSHIP_CONFLICT", "auth identity already belongs to another user")
|
||||
}
|
||||
|
||||
channelSubject = strings.TrimSpace(channelSubject)
|
||||
channelAppID := strings.TrimSpace(cfg.appID)
|
||||
if channelSubject == "" || channelAppID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
channel, err := client.AuthIdentityChannel.Query().
|
||||
Where(
|
||||
authidentitychannel.ProviderTypeEQ("wechat"),
|
||||
authidentitychannel.ProviderKeyEQ(wechatOAuthProviderKey),
|
||||
authidentitychannel.ChannelEQ(strings.TrimSpace(cfg.mode)),
|
||||
authidentitychannel.ChannelAppIDEQ(channelAppID),
|
||||
authidentitychannel.ChannelSubjectEQ(channelSubject),
|
||||
).
|
||||
WithIdentity().
|
||||
Only(ctx)
|
||||
if err != nil && !dbent.IsNotFound(err) {
|
||||
return infraerrors.InternalServer("WECHAT_BIND_CHANNEL_LOOKUP_FAILED", "failed to inspect wechat identity channel ownership").WithCause(err)
|
||||
}
|
||||
if channel != nil && channel.Edges.Identity != nil && channel.Edges.Identity.UserID != userID {
|
||||
return infraerrors.Conflict("AUTH_IDENTITY_CHANNEL_OWNERSHIP_CONFLICT", "auth identity channel already belongs to another user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AuthHandler) getWeChatOAuthConfig(ctx context.Context, rawMode string, c *gin.Context) (wechatOAuthConfig, error) {
|
||||
mode, err := resolveWeChatOAuthMode(rawMode, c)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user