From de2932b5fbcb2bd1cc4156570826d3d786b1b59b Mon Sep 17 00:00:00 2001 From: LiusCraft <47062832+LiusCraft@users.noreply.github.com> Date: Sat, 20 Jan 2024 21:49:02 +0800 Subject: [PATCH] feat: use standalone Twitter OAuth provider instead of goth (#2632) --- idp/provider.go | 3 +- idp/twitter.go | 190 +++++++++++++++++++++++++++++++++++++++ web/src/auth/Provider.js | 2 +- 3 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 idp/twitter.go diff --git a/idp/provider.go b/idp/provider.go index f5446ee6..959d85cf 100644 --- a/idp/provider.go +++ b/idp/provider.go @@ -119,6 +119,8 @@ func GetIdProvider(idpInfo *ProviderInfo, redirectUrl string) (IdProvider, error return NewMetaMaskIdProvider(), nil case "Web3Onboard": return NewWeb3OnboardIdProvider(), nil + case "Twitter": + return NewTwitterIdProvider(idpInfo.ClientId, idpInfo.ClientSecret, redirectUrl), nil default: if isGothSupport(idpInfo.Type) { return NewGothIdProvider(idpInfo.Type, idpInfo.ClientId, idpInfo.ClientSecret, idpInfo.ClientId2, idpInfo.ClientSecret2, redirectUrl, idpInfo.HostUrl) @@ -171,7 +173,6 @@ var gothList = []string{ "TikTok", "Tumblr", "Twitch", - "Twitter", "Typetalk", "Uber", "VK", diff --git a/idp/twitter.go b/idp/twitter.go new file mode 100644 index 00000000..1257fd9e --- /dev/null +++ b/idp/twitter.go @@ -0,0 +1,190 @@ +// Copyright 2021 The Casdoor Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type TwitterIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewTwitterIdProvider(clientId string, clientSecret string, redirectUrl string) *TwitterIdProvider { + idp := &TwitterIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *TwitterIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *TwitterIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + endpoint := oauth2.Endpoint{ + TokenURL: "https://api.twitter.com/2/oauth2/token", + } + + config := &oauth2.Config{ + Scopes: []string{"users.read", "tweet.read"}, + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type TwitterAccessToken struct { + AccessToken string `json:"access_token"` // Interface call credentials + TokenType string `json:"token_type"` // Access token type + ExpiresIn int64 `json:"expires_in"` // access_token interface call credential timeout time, unit (seconds) +} + +type TwitterCheckToken struct { + Data TwitterUserInfo `json:"data"` +} + +// TwitterCheckTokenData +// Get more detail via: https://developers.Twitter.com/docs/Twitter-login/guides/advanced/manual-flow#checktoken +type TwitterCheckTokenData struct { + UserId string `json:"user_id"` +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// get more detail via: https://developers.Twitter.com/docs/Twitter-login/guides/advanced/manual-flow#confirm +func (idp *TwitterIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + // params.Add("client_id", idp.Config.ClientID) + params.Add("redirect_uri", idp.Config.RedirectURL) + params.Add("code_verifier", "casdoor-verifier") + params.Add("code", code) + params.Add("grant_type", "authorization_code") + req, err := http.NewRequest("POST", "https://api.twitter.com/2/oauth2/token", strings.NewReader(params.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + e := base64.StdEncoding.EncodeToString([]byte(idp.Config.ClientID + ":" + idp.Config.ClientSecret)) + req.Header.Add("Authorization", "Basic "+e) + accessTokenResp, err := idp.GetUrlResp(req) + var TwitterAccessToken TwitterAccessToken + if err = json.Unmarshal([]byte(accessTokenResp), &TwitterAccessToken); err != nil { + return nil, err + } + + token := oauth2.Token{ + AccessToken: TwitterAccessToken.AccessToken, + TokenType: TwitterAccessToken.TokenType, + Expiry: time.Time{}, + } + + return &token, nil +} + +//{ +// "id": "123456789", +// "name": "Example Name", +// "name_format": "{first} {last}", +// "picture": { +// "data": { +// "height": 50, +// "is_silhouette": false, +// "url": "https://example.com", +// "width": 50 +// } +// }, +// "email": "test@example.com" +//} + +type TwitterUserInfo struct { + Id string `json:"id"` // The app user's App-Scoped User ID. This ID is unique to the app and cannot be used by other apps. + Name string `json:"name"` // The person's full name. + UserName string `json:"username"` // The person's name formatted to correctly handle Chinese, Japanese, or Korean ordering. + Picture struct { // The person's profile picture. + Data struct { // This struct is different as https://developers.Twitter.com/docs/graph-api/reference/user/picture/ + Height int `json:"height"` + IsSilhouette bool `json:"is_silhouette"` + Url string `json:"url"` + Width int `json:"width"` + } `json:"data"` + } `json:"picture"` + Email string `json:"email"` // The User's primary email address listed on their profile. This field will not be returned if no valid email address is available. +} + +// GetUserInfo use TwitterAccessToken gotten before return TwitterUserInfo +// get more detail via: https://developers.Twitter.com/docs/graph-api/reference/user +func (idp *TwitterIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var TwitterUserInfo TwitterUserInfo + // accessToken := token.AccessToken + + req, err := http.NewRequest("GET", "https://api.twitter.com/2/users/me", nil) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Authorization", "Bearer "+token.AccessToken) + // req.URL.Query().Set("user.fields", "profile_image_url") + // userIdUrl := fmt.Sprintf("https://graph.Twitter.com/me?access_token=%s", accessToken) + userIdResp, err := idp.GetUrlResp(req) + if err != nil { + return nil, err + } + empTwitterCheckToken := &TwitterCheckToken{} + if err = json.Unmarshal([]byte(userIdResp), &empTwitterCheckToken); err != nil { + return nil, err + } + TwitterUserInfo = empTwitterCheckToken.Data + + userInfo := UserInfo{ + Id: TwitterUserInfo.Id, + Username: TwitterUserInfo.UserName, + DisplayName: TwitterUserInfo.Name, + Email: TwitterUserInfo.Email, + AvatarUrl: TwitterUserInfo.Picture.Data.Url, + } + return &userInfo, nil +} + +func (idp *TwitterIdProvider) GetUrlResp(url *http.Request) (string, error) { + resp, err := idp.Client.Do(url) + if err != nil { + return "", 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 "", err + } + + return buf.String(), nil +} diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js index 0ae0ca9e..fe7f6a1a 100644 --- a/web/src/auth/Provider.js +++ b/web/src/auth/Provider.js @@ -282,7 +282,7 @@ const authInfo = { endpoint: "https://id.twitch.tv/oauth2/authorize", }, Twitter: { - scope: "users.read", + scope: "users.read%20tweet.read", endpoint: "https://twitter.com/i/oauth2/authorize", }, Typetalk: {