diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f4dc7742e..77b466cb94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: test: strategy: matrix: - go-version: [1.20.x] + go-version: [1.21.x] runs-on: ubuntu-20.04 services: postgres: diff --git a/.gitignore b/.gitignore index 38a57a9698..24740f7709 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ www/.DS_Store www/node_modules npm-debug.log .data +.idea/ diff --git a/go.mod b/go.mod index 55e71dec8b..c70df3b346 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ 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 ) @@ -40,6 +40,8 @@ require ( require ( github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/gobuffalo/nulls v0.4.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/lib/pq v1.10.9 // indirect ) require ( @@ -84,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 @@ -96,7 +98,7 @@ 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 @@ -133,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 diff --git a/go.sum b/go.sum index a6b3f0fe77..eaa921ea55 100644 --- a/go.sum +++ b/go.sum @@ -137,8 +137,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -225,8 +225,9 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -239,8 +240,8 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= @@ -349,8 +350,9 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/luna-duclos/instrumentedsql v1.1.3 h1:t7mvC0z1jUt5A0UQ6I/0H31ryymuQRnJcWCiqV3lSAA= github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= @@ -561,8 +563,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -634,8 +636,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -657,8 +659,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -711,8 +714,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -726,8 +729,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20160926182426-711ca1cb8763/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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 new file mode 100644 index 0000000000..5f24ba692c --- /dev/null +++ b/internal/api/provider/wechat.go @@ -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" + } +} diff --git a/internal/api/sms_provider/huawei_cloud.go b/internal/api/sms_provider/huawei_cloud.go new file mode 100644 index 0000000000..f1125ef78c --- /dev/null +++ b/internal/api/sms_provider/huawei_cloud.go @@ -0,0 +1,257 @@ +package sms_provider + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + "time" + + "github.com/supabase/gotrue/internal/conf" +) + +// from https://obs.cn-north-1.myhuaweicloud.com/apig-sdk/APIGW-go-sdk.zip signal.go +const ( + DateFormat = "20060102T150405Z" + SignAlgorithm = "SDK-HMAC-SHA256" + HeaderXDateTime = "X-Sdk-Date" + HeaderXHost = "host" + HeaderXAuthorization = "Authorization" + HeaderXContentSha256 = "X-Sdk-Content-Sha256" +) + +func hmacsha256(keyByte []byte, dataStr string) ([]byte, error) { + hm := hmac.New(sha256.New, []byte(keyByte)) + if _, err := hm.Write([]byte(dataStr)); err != nil { + return nil, err + } + return hm.Sum(nil), nil +} + +// Build a CanonicalRequest from a regular request string +func CanonicalRequest(request *http.Request, signedHeaders []string) (string, error) { + var hexencode string + var err error + if hex := request.Header.Get(HeaderXContentSha256); hex != "" { + hexencode = hex + } else { + bodyData, err := RequestPayload(request) + if err != nil { + return "", err + } + hexencode, err = HexEncodeSHA256Hash(bodyData) + if err != nil { + return "", err + } + } + return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", request.Method, CanonicalURI(request), request.URL.RawQuery, CanonicalHeaders(request, signedHeaders), strings.Join(signedHeaders, ";"), hexencode), err +} + +// CanonicalURI returns request uri +func CanonicalURI(request *http.Request) string { + uri := request.URL.Path + if !strings.HasSuffix(uri, "/") { + uri += "/" + } + return uri +} + +// CanonicalHeaders +func CanonicalHeaders(request *http.Request, signerHeaders []string) string { + var canonicalHeaders []string + header := make(map[string][]string) + for k, v := range request.Header { + header[strings.ToLower(k)] = v + } + for _, key := range signerHeaders { + value := header[key] + if strings.EqualFold(key, HeaderXHost) { + value = []string{request.Host} + } + sort.Strings(value) + for _, v := range value { + canonicalHeaders = append(canonicalHeaders, key+":"+strings.TrimSpace(v)) + } + } + return fmt.Sprintf("%s\n", strings.Join(canonicalHeaders, "\n")) +} + +// SignedHeaders +func SignedHeaders(r *http.Request) []string { + var signedHeaders []string + for key := range r.Header { + signedHeaders = append(signedHeaders, strings.ToLower(key)) + } + sort.Strings(signedHeaders) + return signedHeaders +} + +// RequestPayload +func RequestPayload(request *http.Request) ([]byte, error) { + if request.Body == nil { + return []byte(""), nil + } + bodyByte, err := io.ReadAll(request.Body) + if err != nil { + return []byte(""), err + } + request.Body = io.NopCloser(bytes.NewBuffer(bodyByte)) + return bodyByte, err +} + +// Create a "String to Sign". +func StringToSign(canonicalRequest string, t time.Time) (string, error) { + hashStruct := sha256.New() + _, err := hashStruct.Write([]byte(canonicalRequest)) + if err != nil { + return "", err + } + return fmt.Sprintf("%s\n%s\n%x", + SignAlgorithm, t.UTC().Format(DateFormat), hashStruct.Sum(nil)), nil +} + +// Create the HWS Signature. +func SignStringToSign(stringToSign string, signingKey []byte) (string, error) { + hmsha, err := hmacsha256(signingKey, stringToSign) + return fmt.Sprintf("%x", hmsha), err +} + +// HexEncodeSHA256Hash returns hexcode of sha256 +func HexEncodeSHA256Hash(body []byte) (string, error) { + hashStruct := sha256.New() + if len(body) == 0 { + body = []byte("") + } + _, err := hashStruct.Write(body) + return fmt.Sprintf("%x", hashStruct.Sum(nil)), err +} + +// Get the finalized value for the "Authorization" header. The signature parameter is the output from SignStringToSign +func AuthHeaderValue(signatureStr, accessKeyStr string, signedHeaders []string) string { + return fmt.Sprintf("%s Access=%s, SignedHeaders=%s, Signature=%s", SignAlgorithm, accessKeyStr, strings.Join(signedHeaders, ";"), signatureStr) +} + +// Signature HWS meta +type Signer struct { + Key string + Secret string +} + +// SignRequest set Authorization header +func (s *Signer) Sign(request *http.Request) error { + var t time.Time + var err error + var date string + if date = request.Header.Get(HeaderXDateTime); date != "" { + t, err = time.Parse(DateFormat, date) + } + if err != nil || date == "" { + t = time.Now() + request.Header.Set(HeaderXDateTime, t.UTC().Format(DateFormat)) + } + signedHeaders := SignedHeaders(request) + canonicalRequest, err := CanonicalRequest(request, signedHeaders) + if err != nil { + return err + } + stringToSignStr, err := StringToSign(canonicalRequest, t) + if err != nil { + return err + } + signatureStr, err := SignStringToSign(stringToSignStr, []byte(s.Secret)) + if err != nil { + return err + } + authValueStr := AuthHeaderValue(signatureStr, s.Key, signedHeaders) + request.Header.Set(HeaderXAuthorization, authValueStr) + return nil +} + +type HuaweiCloudResponse struct { + Code string `json:"code"` + Description string `json:"description"` + Result []ResultItem `json:"result"` +} + +type ResultItem struct { + Total int `json:"total"` + OriginTo string `json:"originTo"` + CreateTime string `json:"createTime"` + From string `json:"from"` + SmsMsgId string `json:"smsMsgId"` + CountryId string `json:"countryId"` + Status string `json:"status"` +} + +type HuaweiCloudProvider struct { + Config *conf.HuaweiCloudProviderConfiguration +} + +// SendMessage implements SmsProvider. +// note that the `message` is the template ID which has been registered on Huawei Cloud +// see https://support.huaweicloud.com/intl/en-us/msgsms_faq/sms_faq_sec03.html +func (p *HuaweiCloudProvider) SendMessage(phone string, message string, channel string, otp string) (string, error) { + switch channel { + case SMSProvider: + return p.SendSms(phone, message, otp) + default: + return "", fmt.Errorf("channel type %q is not supported for Huawei Cloud Message & SMS service", channel) + } +} + +func (p *HuaweiCloudProvider) SendSms(phone, templateId, otp string) (string, error) { + params := buildRequestParams(p.Config.ChannelNumber, phone, templateId, otp, p.Config.ChannelName) + url := p.Config.ApiPath + "/sms/batchSendSms/v1" + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(params))) + if err != nil { + return "", err + } + + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + s := Signer{ + Key: p.Config.ApiKey, + Secret: p.Config.ApiSecret, + } + serr := s.Sign(req) + if serr != nil { + return "", serr + } + + client := &http.Client{Timeout: defaultTimeout} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + r := &HuaweiCloudResponse{} + derr := json.NewDecoder(resp.Body).Decode(r) + if derr != nil { + return "", derr + } + + if r.Code != "000000" || len(r.Result) == 0 { + return "", fmt.Errorf("error code: %s, %s", r.Code, r.Description) + } + + return r.Result[0].SmsMsgId, nil +} + +func buildRequestParams(sender, receiver, templateId, otp, signature string) string { + return fmt.Sprintf("from=%s&to=%s&templateId=%s&templateParas=%s&signature=%s", + url.QueryEscape(sender), url.QueryEscape(receiver), + url.QueryEscape(templateId), url.QueryEscape(fmt.Sprintf("[\"%s\"]", otp)), + url.QueryEscape(signature)) +} + +func NewHuaweiCloudProvider(config conf.HuaweiCloudProviderConfiguration) (SmsProvider, error) { + return &HuaweiCloudProvider{ + Config: &config, + }, nil +} diff --git a/internal/api/sms_provider/sms_provider.go b/internal/api/sms_provider/sms_provider.go index 4f4a0e138e..efabd8f4ac 100644 --- a/internal/api/sms_provider/sms_provider.go +++ b/internal/api/sms_provider/sms_provider.go @@ -41,6 +41,8 @@ func GetSmsProvider(config conf.GlobalConfiguration) (SmsProvider, error) { return NewVonageProvider(config.Sms.Vonage) case "twilio_verify": return NewTwilioVerifyProvider(config.Sms.TwilioVerify) + case "huawei_cloud": + return NewHuaweiCloudProvider(config.Sms.HuaweiCloud) default: return nil, fmt.Errorf("sms Provider %s could not be found", name) } diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index b6563fd027..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"` @@ -256,6 +257,7 @@ type SmsProviderConfiguration struct { Messagebird MessagebirdProviderConfiguration `json:"messagebird"` Textlocal TextlocalProviderConfiguration `json:"textlocal"` Vonage VonageProviderConfiguration `json:"vonage"` + HuaweiCloud HuaweiCloudProviderConfiguration `json:"huawei_cloud" split_words:"true"` } func (c *SmsProviderConfiguration) GetTestOTP(phone string, now time.Time) (string, bool) { @@ -296,6 +298,14 @@ type VonageProviderConfiguration struct { From string `json:"from" split_words:"true"` } +type HuaweiCloudProviderConfiguration struct { + ApiKey string `json:"api_key" split_words:"true"` + ApiSecret string `json:"secret" split_words:"true"` + ApiPath string `json:"api_path" split_words:"true"` + ChannelName string `json:"channel_name" split_words:"true"` + ChannelNumber string `json:"channel_number" split_words:"true"` +} + type CaptchaConfiguration struct { Enabled bool `json:"enabled" default:"false"` Provider string `json:"provider" default:"hcaptcha"`