Skip to content

Commit

Permalink
Merge pull request #2 from Limit-LAB/lxw/wechat-provider
Browse files Browse the repository at this point in the history
feat: wechat login in [wip]
  • Loading branch information
lxw665 authored Jun 25, 2024
2 parents 5e9a361 + 05656ab commit fe8259f
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 80 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ www/.DS_Store
www/node_modules
npm-debug.log
.data
.idea/
28 changes: 9 additions & 19 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,16 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.4
golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.23.0
golang.org/x/oauth2 v0.6.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)

require (
github.com/Bowery/prompt v0.0.0-20190916142128-fa8279994f75 // indirect
github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 // indirect
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
github.com/gobuffalo/nulls v0.4.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kardianos/govendor v1.0.9 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/lib/pq v1.10.9 // indirect
)

require (
Expand Down Expand Up @@ -95,7 +86,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/gobuffalo/envy v1.10.2 // indirect
Expand All @@ -107,12 +98,11 @@ require (
github.com/gobuffalo/tags/v3 v3.1.4 // indirect
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.61
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
Expand Down Expand Up @@ -145,10 +135,10 @@ require (
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
Expand Down
79 changes: 18 additions & 61 deletions go.sum

Large diffs are not rendered by default.

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
206 changes: 206 additions & 0 deletions internal/api/provider/wechat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package provider

import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/supabase/gotrue/internal/conf"
"golang.org/x/oauth2"
"log"
"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
)

type WechatCacheMapValue struct {
IsScanned bool
WechatUnionId string
}

type Config struct {
AppID string
Secret string
Endpoint oauth2.Endpoint
RedirectURL string
Scopes []string
}

type weChatProvider struct {
Client *http.Client
*oauth2.Config
}

type WechatUser struct {
Openid string `json:"openid"` // The ID of an ordinary user, which is unique to the current developer account
Nickname string `json:"nickname"` // Ordinary user nickname
Sex int `json:"sex"` // Ordinary user gender, 1 is male, 2 is female
Language string `json:"language"`
City string `json:"city"` // City filled in by general user's personal data
Province string `json:"province"` // Province filled in by ordinary user's personal information
Country string `json:"country"` // Country, such as China is CN
Headimgurl string `json:"headimgurl"` // User avatar, the last value represents the size of the square avatar (there are optional values of 0, 46, 64, 96, 132, 0 represents a 640*640 square avatar), this item is empty when the user does not have an avatar
Privilege []string `json:"privilege"` // User Privilege information, json array, such as Wechat Woka user (chinaunicom)
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 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() {
err := tokenResponse.Body.Close()
if err != nil {
log.Fatalf("Failed to close response body: %v", err)
return
}
}()
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() {
err := resp.Body.Close()
if err != nil {
log.Fatalf("Failed to close response body: %v", err)
return
}
}()
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 mapGender(sex int) string {
switch sex {
case 0:
return "male"
case 1:
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 fe8259f

Please sign in to comment.