Skip to content

Commit

Permalink
feat: implement wechat login in
Browse files Browse the repository at this point in the history
  • Loading branch information
lxw665 committed Jun 21, 2024
1 parent 3f34189 commit da48468
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 28 deletions.
2 changes: 2 additions & 0 deletions internal/api/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions internal/api/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
194 changes: 166 additions & 28 deletions internal/api/provider/wechat.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand All @@ -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"
}
}
1 change: 1 addition & 0 deletions internal/conf/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down

0 comments on commit da48468

Please sign in to comment.