From da48468ff181682bfa9ef473db55bac29b1eaeab Mon Sep 17 00:00:00 2001 From: lxw665 <62870122+lxw665@users.noreply.github.com> Date: Fri, 21 Jun 2024 16:23:53 +0800 Subject: [PATCH] feat: implement wechat login in --- internal/api/external.go | 2 + internal/api/provider/provider.go | 1 + internal/api/provider/wechat.go | 194 +++++++++++++++++++++++++----- internal/conf/configuration.go | 1 + 4 files changed, 170 insertions(+), 28 deletions(-) diff --git a/internal/api/external.go b/internal/api/external.go index ca817ffb4a..74a51c051f 100644 --- a/internal/api/external.go +++ b/internal/api/external.go @@ -558,6 +558,8 @@ func (a *API) Provider(ctx context.Context, name string, scopes string) (provide return provider.NewWorkOSProvider(config.External.WorkOS) case "zoom": return provider.NewZoomProvider(config.External.Zoom) + case "wechat": + return provider.NewWeChatProvider(config.External.WeChat) default: return nil, fmt.Errorf("Provider %s could not be found", name) } diff --git a/internal/api/provider/provider.go b/internal/api/provider/provider.go index b72f372544..e5d4c423db 100644 --- a/internal/api/provider/provider.go +++ b/internal/api/provider/provider.go @@ -54,6 +54,7 @@ type Claims struct { EmailVerified bool `json:"email_verified,omitempty" structs:"email_verified,omitempty"` Phone string `json:"phone,omitempty" structs:"phone,omitempty"` PhoneVerified bool `json:"phone_verified,omitempty" structs:"phone_verified,omitempty"` + Id string `json:"id,omitempty" structs:"id,omitempty"` // Custom profile claims that are provider specific CustomClaims map[string]interface{} `json:"custom_claims,omitempty" structs:"custom_claims,omitempty"` diff --git a/internal/api/provider/wechat.go b/internal/api/provider/wechat.go index 3c55cafd9f..631b980247 100644 --- a/internal/api/provider/wechat.go +++ b/internal/api/provider/wechat.go @@ -1,13 +1,29 @@ package provider import ( + "bytes" "context" + "encoding/json" + "fmt" "github.com/supabase/gotrue/internal/conf" "golang.org/x/oauth2" + "io" "net/http" + "net/url" + "strings" "sync" + "time" ) +type WechatAccessToken struct { + AccessToken string `json:"access_token"` // Interface call credentials + ExpiresIn int64 `json:"expires_in"` // access_token interface call credential timeout time, unit (seconds) + RefreshToken string `json:"refresh_token"` // User refresh access_token + Openid string `json:"openid"` // Unique ID of authorized user + Scope string `json:"scope"` // The scope of user authorization, separated by commas. (,) + Unionid string `json:"unionid"` // This field will appear if and only if the website application has been authorized by the user's UserInfo. +} + var ( WechatCacheMap map[string]WechatCacheMapValue Lock sync.RWMutex @@ -18,9 +34,17 @@ type WechatCacheMapValue struct { WechatUnionId string } +type Config struct { + AppID string + Secret string + Endpoint oauth2.Endpoint + RedirectURL string + Scopes []string +} + type weChatProvider struct { Client *http.Client - Config *oauth2.Config + *oauth2.Config } type WechatUser struct { @@ -36,35 +60,149 @@ type WechatUser struct { Unionid string `json:"unionid"` // Unified user identification. For an application under a WeChat open platform account, the unionid of the same user is unique. } -func NewWeChatIdProvider(clientId string, clientSecret string, redirectUrl string, ext conf.OAuthProviderConfiguration, scopes string) (OAuthProvider, error) { - //idp := &WeChatIdProvider{} - // - //config := idp.getConfig(clientId, clientSecret, redirectUrl) - //idp.Config = config - //if scopes == "" { - // scopes = "logs" - //} - // - //return &weChatProvider{ - // Config: &oauth2.Config{ - // ClientID: ext.ClientID[0], - // ClientSecret: ext.Secret, - // Endpoint: oauth2.Endpoint{ - // AuthURL: authHost + "/login/oauth/authorize", - // TokenURL: authHost + "/login/oauth/access_token", - // }, - // RedirectURL: ext.RedirectURI, - // Scopes: oauthScopes, - // }, - // APIHost: apiHost, - //}, nil - return nil, nil +func NewWeChatProvider(ext conf.OAuthProviderConfiguration) (OAuthProvider, error) { + + return &weChatProvider{ + Config: &oauth2.Config{ + ClientID: ext.ClientID[0], + ClientSecret: ext.Secret, + RedirectURL: ext.RedirectURI, + }, + }, nil +} + +func (idp weChatProvider) GetOAuthToken(code string) (*oauth2.Token, error) { + if strings.HasPrefix(code, "wechat_oa:") { + token := oauth2.Token{ + AccessToken: code, + TokenType: "WeChatAccessToken", + Expiry: time.Time{}, + } + return &token, nil + } + + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("appid", idp.Config.ClientID) + params.Add("secret", idp.Config.ClientSecret) + params.Add("code", code) + + accessTokenUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?%s", params.Encode()) + tokenResponse, err := idp.Client.Get(accessTokenUrl) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(tokenResponse.Body) + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(tokenResponse.Body) + if err != nil { + return nil, err + } + + if strings.Contains(buf.String(), "errcode") { + return nil, fmt.Errorf(buf.String()) + } + + var wechatAccessToken WechatAccessToken + if err = json.Unmarshal(buf.Bytes(), &wechatAccessToken); err != nil { + return nil, err + } + + token := oauth2.Token{ + AccessToken: wechatAccessToken.AccessToken, + TokenType: "WeChatAccessToken", + RefreshToken: wechatAccessToken.RefreshToken, + Expiry: time.Time{}, + } + + raw := make(map[string]string) + raw["Openid"] = wechatAccessToken.Openid + token.WithExtra(raw) + + return &token, nil +} + +func (idp weChatProvider) GetUserData(ctx context.Context, token *oauth2.Token) (*UserProvidedData, error) { + var wechatUser WechatUser + if strings.HasPrefix(token.AccessToken, "wechat_oa:") { + Lock.RLock() + mapValue, ok := WechatCacheMap[token.AccessToken[10:]] + Lock.RUnlock() + + if !ok || mapValue.WechatUnionId == "" { + return nil, fmt.Errorf("error ticket") + } + + Lock.Lock() + delete(WechatCacheMap, token.AccessToken[10:]) + Lock.Unlock() + + userInfo := UserProvidedData{ + Metadata: &Claims{ + Id: mapValue.WechatUnionId, + }, + } + return &userInfo, nil + } + openid := token.Extra("Openid") + + userInfoUrl := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s", token.AccessToken, openid) + resp, err := idp.Client.Get(userInfoUrl) + if err != nil { + return nil, fmt.Errorf("get user info error: %v", err) + + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return nil, err + } + if err = json.Unmarshal(buf.Bytes(), &wechatUser); err != nil { + return nil, err + } + + id := wechatUser.Unionid + if id == "" { + id = wechatUser.Openid + } + email := make([]Email, 0) + email = append(email, Email{Email: wechatUser.Openid, Verified: true, Primary: true}) + userData := UserProvidedData{ + Emails: email, + Metadata: &Claims{ + Id: id, + NickName: wechatUser.Nickname, + Name: wechatUser.Nickname, + Picture: wechatUser.Headimgurl, + Gender: mapGender(wechatUser.Sex), + Email: wechatUser.Openid, + }, + } + return &userData, nil } -func (p weChatProvider) GetOAuthToken(code string) (*oauth2.Token, error) { - return nil, nil +func (idp weChatProvider) RevokeToken(token *oauth2.Token) error { + return nil } -func (p weChatProvider) GetUserData(ctx context.Context, tok *oauth2.Token) (*UserProvidedData, error) { - return nil, nil +func mapGender(sex int) string { + switch sex { + case 1: + return "male" + case 2: + return "female" + default: + return "unknown" + } } diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index ca03843f41..f1e5891f7c 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -206,6 +206,7 @@ type ProviderConfiguration struct { Email EmailProviderConfiguration `json:"email"` Phone PhoneProviderConfiguration `json:"phone"` Zoom OAuthProviderConfiguration `json:"zoom"` + WeChat OAuthProviderConfiguration `json:"wechat"` IosBundleId string `json:"ios_bundle_id" split_words:"true"` RedirectURL string `json:"redirect_url"` AllowedIdTokenIssuers []string `json:"allowed_id_token_issuers" split_words:"true"`